C-taal voor beginners - hoofdstuk 5
pointers en geheugenmanipulatie
[5.1 wat is een pointer?] [5.2 pointertypes en arrays] [5.3 pointers en strings] [5.4 pointers naar arrays]
Eén van de dingen die beginners in de C-taal als moeilijk ervaren is het concept van pointers. Ik heb ondervonden dat de voornaamste reden een zwak of minimaal gevoel voor variabelen is. In het eerste hoofdstuk werd het begrip variabele reeds uit de doeken gedaan. Omdat het begrijpen ervan zeer belangrijk is, even een andere benadering.
Een variabele in een programma is iets met een naam, waarvan de waarde kan variëren. De compiler kent een specifiek blok geheugen toe aan een variabele, om de waarde van die variabele in de computer bij te kunnen houden. De grootte van dat blok hangt af van het bereik waarover de variabele mag variëren. De grootte van het type hangt feitelijk af van de compiler.
Wanneer we een variabele declareren informeren we de compiler over twee dingen: de naam van de variabele en het type van de variabele. We declareren, bijvoorbeeld, een variabele van het type integer met de naam k op de volgende manier:
int k;
Wanneer de compiler de "int" tegenkomt zet deze 2 bytes geheugen opzij (op een PC) om de waarde van die integer vast te houden. Er wordt ook een tabel met symbolen opgesteld. In die tabel wordt het symbool k toegevoegd en het relatieve adres in het geheugen, waar die 2 bytes opzij gezet werden. Dus als we later schrijven:
k = 2;
verwachten we dat, wanneer dit statement uitgevoerd wordt, de waarde 2 in die geheugenlocatie wordt geplaatst die reeds opzij gezet werd voor de opslag van de waarde van k. In C verwijzen we naar een variabele zoals de integer k als een "object". Eigenlijk zijn er twee "waarden" die geassocieerd worden met het object k. Eén ervan is de waarde van de integer, 2 in ons voorbeeld, en de andere is de "waarde" van de geheugenlocatie, het adres van k. Men verwijst naar deze twee waarden met de naamgeving "rvalue" (rechtse waarde) en "lvalue" (linkse waarde). We kunnen lvalue beschouwen als de waarde die toegelaten wordt aan de linkse zijde van de toekenningsoperator =. De rvalue is degene aan de rechtse kant van de toekenningsoperator, in ons voorbeeld is dat de 2. Dus 2 = k; is illegaal.
Beschouw nu het volgende:
int j, k;k = 2; j = 7; /* lijn 1 (zie tekst) */ k = j; /* lijn2 (zie tekst) */
In dit voorbeeld interpreteert de compiler de j in lijn 1 als het adres van de variabele j (lvalue) en creëert de code om de waarde 7 naar dat adres te kopiëren. In lijn 2, anderzijds, wordt de j geïnterpreteerd als zijn rvalue, aangezien deze aan de rechtse kant van de = staat. Dat wil zeggen: de j verwijst naar de waarde die in de geheugenplaats zit, die opzij gehouden werd voor j (7 in dit geval). Dus de 7 wordt gekopieerd naar het adres dat gereserveerdd werd door de lvalue van k.
Stel nu dat we een variabele willen om een lvalue vast te houden (een adres). De grootte die daarvoor vereist is hangt af van het systeem. Op oudere desktop computers kan het geheugen in 2 bytes bijgehouden worden. Nieuwere systemen hebben meer geheugen nodig om een adres te bewaren. De eigenlijke grootte is niet zo belangrijk, zolang we de compiler maar laten weten dat we een adres willen bewaren. Zulk een variabele noemt men een pointer variabele. In C definiëren we een pointer variabele door er een asterisk * voor te plaatsen. We geven ook het type weer dat, in dit geval, verwijst naar het type van data opgeslagen in het adres waar we onze pointer gaan bewaren:
int *ptr;
ptr is de naam van onze variabele. De * informeert de compiler dat we een pointer variabele willen. De int laat weten dat we onze variabele willen gebruiken om het adres van een integer vast te houden. Zulk een pointer "wijst" of wordt als het ware gericht naar een integer. In het Engels zegt men "point to", waarvan de benaming pointer afgeleid werd. Merk op dat toen we int k; schreven we k geen waarde gaven. Indien zo'n definitie buiten een functie gedaan wordt, zal de compiler de waarde op 0 zetten. Dit is ook het geval met een pointer variabele, zodat deze zeker niet naar een object in C verwijst. Een pointer die op deze manier gedeclareerd wordt is een "null" pointer. In feite wijst zulk een pointer naar "het niets". Het eigenlijke patroon dat gebruikt wordt voor een null pointer is niet altijd gelijk aan 0, aangezien dit afhangt van het systeem. Om de broncode toch compatibel te maken tussen verschillende compilers, is er een macro voorzien om een null pointer te representeren, onder de naam NULL. Dus, een pointer initialiseren door hem naar NULL te wijzen, zoals ptr = NULL, verzekert ons dat deze echt een null pointer geworden is. Op dezelfde manier waarop men een integer test op de waarde 0, zoals in if(k == 0), kan men een null pointer testen: if(ptr == NULL).
Terug naar onze nieuwe variabele ptr. Veronderstel nu dat we er het adres van onze integer variabele k in willen stoppen. U weet reeds dat we hiervoor de adresoperator & gebruiken:
ptr = &k;
Wat min of meer wil zeggen: "kopieer het adres van k naar ptr". De & operator neemt de lvalue (adres) van k, niettemin dat k aan de rechterkant van de toekenningsoperator = staat, en kopieert dit naar de inhoud van onze pointer ptr. Nu wijst ptr dus naar k. Naast de adresoperator & hebben we ook nog de indirectieoperator * (asterisk). Deze wordt als volgt gebruikt:
*ptr = 7;
De waarde 7 wordt gekopieerd naar het adres waarnaar ptr wijst. Dus als ptr wijst naar (bevat het adres van) k, zal *ptr = 7; de waarde van k op 7 zetten. Dat is zo als we de * op deze manier gebruiken, namelijk om naar de waarde te verwijzen van datgene waarnaar ptr wijst, niet naar de waarde van de pointer zelf. Op dezelfde manier zouden we kunnen schrijven:
printf("%d\n", *ptr);
om de integer waarde af te drukken die bewaard wordt op het adres waar ptr naar wijst. Om aan te tonen hoe dit allemaal bij elkaar past, een voorbeeldprogramma. Compileer en run dit en bekijk de code en output aandachtig:
#include<stdio.h> int j, k; int *ptr; int main(void) { j = 1; k = 2; ptr = &k; printf("\n"); printf("j heeft de waarde %d en wordt bewaard op adres %p\n", j, (void *)&j); printf("k heeft de waarde %d en wordt bewaard op adres %p\n", k, (void *)&k); printf("ptr heeft de waarde %p en wordt bewaard op adres %p\n", ptr, (void *)&ptr); printf("De waarde van de integer waarnaar ptr wijs is %d\n", *ptr);return 0; }
Nieuw is %p, dat gebruikt wordt om het adres van een pointer af te drukken. We moeten ook de aspecten van C nog bespreken, die het gebruik van de (void *) expressie vereisen. Includeer het op dit moment gewoon in uw code. We verklaren de reden achter deze expressie later in dit hoofdstuk. Omdat het begrijpen van pointers belangrijk is, een overzicht van deze paragraaf:
- een variabele wordt gedeclareerd door hem een type en een naam te geven (vb: int k;)
- een pointer variabele wordt gedeclareerd door hem een type en een naam te geven (vb: int *ptr), waar de asterisk de compiler vertelt dat de variabele ptr een pointer variabele is en het type vertelt de compiler naar welk type de variabele moet wijzen, integer in dit geval
- eens een variabele is gedeclareerd, kunnen we zijn adres verkrijgen door de naam voor te gaan met &, zoals in &k
- om een pointer te "indirecteren", anders gezegd om de waarde ervan te krijgen, gebruiken we de indirectieoperator *, zoals in *ptr
- & en * zijn unaire operators
- een lvalue van een variabele is niets anders dan de waarde van zijn adres
5.2 pointertypes en arrays
Opmerking van een lezer (zie referenties) bij deze paragraaf: "Het probleem is dat jij er vanuit gaat dat het adres van mijn_array[0] lager is dan het adres van mijn_array[1]. Op je PC (en vele andere machines) zal dit best kloppen maar daar kan je niet 100% zeker van zijn. Er zijn namelijk machines die het precies andersom doen (het zijn er niet veel, maar het is toch een reëel probleem)."
Het is dus mogelijk dat hetgeen hier uitgelegd wordt niet helemaal overeenkomt met de manier waarop uw systeem werkt. Meestal zal het echter wel kloppen en kan u het veilig toepassen.
Beschouw het volgende:
int mijn_array[ ] = {1,23,17,4,-5,100};
Hier hebben we een array met als inhoud zes integers. We verwijzen naar elk van deze integers met een subscript, bijvoorbeeld mijn_array[0] t.e.m. mijn_array[5]. Als alternatief kunnen we ook een pointer gebruiken:
int *ptr; ptr = &mijn_array[0]; /* wijst de pointer naar de eerste integer in ons array */
Dan kunnen we ons array afdrukken, gebruik makende van de arraynotatie of van de indirectieoperator. De volgende code illustreert dit duidelijk:
#include<stdio.h>
int mijn_array[ ] = {1,23,17,4,-5,100};
int *ptr;
int main(void)
{
int i;
ptr = &mijn_array[0];
printf("\n\n");
for(i=0;i<6;i++)
{
printf("mijn_array[%d] = %3d ", i, mijn_array[i]); /* lijn A (zie tekst) */
printf("ptr + %d = %d\n", i, *(ptr + i)); /* lijn B (zie tekst) */
}
return 0;
}
Met als uitvoer:
mijn_array[0] = 1 ptr + 0 = 1 mijn_array[1] = 23 ptr + 1 = 23 mijn_array[2] = 17 ptr + 2 = 17 mijn_array[3] = 4 ptr + 3 = 4 mijn_array[4] = -5 ptr + 4 = -5 mijn_array[5] = 100 ptr + 5 = 100
Bestudeer lijn A en B en merk op dat de compiler in beide gevallen dezelfde waarden afdrukt. Bekijk ook hoe we de indirectie gebruikten in lijn B. Verander nu lijn B als volgt:
printf("ptr + %d = %d\n", i, *ptr++);
en compileer en run het programma opnieuw. Verander lijn B daarna als volgt:
printf("ptr + %d = %d\n", i, *(ptr++));
en probeer opnieuw. Voorspel elke keer de uitkomst en bestudeer de output.
In C is het de standaard om waar we &var_naam[0] kunnen gebruiken, dat veranderen in var_naam, dus in onze code kunnen we ptr = &mijn_array[0]; veranderen in ptr = mijn_array; om hetzelfde resultaat te verkrijgen. Dit heeft als gevolg dat velen zeggen dat de naam van een array een pointer is. Ik verkies het volgende te denken: "de naam van het array is het adres van het eerste element in het array". Vele beginners worden verward als ze het bekijken als een pointer. Bijvoorbeeld, terwijl we wel kunnen schrijven:
ptr = mijn_array;
kunnen we niet schrijven:
mijn_array = ptr;
De reden is dat ptr een variabele en mijn_array een constante is. De locatie waar het eerste element van mijn_array zal opgeslagen worden kan niet veranderd worden vanaf het moment dat mijn_array[ ] gedeclareerd is. Laten we nu wat dieper ingaan op het verschil tussen de namen ptr en mijn_array, zoals we ze hierboven gebruikten. Sommige schrijvers verwijzen naar een naam van een array als een constante pointer. Om de term "constant" in deze context te begrijpen gaan we even terug naar onze definitie van de term "variabele". Wanneer we een variabele declareren zetten we een stukje geheugen opzij om de waarde van het juiste type vast te houden. Eénmaal dat gebeurd is kan de naam van de variabele op een paar manieren geïnterpreteerd worden. Indien de variabele gebruikt wordt aan de linkse kant van de toekenningsoperator, zal de compiler dit bekijken als de geheugenplaats waar de waarde, die het resultaat is van de rechtse statement(s), naar verplaatst moet worden. Maar, wanneer aan de rechtse kant gebruikt, wordt de de variabele bekeken als de waarde om in die geheugenlocatie te plaatsen. Dat klinkt allemaal nogal complex, maar is het in feite niet.
Met het vorige in gedachten, laten we de simpelste manier van constanten beschouwen:
int i, k; i = 2;
Terwijl i een variabele is en dus ruimte inneemt in
het data gedeelte van het geheugen, is 2 een constante die direct in het
code gedeelte van het geheugen terecht komt. Wanneer we iets schrijven als k = i; vertellen
we de compiler om code te creëeren die tijdens het runnen naar de geheugenlocatie &i
zal kijken om de waarde te bepalen om naar k te verplaatsen. Maar i
= 2; zal simpelweg 2 in de code plaatsen en naar niets anders
verwijzen. Dus k en i zijn objecten, 2
niet.
Op dezelfde manier, aangezien mijn_array een constante is zal de compiler
het adres kennen van mijn_array[0], éénmaal hij beslist waar het array
op zichzelf opgeslagen zal worden. Dus bij het zien van
ptr = mijn_array;
gebruikt de compiler dit adres als een constante en is er verder geen andere verwijzing.
Dit is een goed moment om het gebruik van de (void *) expressie verder te verklaren, die we eerder in een programma gebruikten. Zoals u reeds weet kunnen we pointers hebben van verschillende types. Tot dusver hebben we pointers naar integers en pointers naar karakters besproken. We hebben ook geleerd dat op verschillende systemen de grootte van een array kan variëren. Het is echter ook mogelijk dat de grootte van een pointer kan variëren, afhankelijk van het data type van het object waarnaar de pointer wijst. Dus net zoals met integers, waarbij je problemen kunt ervaren bij het toekennen van een long int aan een variabele van het type short int, kunnen er problemen opdagen bij het toekennen van de waarde van een pointer aan pointer variabelen van andere types. Om dit probleem te minimaliseren, voorziet C het type void voor een pointer. We kunnen zo'n pointer als volgt declareren:
void *ptr;
Een void pointer is eigenlijk een soort van een generieke pointer. Bijvoorbeeld: alhoewel C een vergelijking tussen een pointer naar het type int en een pointer naar het type char niet toelaat, kunnen deze wel vergeleken worden met een void pointer. U kan natuurlijk wel ingewikkelde manieren bedenken om een pointer van het ene type naar het andere te converteren.
Het bestuderen van strings is nuttig voor het begrijpen van de relatie tussen pointers en arrays. Het maakt het ook makkelijk te illustreren hoe sommige van de standaard C stringfuncties toegepast kunnen worden. Uiteindelijk maakt het duidelijk hoe en wanneer pointers kunnen en zouden moeten gepasseerd worden naar functies.
U wist reeds dat in C strings eigenlijk karakterarrays zijn. Dit is niet altijd zo bij andere programmeertalen. In BASIC, Pascal, Fortran en een reeks andere talen, heeft een string zijn eigen datatype. In C is dit niet zo. Daar is een string een array van het type char, afgesloten met een binair null karakter (geschreven als '\0' of soms gewoon 0). Om onze discussie te beginnen zullen we een stukje code schrijven dat, aangezien het enkel als illustratie dient, u waarschijnlijk nooit zou gebruiken in uw eigen programma. Bijvoorbeeld:
char mijn_string[40];mijn_string[0] = 'T'; mijn_string[1] = 'e'; mijn_string[2] = 'd'; mijn_string[3] = '\0';
Het eindresultaat is een string die eigenlijk een array van karakters is, afgesloten met het null karakter. Let op: "null" is niet hetzelfde als "NULL". De null verwijst naar een 0, zoals gedefinieerd met de escape sequentie '\0'. Dit neemt 1 byte geheugen in beslag. NULL is de naam van de macro om null pointers te initialiseren en is gedefinieerd in een header van uw C compiler; null is mogelijk niet eens gedefinieerd. Aangezien de code hierboven zeer tijdrovend is, voorziet C twee alternatieve manieren om hetzelfde resultaat te verkrijgen. Ten eerste kan u schrijven:
char mijn_string[40] = {'T', 'e', 'd', '\0'};
Maar dit is ook niet echt efficiënt. Daarom laat C het volgende toe:
char mijn_string[40] = "Ted";
Wanneer de dubbele quotes " " gebruikt
worden, in plaats van de enkele quotes ' ', wordt het null karakter '\0'
automatisch achteraan de string geplaatst. In elk van de bovenvernoemde gevalen gebeurt
hetzelfde. De compiler zet een aaneengrenzend stuk geheugen van 40 bytes opzij om
karakters vast te houden en initialiseert de eerste 4 karakters als "Ted\0".
Tot hier de korte herhaling van strings en arrays. Beschouw nu het volgende programma.
#include<stdio.h>
char strA[80] = "Een string die gebruikt wordt als demonstratie";
char strB[80];
int main(void)
{
char *pA; /* een pointer naar het char type */
char *pB; /* nog een pointer naar het char type */
puts(strA); /* toon string A */
pA = strA; /* wijs pA naar string A */
puts(pA); /* toon waar pA naar wijst */
pB = strB; /* wijs pB naar string B */
putchar('\n'); /* een lege lijn */
while(*pA != '\0') /* lijn A (zie tekst) */
{
*pB++ = *pA++; /* lijn B (zie tekst) */
}
*pB = '\0'; /* lijn C (zie tekst) */
puts(strB); /* toon strB */
return 0;
}
In ons voorbeeldprogramma beginnen we met het definiëren van twee karakter arrays van elk 80 karakters. Omdat deze globaal zijn, worden ze door de compiler eerst geïnitialiseerd op '\0', zoals eerder uitgelegd. Daarna worden de gegeven karakters van strA geïnitialiseerd tussen enkele quotes. We declareren twee karakter pointers en tonen de string op het scherm. Daarna "wijzen" of richten we de pointer pA naar strA. In feite kopiëren we het adres van strA[0] naar onze variabele pA. Daarna gebruiken we puts() om hetgeen te tonen waar pA naar verwijst. In hoofdstuk 3 hadden we het reeds even over gets() en puts(). Deze functies dienen respectievelijk om een string om te nemen en om op het scherm te tonen. De variabele (de string) wordt tussen de ronde haken geplaatst. Het functieprototype (als gedefinieerd door de compiler) voor puts() is als volgt:
int puts(const char *s);
Negeer op het moment de "const". De parameter die naar puts() gepasseerd wordt is een pointer, meerbepaald de waarde van een pointer (aangezien alle parameters in C via "passed by value" werken). De waarde van een pointer is het adres van hetgeen waarnaar hij wijst. Dus wanneer we puts(strA); schrijven geven we het adres van strA[0] door. Als we puts(pA); schrijven geven we hetzelfde adres door, aangezien we pA = strA; schreven. Met dat gegeven in gedachte, bekijken we de code van de while() structuur op lijn A. Lijn A zegt in feite: zolang het karakter waar pA naar wijst niet het null karakter is, doe het volgende: kopieer het karakter waar pA naar wijst naar de ruimte waar pB naar wijst. Incrementeer pA daarna zodat deze naar het volgende karakter wijst en pB zodat deze naar de volgende ruimte wijst (lijn B). Wanneer we het laatste karakter gekopieerd hebben wijst pA naar het afsluitende null karakter en de loop eindigt. We hebben het null karakter zelf niet gekopieerd, maar in C moet een string wel met een nul karakter afgesloten worden. Daarom voegen we dit toe (lijn C).
Bekijk opnieuw het prototype voor puts(). De
"const", die gebruikt wordt als parameterwijziger, informeert de gebruiker dat
de functie de string waar s naar wijst niet zal aanpassen of veranderen,
dus wordt deze string behandelt als een constante.
Wat het bovenstaande voorbeeldprogramma illustreert is een simpele manier om een string te
kopiëren. Nadat u het voorbeeld voldoende bestudeerde en volledig begrijpt, kunnen we
onze eigen vervanging voor strcpy(), dat in hoofdstuk
3 besproken werd, verzinnen. Dit kan er als volgt uitzien:
char *mijn_strcpy(char *bestemming, char *bron)
{
char *p = bestemming;
while (*bron != '\0')
{
*p++ = *bron++;
}
*p = '\0'
return bestemming;
}
Ik gebruik hier de Nederlandse benamingen "bestemming" en
"bron", maar meestal zal u respectievelijk de Engelse benamingen
"destination" en "source" tegenkomen in referenties en
compilerdocumentatie.
In dit geval heb ik de standaardroutine voor het terugkeren (return) van
een pointer naar de bestemming gebruikt. Opnieuw is de functie ontworpen om de waarden van
twee karakter pointers aan te nemen. Dus in ons programma zouden we kunnen schrijven:
int main(void) { mijn_strcpy(strB, strA); puts(strB);return 0; }
Ik wijk hier misschien lichtjes af van de vorm die in standaard C gebruikt wordt, welke er als volgt zou uitzien:
char *mijn_strcpy(char *bestemming, const char *bron);
Hier wordt de "const" bepaling gebruikt om te verzekeren dat de functie de inhoud, waarnaar de bronpointer naar wijst, niet zal veranderen. Dit kan bewezen worden door bovenstaande functie aan te passen, alsook het prototype, om de "const" bepaling te includeren, zoals getoond. In de functie kan u dan een statement toevoegen dat probeert om de inhoud te veranderen van datgene waar de bron naar wijst, zoals:
*bron = 'X';
wat normaal gezien het eerste karakter van de string in een 'X' zou veranderen. Door de const zou uw compiler dit als een fout moeten zien. Probeer dit uit.
Laten we nu enkele zaken beschouwen die de voorbeelden ons getoond hebben. Ten eerste, beschouw het feit dat *ptr++ geïnterpreteerd moet worden als het teruggeven (return) van de waarde waar ptr naar wijst, waarna de pointerwaarde geïncrementeerd wordt. Dit heeft te maken met de prioriteit van de operators. Indien we (*ptr)++ schreven zouden we niet de pointer incrementeren, maar wel datgene waar hij naar wijst, aangezien ronde haken voorgaan op de incrementie. Bijvoorbeeld, indien toegepast op het eerste karakter van bovenstaande voorbeeldstring, zou de 'T' geïncrementeerd worden tot 'U'. U kan zelf een simpel stukje code schrijven om dit te illustreren.
Merk op: wanneer we een integer doorgeven, maken we een kopie van deze integer. Als de doorgegeven waarde dan wordt gemanipuleerd heeft dit totaal geen effect op de originele integer. Maar bij arrays en pointers kunnen we het adres van de variabele doorgeven en vanaf dan de waarde manipuleren van de originele variabelen.
We hebben in een korte tijd reeds heel wat voortuitgang geboekt. Laten we even terugkeren naar hetgeen we deden bij het kopiëren van strings, maar nu in een ander licht. Beschouw de volgende functie:
char *mijn_strcpy(char dest[ ], char source[ ])
{
int i = 0;
while(source[i] != '\0')
{
dest[i] = source[i];
i++;
}
dest[i] = '\0';
return dest;
}
Ik gebruik hier voor de verandering de Engelse benamingen voor
"bestemming" en "bron".
Herinner u dat strings arrays zijn van het type char. Hier hebben we
gekozen voor een arraynotatie in plaats van een pointernotatie om het eigenlijke kopiëren
te doen. Het resultaat is hetzelfde: de string wordt gekopieerd. Dit brengt een paar
interessante zaken aan het licht.
Bij zowel het doorgeven van een karakter pointer als het doorgeven van de naam van het array, zoals hierboven, wordt het adres van het eerste element van elk array doorgegeven. Bijgevolg is de numerieke waarde van de doorgegeven parameter dezelfde, of we nu een karakter pointer of een array-naam als parameter gebruiken. Dit zou suggereren dat op de één of andere manier source[i] hetzelfde is als *(source+i). In feite is dit waar. Wanneer iemand bijvoorbeeld a[i] schrijft, dan kan dat vervangen worden door *(a+i), zonder enige problemen. De compiler creëert in beide gevallen dezelfde code. We kunnen dus zien dat pointer-rekenkunde hetzelfde is als array-indexering. Beide syntaxen geven hetzelfde resultaat. Dit wil niet zeggen dat pointers en arrays hetzelfde zijn; dat is niet zo. Het betekent gewoon dat om een gegeven element van een array te identificeren, we de keuze hebben tussen twee mogelijkheden, met hetzelfde resultaat als gevolg.
Bekijk nu de uitdrukking (a+i). Dit is een simpele optelling, gebruik makend van de + operator en de regels van C dat zulk een berekening commutatief is. Dus (a+i) is hetzelfde als (i+a). Bijgevolg kunnen we *(i+a) evengoed als *(a+i) schrijven. Maar *(i+a) kon ook afkomstig zijn van i[a]. Dit alles heeft het merkwaardige gevolg dat indien:
char a[20]; int i;
en we schrijven:
a[3] = 'x';
dit hetzelfde is als:
3[a] = 'x';
Let op: dit laatste kan u beter niet toepassen, aangezien velen het niet zullen aanvaarden als legaal programmeren. Ik toon het hier enkel om een merkwaardigheid duidelijk te maken.
Laten we nu onze bovenstaande functie opnieuw bekijken, waar we schrijven:
dest[i] = source[i];
U weet ondertussen dat we dit ook kunnen schrijven als:
*(dest + i) = *(source + i);
Dit vereist echter twee optellingen voor elke waarde die i aanneemt. Dus, de pointerversie kan een beetje sneller zijn dan de arrayversie. Dit is ook weer erg compiler en platform specifiek. Soms kan het ook gewoon omgekeerd zijn. Een andere manier om de methode met de pointers te versnellen zou zijn door
while(*source != '\0')
te vervangen door
while (*source)
aangezien de waarde tussen de haken op hetzelfde moment naar 0 zal gaan, in beide gevallen.
Pointers kunnen natuurlijk naar elk type data object "gericht" worden, inclusief arrays. Dat wist u ondertussen al wel, maar het is belangrijk om dit uit te breiden naar hoe we dit doen als het om multi-dimensionele arrays gaat.
Om even te herhalen, in paragraaf 5.2 leerden we dat, indien we een array van integers hebben, we een integer pointer naar dat array kunnen wijzen, als volgt:
int *ptr; ptr = &mijn_array[0]; /* wijst de pointer naar de eerste integer in ons array */
Het type van de pointer variabele moet dus hetzelfde zijn als het type van het eerste element van het array. Bovendien kunnen we een pointer als een formele parameter, van een functie die ontworpen is om een array te manipuleren, gebruiken. Bijvoorbeeld: gegeven is het volgende:
int array[3] = {'1', '5', '7'};
void een_functie(int *p);
Sommige programmeurs zouden het functieprototype als volgt schrijven:
void een_functie(int p[ ]);
wat andere gebruikers van de functie ervan zou moeten inlichten dat ze ontworpen is om elementen van een array de manipuleren. Hetgeen in beide gevallen wordt doorgegeven is de waarde van een pointer naar het eerste element van het array, onafhankelijk van de notatie die gebruikt werd in het functieprototype of in de definitie. Merk op dat, indien de arraynotatie gebruikt wordt, het niet nodig is om de eigenlijke dimensie van het array door te geven, aangezien we enkel het adres van het eerste element doorgeven en niet het ganse array.
Laten we nu het probleem van het 2-dimensionele array beschouwen. C interpreteert een 2-dimensioneel array als een array van 1-dimensionele arrays. Bijgevolg is het eerste element van een 2-dimensioneel array van integers, een 1-dimensioneel array van integers. Een pointer naar een 2-dimensioneel array van integers moet een pointer zijn van datzelfde datatype. Eén manier om dit te verwezenlijken is door gebruik te maken het sleutelwoord typedef. Dit kent een nieuwe naam toe aan een gespecifieerd data type, bijvoorbeeld:
typedef unsigned char byte;
dit zal de naam byte als het type unsigned char definiëren. Van dan af zal bijvoorbeeld
byte b[10];
een array zijn van unsigned karakters. Merk op dat in de typedef declaratie, het woord byte hetgeen heeft vervangen dat normaal de naam zou zijn van onze unsigned char. Want de regel bij het gebruik van typedef is dat de nieuwe naam van het data type de naam is die in de definitie van het data type gebruikt wordt. Dus bij
typedef int Array[10];
wordt Array een data type voor een array van tien
integers. Zo zal, bijvoorbeeld, Array mijn_arr; de variabele mijn_arr
declareren als een array van tien integers en Array arr2d[5]; maakt van arr2d
een array van vijf arrays van tien integers elk.
Merk ook op dat Array *p1d; er voor zorgt dat p1d een
pointer is naar een array van tien integers. Omdat *p1d naar hetzelfde
type wijst als arr2d, het adres van het 2-dimensinele array arr2d
aan p1d toekennend, is de pointer naar een 1-dimensioneel array van tien
integers acceptabel, zoals p1d = &arr2d[0]; of p1d = arr2d;
Deze zijn beide correct.
Niettegenstaande het gebruik van typedef de dingen duidelijker en makkelijker maakt voor de gebruiker en programmeur, is het niet echt noodzakelijk. Wat we nodig hebben is een manier om een pointer te declareren zoals p1d zonder gebruik van het typedef sleutelwoord. Dit kan op de volgende manier gedaan worden:
int (*p1d) [10];
waarbij p1d dus een pointer is naar een array van tien integers, net zoals het was onder de declaratie bij het gebruik van het arraytype. Dit is niet hetzelfde als
int *p1d[10];
wat p1d de naam van een array van tien pointers naar het type integer zou maken.
Tenslotte: dit hoofdstuk is veruit het meest ingewikkelde van de cursus. Toch is het ook zowat het belangrijkste. Geen enkel degelijk C-programma werkt zonder pointers en, alhoewel u dit nu waarschijnlijk moeilijk kunt geloven, pointers maken het leven van de programmeur een stuk eenvoudiger. Leer dit hoofdstuk grondig en bestudeer alle voorbeelden. Neem rustig de tijd, want als u dit onder de knie heeft zal de rest meevallen.
opdrachten
1. Probeer uw eigen versies the schrijven van stringfuncties als strlen(), strcat(), strchr(), enz.. .
2. Definieer een karakter array en gebruik strcpy() om er een string naar te kopiëren. Druk de string af door een loop te gebruiken met een pointer om één karakter per keer af te drukken. Initialiseer de pointer op het eerste element en gebruik incrementatie. Neem een aparte integer variabele om de af te drukken karakters te tellen.
3. Schrijf een programma dat aan de gebruiker vraagt om een string in te geven. Met een loop neem je telkens één karakter op van die string. In een aparte functie druk je de string terug af. In een andere functie druk je de string in omgekeerde volgorde af. De main() roept functie1() op en functie1() roept functie2() op. Gebruik waar mogelijk pointers. Tip: om nadien de string in omgekeerde volgorde te kunnen afdrukken, sla je elk ingescand karakter op in een string, gebruik makend van een incrementerende index.
voorbeelden
Bekijk ook de voorbeelden die reeds in dit hoofdstuk getoond werden.
5-1.c: Bepaalde tekens van een string outputten, op verschillende methodes.
5-2.c: Een aantal waarden op verschillende manieren afdrukken, met pointers en functies.
referenties
[Pointers and Arrays Tutorial]
Ted Jensen