• De afgelopen dagen zijn er meerdere fora waarop bestaande accounts worden overgenomen door spammers. De gebruikersnamen en wachtwoorden zijn via een hack of een lek via andere sites buitgemaakt. Via have i been pwned? kan je controleren of jouw gegeven ook zijn buitgemaakt. Wijzig bij twijfel jouw wachtwoord of schakel de twee-staps-verificatie in.

[C++] Pointers

Wat vind je van deze tutorial?

  • Ik slaap er nog een nachtje over...

    Stemmen: 0 0,0%

  • Totaal aantal stemmers
    19
Status
Niet open voor verdere reacties.

D Drmmr

Heeft veel posts
Lid geworden
5 jul 2005
Berichten
13.482
Waarderingsscore
151
[hand]
C++ Pointers

Pointers zijn een onderdeel van C++ dat voor veel mensen lastig is om onder de knie te krijgen. Om het goed te kunnen begrijpen, is het allereerst belangrijk dat je een goede kijk op zaken ontwikkeld. Het gaat daarbij met name om het verschil tussen typen en objecten.

Een type geeft aan wat een bepaalde waarde voorstelt. Het wordt door de compiler gebruikt om te bepalen welke bewerkingen er op de data kunnen of moeten worden uitgevoerd. Als je bijvoorbeeld een int en een char naar het scherm print, worden twee verschillende functies aangeroepen (een voor de int en een voor de char) die ieder precies weten wat ze met de data die ze krijgen moeten doen.

Een object is in principe niets meer dan een zootje enen en nullen, precies zoveel als er nodig zijn om de data van het type van het object in op te slaan. Een object is een instantie (een exemplaar, zogezegd) van een bepaald type. Doordat je de compiler het type van het object hebt verteld, kun je bepaalde bewerkingen op het object uitvoeren. De compiler weet dan welke routines moeten worden aangeroepen.

Een voorbeeldje
Code:
// pointers00.cpp
int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	a = b + c;
	float f = a + b + c;
	f /= 3;

	return 0;
}
We hebben hier drie objecten aangemaakt van het type int en ze de waarden 1, 2 en 3 gegeven. Vervolgens tellen we de waarde van twee van die objecten op. Dit kan omdat de compiler weet hoe die twee integers moet optellen. Het resultaat van de optelling kennen we toe aan de eerste integer. Ook dit kan weer omdat de compiler weet hoe die een object van het type int (het resultaat van de optelling) moet toekennen aan een object van het type int (de variabele a).

In de vijfde regel wordt het wat lastiger. Het resultaat van de optelling is weer een object van het type int. Maar de compiler weet niet hoe die een int moet toekennen aan een float. Hij weet wel hoe een float toe te kennen aan een float en hoe een int te converteren naar een float. Met die twee samen is het toch mogelijk om een int toe te kennen aan een float. Het resultaat van de optelling is dus een object van het type int. Dit wordt geconverteerd naar een object van het type float en dat wordt toegekend aan het object van het type float dat we 'f' hebben genoemd.

Voor de laatste regel geldt hetzelfde. De compiler weet niet hoe die een float moet delen door een int, maar wel hoe een float te delen door een float en hoe een int te converteren naar een float. Door het feit dat ieder object een bepaald type heeft, is de compiler in staat om na te gaan welke bewerking er op het object moet worden uitgevoerd.


Ok, dan nu het echte werk...

Inhoud

Wat is een pointer?
Wat is het nut van een pointer?
Waar moet je op letten als je pointers gebruikt?


Gebruikte code:
http://ncf.ddrmmr.nl/code/pointers00.cpp

[/hand]
 
Laatst bewerkt door een moderator:
[hand]
Wat is een pointer?

