Pointers en references

Zowel pointers als referenties dienen om onrechtstreeks toegang tot een variabele te krijgen. De syntax (schrijfwijze) is echter verschillend.

Een pointer is een variabele die een adres als waarde heeft.

	int  a = 7;
	int* b;     // b is een pointer naar int
	b = &a;     // b krijgt als waarde het adres van a
	            // men zegt: "b wijst naar a"
	*b = 5;     // manipuleer a indirect via b
	cout << a;  // output: 5;
Het verkrijgen van de waarde waar een pointer naar wijst via de operator* noemt men een dereference-operatie.

Een referentie-variabele, kortweg referentie (reference) combineert de betekenis van een pointer met de syntax van een gewone variabele. Een referentie is een alias (andere naam) van een bestaande variabele.

	int  a = 7;
	int& c = a;  // c is referentie en wordt aan a "gebonden"
	             // men zegt: "c is een alias van a"
	c = 5;       // manipuleer a indirect via c
	cout << a;   // output: 5

Een referentie "binden"

Opgelet: men moet een referentie onmiddellijk bij zijn definitie "binden" (specifiëren naar welke variabele deze wijst):

	char c;
	char& d = c;  // ok: d is alias van c
	char& e;      // fout
Men kan dus de "binding" niet uitstellen. (opm. ook als veld in een klasse gelden speciale regels: zie verder)

Ook een referentie opnieuw "binden" kan niet:

	double x = 2.0, y = 4.0;
	double& r = x;
	r = y;          // x krijgt de waarde van y
	                // niet: r is (vanaf hier) een alias van y 
Pointers opnieuw binden kan uiteraard wel.

Invoer- uitvoerparameter

Beschouw volgende procedure die zijn argument wijzigt:

void init(double &x) {
	x = 10;
}
void main() {
	double y;
	init(y);
}
Wat men soms invoer- uitvoerparameter noemt, is niets anders dan een referentie!

In pure C, moe(s)t men dit oplossen d.m.v. een pointer-argument:

void init(double *x) {
	*x = 10;
}
void main() {
	double y;
	init(&y);
}
Dit is vervelend voor de schrijver van init omdat er telkens een sterretje voor x moet komen, maar wellicht nog vervelender voor de gebruiker van deze procedure, omdat hij de adres-van operator moet gebruiken. Typisch voorbeeld is de standaard functie scanf().

Referenties als klasse-attribuut

Een klasse-attribuut (veld, lid) kan ook een referentie zijn. Dit betekent dat dit attribuut een alias is van een andere variabele. Wijzigingen aan dit attribuut hebben ook effect op deze variabele.

Indien een klasse een referentie-attribuut heeft, dan moet deze geïnitialiseerd worden in een initializer list.
Typisch voorbeeld:

class A {
	int &r;
public:
	A() {}                             // mag niet!!! r niet gebonden

	A(int &r_) { r = r_; }             // mag niet!!!

	A(int &r_) : r(r_) {}              // ok
};

Opm. men moet ook een initializer-list gebruiken bij gewone attributen indien deze geen default constructor hebben. Idem voor erven van klasse zonder default constructor.

null

In tegenstelling tot Java bestaat er in C++ geen null-referentie. De ontwerper van C++ (B. Stroustrup) heeft er voor geopteerd dat een referentie ten allen tijde naar een "echt" (niet null) object wijst.

Indien met toch null wenst te gebruiken, bv. om aan te geven dat een object (nog) geen waarde heeft, dan zal men een pointer moeten gebruiken, uiteraard met alle risico's vandien.

	int *p = 0;         // null-pointer
	cout << *p;   // "fout: null dereference"

Sterretjes en ampersands

Voor beginnelingen is het dubbel gebruik van het sterretje (*) verwarrend: int *b; is een pointerdeclaratie (eenmalig); daarna betekent *b "de waarde waar b naar wijst". Analoog heeft de ampersand (&) twee betekenissen: int &c is een referentiedeclaratie, terwijl elders &a "het adres van a" betekent.

Syntax

