C/Pekere
I dette kapittelet skal vi se på noe av det som regnes som det "vanskeligste" i C; pekere. Som en effekt av det vil vi også for første gang se hvordan datatyper vi allerede har lært om i C relaterer til datamaskinens minne.
Alle variabler vi definerer i C får avsatt en plass i minnet til maskinen når programmet kjører. Hvor stor plass variabelen opptar i minnet, spørs hvilken type den er av. En int tar eksempelvis 4 bytes, mens en char tar 1. Størrelsen av datastrukturer og tabeller avhenger av hvor mange felt strukturen har, og hvilken type de er av, eller hvor mange elementer tabellen har, og hvilken type de er av. Hver eneste byte i minnet har en egen adresse, som er et nummer som brukes for å finne frem til hver byte. Variabler som er større enn én byte har alltid adressen til den første byten den opptar i minnet. I C kan man opprette variabler som, i stedet for data, holder adressen til en annen variabel. En slik variabel kalles en pekervariabel eller en peker, fordi den "peker" til noe i minnet. Pekere brukes til å lese og skrive til/fra minnet direkte, uten å måtte referere til et spesielt variabelnavn på det som skal skrives til.
Grunnleggende: definisjon og dereferering
[rediger]En pekervariabel defineres med følgende syntaks:
datatype *navn;
Stjernen bak datatypen forteller at vi her definerer en pekervariabel som skal kunne peke til variabler av den datatypen, og ikke en vanlig variabel. For å definere en pekervariabel, p, som av typen peker til int gjør vi altså slik:
int *p;
Nå kan vi gi p en adresse. Det gjør vi også som når vi gir vanlige variabler en verdi, ved å bruke tilordningsoperatøren:
p = adresse;
Om vi absolutt vil kan vi oppgi en spesifikk adresse som p skal peke til. Det er dog en dårlig idé: når programmet startes blir det tildelt et minneområde av operativsystemet. Dersom en peker brukes utenfor det området, vil programmet terminere av sikkerhetsårsaker, for å hindre at programmet skriver til andre programmers minne. Dette gjør egentlig ikke noe, da det ikke er dette som er poenget med pekere (vi skal se på det snart.), men heller å peke dem til andre objekter i koden. For å gjøre dette må vi naturligvis finne adressen til det vi vil peke den til. Det gjør vi vha. adresseoperatøren; &. Dersom vi har en variabel, a, vil &a returnere adressen dens.
int a = 1234; int *p = &a;
Her definerer vi variabelen a, og gir den verdien 1234. Deretter oppretter vi pekervariabelen p, av typen int, og peker den til adressen til a. Nå kan vi bruke pekeren til å lese og skrive til den adressen den peker til. For å gjøre det må vi bruke enda en ny operatør, derefereringsoperatøren, *. Det er viktig å skille mellom denne operatøren og stjernen i definisjonen av pekeren. * tar en adresse, normalt i en peker, og returnerer det som ligger lagret i den adressen. *p leser vi da som "innholdet av adressen som p peker til". Følgende kode:
int a = 1234; int *p = &a; printf("%d\n", *p);
... vil printe 1234 på skjermen. p gis adressen der a ligger lagret i minnet -- p peker til a. Når p derefereres med *, vil vi da få innholdet av adressen som p peker til, som er innholdet av a: 1234. Forskjellen mellom p og *p er viktig:
p - adressen den peker til *p - innholdet i adressen den peker til &p - adressen til selve pekervariabelen
For å illustrere sammenhengen mellom det hele, kan vi se på et eksempelprogram som printer en del informasjon på skjermen:
#include <stdio.h> int main(void) { int a = 1234, *p = &a; printf("a inneholder %d\n", a); printf("a ligger i adresse %u\n", &a); printf("p inneholder %u\n", p); printf("Adressen som p peker til inneholder %d\n", *p); printf("p ligger i adresse %u\n", &p); return 0; }
Dette gir omtrent følgende utskrift:
a inneholder 1234 a ligger i adresse 3220238128 p inneholder 3220238128 Adressen som p peker til inneholder 1234 p ligger i adresse 3220238124
Her ser vi at a inneholder 1234, og ligger i adresse 3220238128. Vi ser også at innholdet til pekervariabelen p, men ikke adressen der den ligger i minnet, også er 3220238128. Når vi dereferer p, altså ser på det som ligger i adressen som p peker til, får vi også 1234, siden p peker til samme adresse som adressen til a. p i seg selv ligger i adresse 3220238124. Disse adressene er dog ikke faste; de varierer fra hver gang du kjører programmet. Grunnen til at vi printer adressene med %u-direktivet, er at adresser ikke kan holde negative verdier, og dermed må de printes som unsigned.
Vi kan også benytte tilordningsoperatøren på en dereferert peker:
int a = 1234, *p = &a; *p = 4321; printf("%d\n", a);
Her får vi 4321 printet på skjermen. Som før blir a opprettet og satt til 1234, p blir oprettet og satt til å peke til adressen til a. Deretter blir innholdet av minneadressen som p peker til, satt til 4321. Siden adressen som p peker til er den samme som adressen til a, blir også a endret av dette.
Pekere kan gis som argument til funksjoner, og returneres som returverdi av funksjoner. Spesielt førstnevnte er nyttig dersom vi vil omgå det faktum at C kopierer alle variabler (utenom tabeller), når de sendes som argument til funksjoner. Dersom vi vil at en funksjon skal kunne endre variabler den får som argumenter, kan vi benytte en peker:
#include <stdio.h> void sett_null(int *var) { *var = 0; return; } int main(void) { int a = 1234; printf("a inneholder %d\n", a); sett_null(&a); printf("a inneholder %d\n", a); return 0; }
Her definerer vi funksjonen sett_null som tar et argument -- en peker av typen int -- og setter innholdet av det pekeren peker til, til 0. I main definerer vi a og gir den 1234, printer a, og kaller sett_null med adressen til a som argument. Pekeren var i sett_null vil da peke til adressen til a, og dermed kunne endre innholdet av a, også utenfor main. Dette ser vi effekten av når a printes til slutt; a er da satt til 0.
Pekere og tabeller. Pekeraritmetikk
[rediger]Så langt virker kanskje pekere som en forvirrende måte å endre definerte variabler på, og en måte å gi funksjoner muligheten til å endre variabler i andre funksjoner. Nå skal vi se på pekere som en annen måte å behandle tabeller på, og samtidig vil vi se at tabeller og pekere har en sterk sammenheng.
Vi kan tenke oss at vi definerer en tabell med 5 elementer av typen int og initialiserer elementene:
int tabell[5] = {2,4,6,8,10};
Det som egentlig skjer her, er at 5 variabler av typen int, etter hverandre, gis plass i minnet, og en konstant pekervariabel (adressen i pekervariabelen kan ikke endres) med navnet tabell opprettes. tabell blir deretter gitt adressen til det første av de 5 elementene i minnet. Pekeren tabell peker altså til starten av tabellen. Dette kan vi illustrere slik:
+----+----+----+----+----+ const int* tabell | 2 | 4 | 6 | 8 | 10 | | +----+----+----+----+----+ -------------------^
tabell inneholder altså adressen til det første elementet i tabellen. Når vi dereferer den, får vi naturlig nok 2:
int tabell[5] = {2,4,6,8,10}; printf("%d\n", *tabell);
Dette gir 2 som utskrift på skjermen. Vi kan også utføre aritmetiske operasjoner på pekere. Det gjør det mulig å peke til andre steder i minnet, ved å legge til og trekke verdier fra adressen i pekeren. Å utføre aritmetiske operasjoner er dog ikke rett frem, slik som med vanlige variabler. For å komme til det neste elementet i tabellen kan det jo f.eks. virke logisk å legge 4 til adressen i tabell, i og med at det første elementet i tabellen er en int som tar opp 4 bytes (og dermed fire adresser, siden hver adresse går til én byte.) Slik er det ikke; når kompilatoren ser et aritmetisk uttrykk der en pekervariabel er involvert, vil den se på typen til pekeren, og legge til størrelsen av x antall elementer og ikke x antall bytes. Uttrykket tabell + 1 vil da resultere i adressen i tabell + 4, eller den fjerde adressen etter adressen i tabell. Dette er fordi tabell er av typen peker til int. En int er 4 bytes stor, og dermed vil ett element føre til at 4 blir lagt til adressen. På samme måte vil uttrykket tabell + 3 føre til adressen i tabell + 12. For å dereferere det tredje elementet i tabellen gjør vi altså følgende:
int tabell[5] = {2,4,6,8,10}; printf("%d\n", *(tabell + 2));
Her må vi legge paranteser rundt det vi vil skal være pekeren som skal derefereres. Grunnen til det er at * har høyere operatørprioritet enn +, som gjør at derefereringen blir gjort før 2 blir lagt til adressen i tabell. Adressen som til slutt blir dereferert med * er summen av adressen i tabell og størrelsen av to int-elementer. Dermed får vi adressen til det tredje elementet, og når denne dereferes får vi tallet 6. Vi kan illustrere tabell + 2 slik:
+----+----+----+----+----+ tabell + 2 | 2 | 4 | 6 | 8 | 10 | | +----+----+----+----+----+ --------------------------^
Siden tabellpekere er konstante, kan vi ikke endre adressen som ligger lagret i dem. Det er dog lett å løse, ved å definere en ny peker, og sette adressen i denne til den samme som er i tabellpekeren:
int tabell[5] = {2,4,6,8,10}; int* p; for (p = tabell; p < tabell + 5; p++) printf("%d\n", *p);
Her oppretter vi også en peker, p. I initialiseringsdelen av for-løkka setter vi den til å peke til adressen som tabell peker til, som er adressen til det første elementet i tabellen. Merk at vi ikke bruker adresseoperatøren på tabell, siden vi faktisk vil ha innholdet av den (adressen den peker til). Hadde vi hatt & foran, ville vi fått adressen til selve pekervariabelen tabell. Løkka vil gå så lenge adressen i p er mindre enn adressen i tabell + størrelsen av 5 int-elementer. Etter hver gjennomgang økes adressen i p med størrelsen av et int-element, altså 4, slik at den peker til neste element i tabellen. Om vi vil kan vi også bruke indekseringsoperatøren til det samme formålet:
int tabell[5] = {2,4,6,8,10}; int* p = tabell, i; for (i = 0; i < 5; i++) printf("%d\n", p[i]);
Her ser vi at indekseringsoperatøren ikke har en direkte sammenheng med tabeller, den kan også brukes på «vanlige» pekere, for å referere til innholdet i en adresse:
*(p + x);
og
p[x];
... er likeverdige uttrykk.
I kapittelet om funksjoner så vi hvordan vi kan gi tabeller som argument til funksjoner. Da definerte vi funksjonen som noe slikt:
void f(int tabell[]) { ... }
Vi oppgir altså ikke hvor stor tabellen er. Grunnen til det er at vi skal kunne gi tabeller av variabel størrelse til funksjoner. Når en funksjon kalles med en tabell som argument, blir tabellen aldri sendt til funksjonen. I stedet blir en peker til tabellen gitt. Med andre ord er følgende definisjon av funksjonen like gyldig, og kan brukes på akkurat samme måte:
void f(int* tabell) { ... }
Nå bør det også gå litt opp for en hvorfor scanf trenger adresseoperatøren, &, foran målvariabelen når det skal leses inn tall og bokstaver, og hvorfor man ikke trenger det når man skal lese inn strenger:
char bokstav; char streng[20]; scanf("%c", &bokstav); scanf("%s", streng);
Siden bokstav er en variabel av typen char, må scanf vite hvor, altså i hvilken adresse, den ligger. Bare variabelnavnet, uten adresseoperatøren foran, gir bare verdien den har når scanf blir kalt. Det neste scanf-uttrykket derimot, skal lese inn en streng. Siden streng er en peker til en tabell av typen char, trenger vi ikke oppgi noen adresse til scanf -- pekeren i seg selv gir jo adressen til det første elementet av strengen.
Pekere til datastrukturer
[rediger]Så å si alle datatyper kan defineres pekere til -- også egne datastrukturer. Dersom vi har en struktur, s, definerer vi pekere av den slik:
struct s* p;
Om vi har typedefinert struct s { ...} til s definerer vi pekere slik:
s* p;
Nå kan p gis adressen til en datastruktur:
struct s { int a, b; }; struct s test; struct s* p = &test;
For å dereferere et felt i en struktur som en strukturpeker peker til, gjør vi følgende:
(*p).a;
Grunnen til det er at medlemsoperatøren, . har høyere prioritet enn derefereringsoperatøren. Dermed ville vi, om vi ikke hadde hatt paranteser rundt, prøvd å finne feltet a i pekervariabelen, noe den ikke har. Ved å sette paranteser rundt derefereringsoperatøren, vil det skje først, og resultere i strukturobjektet som p peker til, som deretter kan benyttes med medlemsoperatøren. Denne formen for dereferering av feltene til strukturpekere er såpass mye brukt at C har en enklere syntaks:
p->a;
Pil-operatøren gjør akkurat det samme som uttrykket over, men med en enklere og mindre komplisert syntaks.