Definitie: Een pointer is een type dat een geheugenadres opslaat waar een object van een bepaald type staat. :huh: (No worries, aan het eind van deze post snap je 't. ;))

De syntax voor een pointer is

type* naam

Bijvoorbeeld
int* pointer_to_int;
char* pointer_to_char;
void* p;​

Bij een pointer moet je alles behalve de naam eigenlijk van rechts naar links lezen. Op de eerste regel staat dus een pointer naar een int (met de naam pointer_to_int). We zullen hier later nog eens op terugkomen. Eerst even een simpel voorbeeldje.
Code:
int n = 2;
int* p = &n;
In de eerste regel definiren we een variabele met de naam 'n' waarin een integer wordt opgeslagen. De variabele n wordt genitialiseerd met de waarde 2. Wat de computer dan doet is ergens genoeg geheugen reserveren om een integer waarde in op te slaan. Dat geheugen wordt vervolgens gezet op de waarde die 2 voorstelt.
In de tweede regel definiren we een variabele met de naam p waarin een pointer naar een integer wordt opgeslagen. De variabele p wordt genitialiseerd met het geheugenadres waarin de waarde van de variabele n wordt opgeslagen. (Door een & (ampersand) voor een variabele te zetten, krijg je het geheugenadres waarin die variabele wordt opgeslagen.)

In het plaatje hieronder wordt dit schematisch weergegeven. De compiler weet dat de naam 'n' hoort bij een object van het type int en heeft in dat object de waarde die het getal 2 voorsteld opgeslagen. Bij de naam 'p' hoort een object van het type int* (pointer naar een int). De waarde van dat object is ingesteld op het geheugenadres van het object met de naam n. De compiler weet dat op dit adres een object is opgeslagen van het type int.

Pointers01.gif

We kunnen nu de waarde van n via de variabele p veranderen. In p is het geheugenadres van n opgeslagen. Als we dit geheugenadres 'derefenencen' (in goed nederlands), krijgen we de waarde van n en kunnen we die ook veranderen. Dat gaat als volgt:
Code:
*p = 3;
Met de * (asterisk) voor de p vragen we de waarde op die is opgeslagen in het geheugenadres waar p naar verwijst. *p is dus een integer en in dit geval is het dezelfde integer als de variabele n. We kennen nu de waarde 3 toe aan deze integer.

Wat we nu gedaan hebben, heet een indirectie. We hebben indirect de waarde van de variabele n veranderd. Dit is precies hoe pointers werken. Normaal wordt het geheugenadres waar de waarde van n in is opgeslagen door de compiler bijgehouden en kunnen we de variabele n direct aanspreken. Bij een pointer slaan we het geheugenadres expliciet op in een object en moeten we een indirectie gebruiken om bij de waarde van de variabele n te komen.

Oefening
Laten we dit alles even bij elkaar zetten in een klein programaatje. Neem dit programma over en ga er stap voor stap doorheen. Probeer telkens te bedenken wat de uitkomst van de volgende stap moet zijn en controleer of dat klopt.
Code:
// pointers01.cpp
#include <iostream>

int main()
{
	int n = 2;
	int* p = &n;

	std::cout << "De waarde van n is " << n << std::endl;
	std::cout << "Het geheugenadres van n is " << &n << std::endl;
	std::cout << "De waarde van p is " << p << std::endl;

	*p = 3;

	std::cout << "De waarde van p is " << p << std::endl;
	std::cout << "De waarde van *p is " << *p << std::endl;
	std::cout << "De waarde van n is " << n << std::endl;

	return 0;
}

Nog een voorbeeldje:
Code:
// pointers02.cpp
void SetLetter(char* p)
{
	if (p)
		*p = 'n';
}

int main()
{
	char c = 'y';
	SetLetter(&c);
	
	return 0;
}
In de main functie wordt een variabele c aangemaakt waarin de letter y wordt opgeslagen. Vervolgens wordt de functie SetLetter aangeroepen en wordt het adres van de variabele c meegegeven. In de functie SetLetter wordt eerst gecontroleerd of het argument p niet nul is. (Een pointer die naar het geheugenadres 0 verwijst is een speciaal geval, dat zullen we later nog zien.) Als p niet nul is, wordt de waarde 'n' toegekend aan de variabele waar p naar verwijst. De functie retourneert en de waarde van de variabele c in de main functie is nu 'n'.

Terug naar de definitie. In een pointer wordt dus een geheugenadres opgeslagen. Als we dat adres dereferencen, krijgen we de waarde van de variabele die op dat geheugenadres wordt opgeslagen. Tenminste, als de pointer naar hetzelfde type verwijst als de variabele waarvan we het geheugenadres hebben genomen. In het eerste voorbeeld hadden we een variabele van het type int en een pointer naar een int (int*). In het tweede voorbeeld hadden we een variabele van het type char en een pointer naar een char (char*).

Wat zou er nu gebeuren als we in een pointer naar een int het geheugenadres van een char willen opslaan? Probeer maar.
Code:
// pointers03.cpp
int main()
{
	char c = 'y';
	int* p = &c;
	
	return 0;
}
Je krijgt een compiler error, zoals
'initializing' : cannot convert from 'char *' to 'int *'
Dus met &c vragen we het geheugenadres van de variabele c op, maar we krijgen niet zomaar een geheugenadres terug. We krijgen een char* terug, oftewel een geheugenadres waar de waarde van een char in staat. Een char* kunnen we niet toekennen aan een int*, omdat het niet dezelfde types zijn.

Het type van een pointer is dus net wat anders dan het type van een 'normale' variabele. Bij een normale variabele is het type de interpretatie van de binaire waarde die in het geheugen van de computer wordt opgeslagen. Bij het type int sla je een geheel getal op, bij het type char sla je een letter op, bij het type bool sla je een boolean op, enz. Maar bij alle pointers sla je een geheugenadres op en toch zijn niet alle pointers van hetzelfde type. We moeten dus verschillende niveau's onderscheiden van het 'type' van een variabele.

Syntax | Bit-niveau | Interpretatie | Type-niveau
char | 8 bits waarde | letter | letter
int | 32 bits waarde | geheel getal | geheel getal
char* | 32 bits waarde | geheugenadres | geheugenadres waar iets in staat van het type char
int* | 32 bits waarde | geheugenadres | geheugenadres waar iets in staat van het type int
void* | 32 bits waarde | geheugenadres | geheugenadres waar iets in staat van het type void

Nog eens kijken...
Een pointer is een type dat een geheugenadres opslaat waar een object van een bepaald type staat. :idea:


Gebruikte code:
http://ncf.ddrmmr.nl/code/pointers01.cpp
http://ncf.ddrmmr.nl/code/pointers02.cpp
http://ncf.ddrmmr.nl/code/pointers03.cpp

[/hand]
 
Laatst bewerkt door een moderator:
[hand]
Wat is het nut van een pointer?

Pointers worden wel eens evil genoemd, wat inhoudt dat je ze alleen moet gebruiken als er geen ander alternatief is of het alternatief nog erger is. Dit betekent dat er een (rood) lampje bij je moet gaan branden op het moment dat je een pointer tegenkomt. Bijvoorbeeld:
Code:
int main()
{
	int* p = new int(5);

	return 0;
}
Er is geen enkele reden om een integer aan te maken met new i.p.v. gewoon een variabele van het type int aan te maken.
Code:
void Reset(int* p)
{
	*p = 0;
}
Er is geen enkele reden om de integer niet 'by reference' door te geven. Sterker nog, het is beter, omdat een reference altijd verwijst naar een valide object en een pointer niet. Dit is dus beter:
Code:
void Reset(int& p)
{
	p = 0;
}


Toch zijn er goede redenen om een pointer te gebruiken. Gevallen waarin er geen andere (betere) oplossing mogelijk is. Er zijn drie redenen om pointers te gebruiken. Ik zal ze eerst even noemen om ze vervolgens stuk voor stuk uit te leggen.
<ol><li>[linkje]Indirectie[/linkje].
Een pointer is een verwijzing naar een object. Het verschil met een reference is dat je kunt wijzigen naar welk object een pointer verwijst en een pointer ook naar een leeg object kan verwijzen (als de waarde van de pointer 0 is). Aan een reference moet je bij de definitie een valide object toekennen en kun je vervolgens niet meer veranderen naar welk object de reference verwijst.</li>
<li>[linkje]Dynamische allocatie[/linkje].
Een pointer kan gebruikt worden om een verwijzing naar een object dat dynamisch is gealloceerd (m.b.v. 'new') op te slaan. Hierdoor wordt de levensduur van het object niet meer beperkt tot de scope waarin het zich bevindt.</li>
<li>[linkje]Polymorfisme[/linkje].
Een pointer kan verwijzen naar de basisklasse van een object dat is afgeleid van die basisklasse. Omdat verschillende klassen kunnen zijn afgeleid van dezelfde basisklasse, kun je met een pointer een verwijzing opslaan naar een object waarvan je het type niet precies weet (je weet slechts dat het type is afgeleid van de basisklasse).</li></ol>
1. [kopje]Indirectie[/kopje]
Stel dat we een aantal variabelen van het type int hebben en we willen de variabele met de hoogste waarde met 3 verminderen. Dan kunnen we met een heleboel if/else constructies nagaan welke variabele de hoogste waarde heeft en die aanpassen, maar we kunnen ook indirectie gebruiken.

We maken een klasse aan waarin we een pointer opslaan naar de int met de hoogste waarde. Als de klasse wordt aangemaakt, moet de pointer nog naar niks verwijzen, dus initialiseren we de waarde van de pointer op 0. Vervolgens gaan we de variabelen n voor n af en kijken of de waarde groter is dan het maximum tot nu toe.
Code:
// pointers04.cpp
#include &lt;iostream&gt;

class Maximum
{
public:
	Maximum() :m_p(0) {}

	// change pointer to specified variable, if the value is larger
	bool Evaluate(int& value)
	{
		if (!m_p || *m_p &lt; value)
		{
			m_p = &value;
			return true;
		}
		return false;
	}

	// retrieve a pointer to the maximum value
	int* GetMaximum()
	{
		return m_p;
	}

private:
	int* m_p;
};

int main()
{
	int a = 1, b = 3, c = 2;

	Maximum m;
	m.Evaluate(a);
	m.Evaluate(b);
	m.Evaluate(c);

	std::cout &lt;&lt; "Maximum of a, b and c: " &lt;&lt; *m.GetMaximum() &lt;&lt; std::endl;
	*m.GetMaximum() -= 3;
	std::cout &lt;&lt; "The value of b is " &lt;&lt; b;

	return 0;
}
Resultaat:
Maximum of a, b and c: 3
The value of b is 0
In de functie Evaluate moeten we de pointer aanpassen als de waarde van het argument van de functie (value) groter is dan de waarde waarnaar de pointer verwijst. Maar als deze functie de eerste keer wordt aangeroepen, verwijst die pointer nog naar niks, de waarde van de pointer is 0. In dat geval moeten we voorkomen dat we de waarde van het argument gaan vergelijken met de waarde waarnaar de pointer verwijst. De pointer verwijst namelijk niet naar een valide object. Als we de pointer zouden dereferencen, terwijl die verwijst naar het geheugenadres 0, zal het programma crashen.

In de if kijken we dus eerst of de pointer een waarde ongelijk aan 0 heeft. Als dat zo is, geeft !m_p false en wordt het tweede deel (*m_p < value) gevalueerd. Als dat waar teruggeeft, moeten we de pointer aanpassen.

Een dergelijk gebruik van pointers zie je veel terug in object gerienteerde code. Een pointer wordt dan gebruikt als een verwijzing naar een object. Het feit dat je het object waarnaar een pointer verwijst kunt veranderen en een pointer naar een leeg object kan verwijzen (geheugenadres 0), maakt ontzettend veel meer mogelijk.


2. [kopje]Dynamische allocatie[/kopje]
De levensduur van een object geeft aan op welk moment het wordt gecreerd en op welk punt het verwijderd wordt. Tijdens de levensduur van een object kun je het benaderen (de waarde opvragen, functies aanroepen, enz.). Voor en na de levensduur van een object kun je er niks mee. Dit houdt dus in dat je ervoor moet zorgen dat de levensduur van een object dat je wilt gebruiken lang genoeg is om het te gebruiken.

De levensduur van een normaal object begint bij de definitie van het object en eindigt als het object 'out of scope' gaat. Dit wordt ook wel statische allocatie genoemd. Dynamische allocatie doe je in C++ m.b.v. de operator 'new'. Ieder object dat je alloceert met new, moet je later in je programma dealloceren met 'delete'. Doe je dat niet, dan ontstaat er een 'geheugenlek' (memory leak) en dat is een ernstige programmeerfout, ook al heb je er zelf niet pers last van als je je programma draait.

Een voorbeeldje:
Code:
// pointers05.cpp
#include &lt;iostream&gt;

int* AllocateInt(int initialValue)
{
	int* p = new int(initialValue);
	return p;
}

void DeallocateInt(int* p)
{
	delete p;
}

int main()
{
	int* ptr = 0;
	ptr = AllocateInt(42);

	std::cout &lt;&lt; *ptr &lt;&lt; std::endl;
	*ptr = 24;
	std::cout &lt;&lt; *ptr &lt;&lt; std::endl;

	DeallocateInt(ptr);
	return 0;
}
Resultaat:

Wanneer hebben we dynamische allocatie nou nodig?

Stel dat we een simpel tekenprogramma hebben, waarmee we lijnen kunnen tekenen in een bitmap. In dit programma willen we een zogenaamde oneindige undo mogelijk maken. Dat houdt in dat je alle bewerkingen die je op een bestand hebt uitgevoerd sinds je het hebt geopend, ongedaan kunt maken. Het is dan makkelijk om een object te maken waarin we het ongedaan maken van het tekenen van een lijn kunnen opslaan. Zo'n object zou er zo ongeveer uit kunnen zien.
Code:
// pointers06.cpp
#include &lt;map&gt;
#include &lt;vector&gt;

typedef unsigned int pixelValue;
typedef std::pair&lt;int, int&gt; pixelIndex;

class Undo
{
public:
	Undo(pixelIndex startOfLine, pixelIndex endOfLine, const std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap)
	{
		// store old values on line
	}

	void Restore(std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap) const
	{
		// restore old values in correct places
	}

private:
	std::map&lt;pixelIndex, pixelValue&gt; m_OldValue;
};
De vraag is nu hoe we al die undo acties gaan opslaan. We zouden natuurlijk een globale variabele kunnen aanmaken die een vector van Undo objecten opslaat. Maar dat is een vrij slecht design, aangezien je Undo objecten dan voor alle code toegankelijk zijn. Dat kan voor veel programmeerfouten gaan zorgen. Het zou beter zijn om een UndoManager object aan te maken dat verantwoordelijk is voor het bijhouden van alle undo objecten en de laatste actie ongedaan kan maken.

In dit UndoManager object moeten we dan nog altijd alle Undo objecten bijhouden. We zouden een vector van Undo objecten kunnen opslaan in de UndoManager, ware het niet dat Undo daarvoor een default constructor moet hebben. Dat is een constructor die zonder argumenten kan worden aangeroepen. Ons Undo object heeft alleen een constructor waarin het begin en eind van de lijn en een const reference naar de bitmap moet worden meegegeven. Er is dus geen default constructor.

Wat we wel kunnen doen is in de UndoManager een vector van pointers naar Undo objecten opslaan. De Undo objecten worden dynamisch aangemaakt door het object dat de lijn-teken-actie uitvoert. Deze worden vervolgens doorgegeven aan de UndoManager die ze bijhoudt. Op het moment dat de laatste actie ongedaan gemaakt moet worden, krijgt de UndoManager hier de opdracht toe. De UndoManager roept de Restore functie aan van het laatste Undo object en verwijdert dat object vervolgens. De code van de UndoManager zou er ongeveer zo uitzien:
Code:
// pointers07.cpp
#include "Undo.h" // Undo class definition
#include &lt;vector&gt;

class UndoManager
{
public:
	~UndoManager()
	{
		// delete all Undo objects
		for (std::vector&lt;Undo*&gt;::iterator it = m_Actions.begin(), end = m_Actions.end(); it != end; ++it)
			delete *it;
		m_Actions.clear();
	}

	void AddUndoObject(Undo* p)
	{
		m_Actions.push_back(p);
	}

	void UndoLastAction(std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap)
	{
		// remove last action from list and undo it
		Undo* p = m_Actions.back();
		m_Actions.pop_back();
		p-&gt;Restore(bitmap);
		delete p;
	}

private:
	std::vector&lt;Undo*&gt; m_Actions;
};
Laten we eens kijken wat nu de levensduur van een Undo object is. Op het moment dat er een lijn wordt getekend, wordt er een Undo object aangemaakt (m.b.v. new) dat deze actie ongedaan kan maken. Een pointer naar dit object wordt doorgegeven aan de UndoManager die de pointer opslaat in een vector. Wanneer de actie ongedaan gemaakt moet worden of als de levensduur van het UndoManager object eindigt, wordt het Undo object gedealloceerd. Verschillende objecten zijn dus verantwoordelijk voor de allocatie en deallocatie van een Undo object. Dit is typisch een geval waarin dynamische allocatie goed van pas komt.

Hier kunnen we best nog wat dieper op in gaan. :wink:
* Waarom kunnen we een Undo object niet statisch (normaal) alloceren?
- Als we het object statisch alloceren, beeindigt de levensduur op het moment dat het object out of scope gaat. Als we het dus in een functie aanmaken, bestaat het object niet meer nadat we de functie verlaten hebben.
* Waarom geven we het object niet 'by value' door aan de UndoManager?
- Ten eerste is dit inefficient, omdat het object meerdere malen gekopieerd moet worden. Door het dynamisch te alloceren wordt het precies n keer aangemaakt. Het enige dat we door hoeven te geven is een pointer naar het object.
Nog belangrijker is dat het soms helemaal niet mogelijk is een object te kopiren. Denk bijvoorbeeld aan een type dat een bestand opent als het wordt aangemaakt en weer sluit als het wordt verwijderd. Je kunt een object van dit type niet kopiren, omdat je het bestand dan voor de tweede keer moet openen.


3. [kopje]Polymorfisme[/kopje]

Stel dat we in ons tekenprogramma vlakke figuren, zoals een driehoek, vierkant, rechthoek, cirkel, enz. willen tekenen. We zouden het tekenen van een driehoek kunnen zien als het tekenen van drie lijnen, maar dat is wel wat beperkt. We kunnen dan bijvoorbeeld geen cirkel tekenen (of het wordt iig heel lastig) en nadat we een driehoek hebben getekend, kunnen we die niet meer veranderen. Een andere oplossing is om de tekening te zien als een verzameling vlakke figuren (een lijn is ook een vlak figuur). Maar hoe gaan we een verzameling vlakke figuren bijhouden?

Een verzameling driehoeken is makkelijk bij te houden in een vector. Voor rechthoeken, cirkels, enz. geldt hetzelfde. Maar het is natuurlijk niet erg makkelijk om voor ieder soort vlak figuur een aparte vector bij te moeten houden. Wat we eigenlijk willen is een vector van vlakke figuren bij kunnen houden. Dat kan. We kunnen alle vlakke figuren afleiden van een basis klasse Shape.
Code:
typedef unsigned int pixelValue;
typedef std::pair&lt;int, int&gt; pixelIndex;

class Shape
{
public:
	virtual void Draw(std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap) const = 0;
};
We hebben in de klasse Shape een zogenaamde puur virtuele functie gemaakt. Dit is een virtuele functie die niet gemplementeerd is. Je doet dit door = 0 achter de functiedeclaratie te zetten. Een klasse met een puur virtuele functie noemen we een abstracte klasse. Je kunt van een abstracte klasse geen instantie aanmaken, je kunt er alleen andere klassen van afleiden. Een afgeleiden klasse moet de puur virtuele functie implementeren.
Code:
class Triangle : public Shape
{
public:
	Triangle(pixelIndex a, pixelIndex b, pixelIndex c)
		: m_a(a), m_b(b), m_c(c)
	{}
	
	void Draw(std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap) const
	{
		// draw the triangle
	}
private:
	pixelIndex m_a;
	pixelIndex m_b;
	pixelIndex m_c;
};
We kunnen nu een instantie van een Triangle aanmaken en een pointer naar dat object opslaan. Die pointer moet dan van het type Trangle* zijn. We kunnen echter ook naar dat object verwijzen met een pointer naar een Shape.
Code:
int main()
{
	Triangle t(pixelIndex(0, 0), pixelIndex(0, 1), pixelIndex(1, 0));
	Triangle* p1 = &t;
	Shape* p2 = &t;

	return 0;
}
Het object p2 slaat hetzelfde geheugenadres op als het object p1. Het verschil is dat p1 denkt dat er op dat adres een Triangle object staat en p2 denkt dat er op dat adres een Shape object staat. Met de pointer p2 kunnen we echter wel de memberfunctie Draw(...) aanroepen, omdat die in de Shape klasse gedeclareerd is. Laten we eens kijken hoe dit in zijn werk gaat.

Op het moment dat een Triangle object wordt gemaakt, wordt er eerst een Shape object gemaakt. Laten we voor de duidelijkheid het Shape object een member variabele geven. We kunnen bijvoorbeeld ieder figuur een ID geven.
Code:
class Shape
{
public:
	Shape(int id) :m_id(id) {}

	virtual void Draw(std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap) const = 0;
private:
	int m_id;
};
De klasse Shape heeft nu geen default constructor meer. In de constructor van de klasse Triangle moeten we eerst de constructor van Shape aanroepen. Dit hoefde eerst niet, omdat toen de default constructor van Shape automatisch werd aangeroepen.
Code:
class Triangle : public Shape
{
public:
	Triangle(int id, pixelIndex a, pixelIndex b, pixelIndex c)
		: Shape(id), m_a(a), m_b(b), m_c(c)
	{}

	void Draw(std::vector&lt;std::vector&lt;pixelValue&gt; &gt;& bitmap) const
	{
		// draw the triangle
	}
private:
	pixelIndex m_a;
	pixelIndex m_b;
	pixelIndex m_c;
};
Eerst wordt dus de data van het Shape object (de variabele m_id) in het geheugen gezet. Direct daarachter komt de data van het Triangle object. Een pointer naar het Shape object verwijst naar het geheugenadres waar de waarde van m_id begint. Een pointer naar het Triangle object verwijst naar datzelfde geheugenadres. In het laatste geval weet de compiler echter ook nog van het bestaan van de zes integers na de m_id (m_a, m_b en m_c). De compiler weet hier niet van als we een pointer naar een Shape object hebben.

Pointers02.gif

Terug naar het begin. Hoe houden we nu een verzameling vlakke figuren bij? We zorgen er dus allereerst voor dat alle vlakke figuren van de klasse Shape zijn afgeleid. Ze moeten dan dus allemaal de virtuele functie Draw(...) implementeren. Vervolgens kunnen we een vector van pointers naar Shape objecten opslaan. De concrete figuren die we in de tekening willen hebben, moeten we dan dynamisch alloceren. We kunnen ze niet statisch alloceren, omdat ze moeten blijven bestaan totdat de tekening wordt afgesloten en bijgehouden moeten worden in de vector.

De hele code is terug te vinden in pointers08.cpp


Gebruikte code:
http://ncf.ddrmmr.nl/code/pointers04.cpp
http://ncf.ddrmmr.nl/code/pointers05.cpp
http://ncf.ddrmmr.nl/code/pointers06.cpp
http://ncf.ddrmmr.nl/code/pointers07.cpp
http://ncf.ddrmmr.nl/code/pointers08.cpp

[/hand]
 
Laatst bewerkt door een moderator:
[hand]
Waar moet je op letten als je pointers gebruikt?

We hebben al gezien hoe je een pointer kunt krijgen naar een object en hoe je de waarde van een pointer kunt opvragen. Nog even ter herhaling:

operator | naam | voorbeeld | omschrijving
& | pointer to | int x; int* p = &x; | Met een ampersand voor een variabele krijg je een pointer naar die variabele.
* | dereference | int y = *p; | Met een asterisk voor een pointer krijg je de waarde waarnaar de pointer verwijst.

Je kunt dit op allerlei manier combineren. Bijvoorbeeld
Code:
// pionter09.cpp
#include &lt;iostream&gt;

int main()
{
	int x = 1;
	int y = 2;
	int* p = &x;
	int** pp = &p;
	*pp = &y;
	if (*p == 2)
		std::cout &lt;&lt; "ok" &lt;&lt; std::endl;
	else
		std::cout &lt;&lt; "kan niet" &lt;&lt; std::endl;

	return 0;
}


Vuistregels bij het gebruik van pointers

Bij het gebruik van pointers is er n ding heel belangrijk. Dereference je een pointer die niet naar een valide object verwijst, dan krijg je ongedefinieerd gedrag. Ongedefinieerd gedrag is een van de ergste programmeerfouten die je kunt maken. Het betekent letterlijk dat je niet weet wat je programma zal doen. Het kan zijn dat het prima loopt, het kan zijn dat het crasht, het kan zijn dat het even later crasht. Als er ongedefinieerd gedrag in je programma zit, kan het zijn dat het programma al je tests doorstaat, maar wanneer het gebruikt wordt toch crasht. Zulke fouten kunnen erg moeilijk zijn om op te sporen. :ph34r:

Door goed met pointers om te gaan, kun je ongedefinieerd gedrag voorkomen. Ten eerste moet je er altijd voor zorgen dat als een pointer niet naar een valide object verwijst de pointer gelijk is aan 0. Dit doe je in eerste instantie door een pointer te initialiseren met 0 of het geheugenadres van een valide object. Dit hebben we ook gedaan in de klasse Maximum in het stuk over [linkje]Indirectie[/linkje].

Ten tweede moeten we altijd controleren of een pointer niet nul is, voordat we die dereferencen. Je kunt dit hooguit achterwege laten als je kunt bewijzen dat de pointer naar een valide object moet wijzen, maar dan nog is het verstandig een assertion in de code te zetten. De code kan namelijk altijd veranderen, waardoor het wel mogelijk wordt dat de pointer nul is op het moment dat je hem wilt dereferencen. In het voorbeeld met de klasse Maximum hadden we in de main functie beter kunnen zetten:
Code:
#include &lt;assert.h&gt;

// ...

int main()
{
	int a = 1, b = 3, c = 2;

	Maximum m;
	m.Evaluate(a);
	m.Evaluate(b);
	m.Evaluate(c);

	assert(m.GetMaximum());
	std::cout << "Maximum of a, b and c: " << *m.GetMaximum() << std::endl;
	*m.GetMaximum() -= 3;
	std::cout << "The value of b is " << b;

	return 0;
}
Als de functie GetMaximum nu 0 terug geeft, krijg je in debug compilatie een foutmelding. In release compilatie doet assert helemaal niks, dus kost het ook geen rekentijd.

Er is slechts n uitzondering op deze regel. Als we delete aanroepen op een pointer die naar geheugenadres 0 wijst, gebeurt er niks. Voordat je een dynamisch gealloceerd object vrijgeeft door delete aan te roepen, hoef je dus niet te controleren of de pointer ongelijk is aan nul.


Vuistregels bij het gebruik van dynamische allocatie

Als je objecten dynamisch gaat alloceren moet je voor twee valkuilen opletten. Ten eerste moet je al het geheugen dat is gealloceerd ook weer vrij geven. Dus voor iedere keer dat je 'new' aanroept, moet je precies n keer 'delete' aanroepen. Doe je dit niet, dan heb je geheugenlekken in je programma. Als je geneog geheugen lekt, kan je programma of het systeem waar je het op draait een tekort aan geheugen krijgen, waardoor je programma niet uitgevoerd kan worden of zelfs crasht of (in het ergste geval) het hele systeem crasht. In de praktijk zul je niet heel snel iets merken van een geheugenlek, maar het is wel degelijk een programmeerfout.

Ten tweede mag je delete alleen aanroepen op een pointer naar een object dat is gealloceerd met new (niet meer dan n keer) of een nul-pointer. Roep je delete aan op een pointer naar een object dat je al hebt vrijgegeven of een niet valide object (behalve 0) dan heb je ongedefinieerd gedrag.

Voor deze valkuilen zijn er helaas geen simpele truckjes om ze te voorkomen. Je zult goed je verstand moeten gebruiken en de structuur van je code goed moeten doordenken. Op geheugenlekken kun je met bepaalde software controleren. Deze software geeft aan het eind van het programma weer waar new is aangeroepen, zonder dat er later een delete is aangeroepen.

Het tweemaal deleten van een pointer kun je voorkomen door nadat je delete aanroept, de pointer op nul te zetten. Daarnaast moet je er voor zorgen dat er precies n object verantwoordelijk is voor het vrijgeven van een object dat dynamisch is gealloceerd (dit heet eigenaarschap). Andere objecten die een pointer naar het dynamisch gealloceerde object hebben, kunnen er dan niet zomaar vanuit gaan dat die pointer nog valide is. Dit is makkelijk te doen door gebruik te maken van std::auto_ptr. auto_ptr zorgt er ook voor dat het dynamisch gealloceerde object wordt vrijgegeven. Je hoeft zelf dus geen delete meer aan te roepen. Het nadeel van auto_ptr is dat het niet altijd doet wat je verwacht. Iets dat je zeker niet moet doen is een auto_ptr in een container (bijv. vector) stoppen.

Op het moment dat het echt niet mogelijk is om n object aan te wijzen dat verantwoordelijk is voor het vrijgeven van een dynamisch gealloceerd object, kun je gebruik maken van smart pointers. Wat deze pointers doen, is tellen hoeveel pointers er naar een object verwijzen en het object verwijderen als er geen pointers meer naar verwijzen. Het probleem met smart pointers is dat wederzijdse verwijzingen ervoor zorgen dat objecten niet verwijdert worden.
Bijvoorbeeld: Een klasse A heeft een smart pointer naar een klasse B. De klasse B heeft een smart pionter naar de klasse A. Als we nu een object van A en van B hebben waarin de smart pointers naar elkaar verwijzen, zullen de objecten nooit worden verwijderd, omdat er altijd tenminste een smart pointer naar het object verwijst.
Ook bij het gebruik van smart pointers zul je dus op moeten letten dat het design van je code goed in elkaar zit.

Goede implementaties van smart pointers zijn te vinden in de boost bibliotheek (documentatie, download) en in de Loki bibliotheek (documentatie, download).


Vuistregels bij het gebruik van overerving

Het gebruik van overerving kan een heleboel problemen veroorzaken, maar ik ga hier alleen in op het gebruik van overerving met dynamische allocatie.
In het stuk over [linkje]Polymorfisme[/linkje] hebben we een container gemaakt van pointers naar dynamisch gealloceerde objecten die zijn afgeleid van een gezamenlijke basisklasse. Als we de objecten weer willen vrijgeven, roepen we dus delete aan op een pointer naar de basisklasse. We moeten er echter wel voor zorgen dat het hele object (dus ook het deel van de afgeleide klasse) wordt vrijgegeven. Dit kunnen we doen door de destructor van de basisklasse virtueel te maken.
Code:
class Shape
{
public:
	Shape() {};
	virtual ~Shape() {};
};
Als we nu delete aanroepen op een pointer naar een Shape object, wordt eerst de destructor van alle afgeleide klassen aangeroepen en pas dan de destructor van Shape. De destructor van de afgeleide klassen hoeven we niet expliciet virtueel te maken, omdat de destructor van de basisklasse al virtueel is.
Samengevat: Een klasse waar andere klassen van kunnen worden afgeleid moet een virtuele destructor hebben, tenzij de klasse zelf is afgeleid van een klasse die al een virtuele destructor heeft.


Nog eentje om het af te leren

Nog een laatste vuistregel bij het gebruik van pointers. Als je in een klasse een member variabele hebt die een pointer is naar een dynamisch gealloceerd object en een instantie van die klasse is verantwoordelijk voor het vrijgeven van dat object, dan zul je de copy constructor, assignment operator en destructor van de klasse moeten implementeren. Het prototype van deze drie ziet er als volgt uit:
Code:
class A
{
public:
	// copy constructor
	A(const A& src);
	// assignment operator
	A& operator =(const A& src);
	// destructor, make virtual if necessary
	~A();
};
De variabele naam src in de copy constructor en assignment operator staat voor source (bron). Hier worden ook wel eens andere namen voor gebruikt.
Met de copy constructor kun je een object initialiseren met een object van hetzelfde type. De twee objecten zullen dan een kopie van elkaar zijn. Met de assignment operator kun je aan een object een object van hetzelfde type toewijzen. Ook hier zullen de twee objecten een kopie zijn van elkaar. Het verschil tussen de copy constructor en de assignment operator is dat je de eerste alleen kunt gebruiken op het moment dat je een object instantieert.

Als je de copy constructor of assignment operator in een klasse niet definieert, wordt hiervoor automatisch een standaard implementatie gemaakt. Deze standaard implementatie kopieert (of initialiseert) de waarde van iedere member variabele met de waarde van de bijbehorende member variabele in het andere object. Als je dus een pointer als member variabele in een klasse hebt staan, zal de default implementatie van copy constructor en assignment operator het geheugenadres van de pointer kopiren. Als de klasse echter eigenaar is van een dynamisch gealloceerd object, wil je niet dat de pointer hiernaar wordt gekopieerd, maar dat het hele object wordt gekopieerd. Dit zul je dus zelf moeten implementeren in de copy constructor en assignment operator.

De vuistregel die bij deze drie member functies hoort, is: moet je n van de drie copy constructor, assignment operator of destructor implementeren, dan moet je ze waarschijnlijk alle drie implementeren. Heb je geen zin om de assignment operator of copy constructor te implementeren wanneer je dit wel zou moeten doen, zorg er dan in ieder geval voor dat je ze als private declareert. Op die manier wordt de default implementatie niet gebruikt en krijgt iemand die de functies aanroept een nette compiler error.
Code:
class A
{
public:
	A() : m_p(new int(0)) {}
	~A() { delete m_p; }
private:
	// too lazy to implement these
	A(const A&);
	A& operator =(const A&);
};


Je ziet dat er nogal wat dingen zijn die fout kunnen gaan als je gebruik maakt van pointers en dat je goed moet weten hoe hiermee om te gaan. Dat is ook precies de reden dat ze evil worden genoemd; je moet pointers pas gebruiken als je ze echt nodig hebt. Ook ervaren software programmeurs maken nog wel eens fouten als ze pointers gebruiken. Het is belangrijk dat je jezelf leert om goed met pointers om te gaan, zodat je dergelijke fouten in je programma kunt ontdekken. Het feit dat je programma een keer goed draait (of een aantal keer) betekent nog niet dat het altijd foutloos zal draaien. :acurate:


Gebruikte code;
http://ncf.ddrmmr.nl/code/pointers09.cpp

[/hand]
 
Laatst bewerkt door een moderator:
Status
Niet open voor verdere reacties.
Steun Ons

Nieuwste berichten

Terug
Bovenaan