Er heerst enige discussie over de precieze schrijfwijze van een pointerdeclaratie: int *p; ofwel int* p;? M.a.w.: hoort het sterretje bij de variabele (p) of bij het datatype (int)? Het tweede klinkt logischer, maar historisch is het anders gegroeid: in C heeft men (spijtig genoeg) de tweede keuze gemaakt, en C++ is gevolgd. (Analoog schrijft men in C/C++ int t[]; voor een tabeldefinitie, terwijl Java en C# het logischere int[] t; kiezen.)

De logische stijlkeuze (* bij het datatype) kan problematisch worden bij meervoudige declaraties:

	double* p, q;  // opgelet: q is GEEN pointer, maar een gewone double !

	double *a, *b; // geen probleem: a en b zijn pointers

Analoog voor referenties: int& x = a; is eingszins logischer dan int &x = a;, maar het laatste is eigenlijk de oorspronkelijke stijl.

Bij functieargumenten is er geen probleem, omdat het type telkens herhaald moet worden.

Advies: kies en wees consequent. Je mag het sterretje aan het datatype plakken (zoals double* a;), maar let dan op bij meervoudige declaraties.

"Grote" objecten en const

Om efficiëntieredenen moet men vermijden "grote objecten" nodeloos te kopiëren (clonen in Java). Dit komt bv. voor indien dergelijk object als parameter aan een functie wordt meegegeven, of indien het een attribuut is van een ander object. Referentie- of pointerparameters zijn vaak ideaal om dergelijke onnodige kopies te vermijden.
Voorbeeld:

class Groot {
	int tabel[1000];
	...
};

void schrijf(Groot g) ...        // kopie niet nodig 

void schrijf(Groot &g) ...       // beter

void schrijf(Groot *g) ...       // kan ook

void init(Groot g) ...           // FOUT; g is kopie, origineel wordt niet veranderd

class XYZ {
	Groot g;					// is een kopie werkelijk nodig?
	...
};

Een referentie- of pointerargument mag dan wel geschikt zijn om onnodige kopies te vermijden, het creëert echter een "veiligheidsprobleem": de procedure/functie kan het argument wijzigen, wat veelal niet de bedoeling is. Oplossing: men kan het keyword const gebruiken om aan te geven dat een referentie- of pointerargument niet gewijzigd mag worden. Zo zal men bv. schrijven

void schrijf(const Groot &g) ...

Ook een klasse-attribuut kan const zijn: de klasse belooft dit attribuut niet te zullen wijzigen: class ABC { const Groot &g; };

const kan ook bij pointer-argumenten of pointer-attributen. Opm. const bij een gewoon functieargument heeft geen zin; vermits de functie krijgt een kopie van het actuele argument krijgt, hebben wijzigingen aan dit argument geen effect op het origineel.

Opm. ook lidfuncties kunnen const zijn: dit betekent dat ze beloven het object zelf (*this) niet te wijzigen. Het getuigt van goede stijl zoveel mogelijk lidfuncties const te maken.

Referenties versus pointers

Polymorfisme en virtuele functies

Polymorfisme is enkel mogelijk via pointers of references. Een pointer of referentie naar een (object van een) basisklasse kan wijzen naar een (object van een) afgeleide klasse. Indien je een virtuele lidfunctie oproept, dan wordt deze van de afgeleide klasse opgeroepen ("dispatching")
Voorbeeld:

class Parent {
public:
	virtual f() { cout << "Parent"; }
}

class Child : public Parent {    // erft
public:
	virtual f() { cout << "Child"; }
}

int main() {
	Parent b;
	Child  c;
	b.f();      // "Parent"
	c.f();      // "Child"
	
	Parent *p = &c;  // pointer naar ouderklasse wijst eigenlijk naar kind
	p->f();     // "Child" !!!!

	Parent &r = c;  // ref. naar ouderklasse wijst eigenlijk naar kind
	r.f();      // "Child" !!!!

	Parent x = c;   // gevaarlijk: alle "Child"-info gaat verloren
	x.f();      // "Parent" !!!!
}

Opmerking: in Java zijn lidfuncties by default virtueel, in C++ moet je dit expliciet vermelden.

Verschillen met Java

In Java bestaan er geen pointers. De primitieve types (int, double...) zijn value types, terwijl alle klasse-objecten reference types zijn.

Een int-variabele in Java gedraagt zich dus precies zoals een gewone int-variabele in C++. Een klasse-variabele is vanzelf een referentie.

In Java kunnen referenties null zijn, en opnieuw gebonden worden. Om een (groot) object te kopiëren moet men expliciet de clone()-methode gebruiken.

	int a;
	a = 5;                 // toekenning
	ArrayList d = null;
	d = new ArrayList();   // "binding"

	void proc(int x, ArrayList y) ...   // x pass by value, y pass by reference

this is in Java een referentie, in C++ een pointer

Opgelet met new: in Java geeft dit een reference terug, in C++ echter een pointer. In Java dealloceert de garbage collector automatisch objecten die niet meer gebruikt worden, in C++ moet men dit zelf doen (destructor)

[W. Schepens sept. 2004]