1 1 I N H O U D S T A B E L 1. Uitbreidingen en verbeteringen in C++ t.o.v. C Inleiding Het gebruik van de iostream bibliotheek In- en uitvoer De iost...
Typedef .................................................................................. 13 Het type const ............................................................................... 14
1.5.1
Het const type ......................................................................... 14
1.5.2 1.6
Expressies ............................................................................... 14 Het type referentie ......................................................................... 15
Inline specifier en inline functie .................................................. 27
1.10.2
Functie versus macro ................................................................ 27
1.10.3 1.11
Vraag aan de compiler .............................................................. 29 Default functie-argumenten ............................................................. 29
Overloaden van de new operator ................................................ 34
1.12.5 1.13
Gebruik van de set_new_handler ................................................ 35 Namespaces .................................................................................. 36
1.13.1
Het concept ‘namespaces’ van C++ ............................................ 36
1.13.2
Gebruik van variabelen.............................................................. 36
Constructor met default argumenten ........................................... 53
2.5.6
Constructor overloading en default argumenten ............................ 54
2.5.6.1 2.5.6.2 2.5.7
Probleem en oplossing ........................................................... 54 Programma voorbeeld ............................................................ 54 Destructor ............................................................................... 55
2.6
Object als argument van een lidfunctie .............................................. 57
2.7
Constante objecten en constante lidfuncties ....................................... 59
De friend operator .................................................................... 83
3.7 3.7.1 3.7.2
Conversie van een klasse naar een standaardtype ............................... 85 De conversie-functie ................................................................. 85 Impliciete en expliciete aanroep van een conversie-functie ............. 86
3.8
Conversie tussen klassen ................................................................. 87
Data hiding............................................................................ 103
4.5.7
Invloed van overerving op de bescherming van leden van een klasse103
4.5.8
Een ‘is een’ – relatie ............................................................... 104
4.5.9 4.6
Beschermende interface via een afgeleide klasse......................... 104 Meer dan één afgeleide klasse ........................................................ 106
Constructoren en afgeleide klassen ........................................... 109 Afgeleide klasse van een afgeleide klasse ......................................... 110
Functietemplate versus preprocessor-macro ............................... 123
4.10.4.1 4.10.4.2
Voorbeeld met preprocessor-macro ....................................... 123 Met een functietemplate ...................................................... 123
4.10.5
Werking van de functietemplate................................................ 124
Programmavoorbeeld .............................................................. 134 Abstracte basisklasse .................................................................... 136 Doel en gebruik van een abstracte klasse................................... 136
5.4.2
Nut van een abstracte klasse.................................................... 136
5.4.3
Definitie van een abstracte klasse ............................................. 136
6. De I/O – bibliotheek van C++ ....................................................140 T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
5
6.1
De isostream – bibliotheek............................................................. 140
6.2
Het begrip stream......................................................................... 140
6.3
De verschillende streamklassen ...................................................... 141
Objecten in een bestand .......................................................... 159 Algemeen........................................................................... 159
6.9.3.2
Schrijven van objecten naar een bestand ................................ 159
6.9.3.3
Lezen van objecten uit een bestand ....................................... 160
6.9.3.4 6.9.4
Tegelijk objecten lezen uit en schrijven in een bestand ............. 161 Random file access ................................................................. 162
Binair bestand versus tekstbestand ........................................... 165
6.11.2
Schrijven van een object naar een binair bestand........................ 165
6.11.3
Lezen van een object uit een binair bestand ............................... 167
6.11.4
Lezen en schrijven van objecten via lidfuncties ........................... 168
6.11.5 6.12
Bijwerken van een bestand met gebruik van random access ......... 170 Kopiëren van een bestand.............................................................. 171
6.12.1
Kopiëren van een bestand, byte per byte. .................................. 171
6.12.2
Kopiëren van een bestand met rdbuf()....................................... 172
6.12.3
Kopiëren van een bestand naar het beeldscherm......................... 172
6.12.4 6.13
Kopiëren van een bestand naar de printer .................................. 173 Oefeningen .................................................................................. 173
De CObject-klasse ........................................................................ 175
7.2.1
De CObject-klasse .................................................................. 175
7.2.2 7.3
Basiseigenschappen van de CObject-klasse ................................ 175 De CFile-klasse ............................................................................ 175
Werking van exception handling ............................................... 182
7.4.3
Exception handling met verschillende types ................................ 183
7.4.4
Definiëren van een exceptie in een klasse .................................. 185
7.4.5
Exception-klasse met informatie ............................................... 186
7.4.6
Het exception mechanisme ...................................................... 187
7.4.7
Teruggooien van een reeds opgevangen exceptie ........................ 189
7.4.7.1 7.4.7.2 7.4.8
Re-throw............................................................................ 189 Voorbeeld........................................................................... 190 Het voorkomen van geheugenlekken bij excepties ....................... 191
Containerklasse...................................................................... 192 Wat is een containerklasse? .................................................. 192
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
7
7.5.1.2
Voorbeeld van containerklasse .............................................. 192
7.5.1.3
Containerklasse versus afgeleide klasse .................................. 193
Inleiding tot het gebruik van de STL-bibliotheek....................... 195
7.5.2.3
Lidfuncties van een container ................................................ 198
7.5.2.4
De Vector container ............................................................. 198
7.5.2.5
De List container ................................................................. 199
7.6 7.6.1
Klassen voor algemeen gebruik ...................................................... 200 De CTime-klasse .................................................................... 200
7.6.2
De CString-klasse ................................................................... 200
1. Uitbreidingen en verbeteringen in C++ t.o.v. C 1.1 Inleiding Het succes van C++ als programmeertaal is voor een groot stuk te wijten aan de opkomst van GUI–systemen (Graphical User Interface), zoals bv. Windows. Het programmeren in een GUI-systeem vraagt de manipulatie van velerlei ‘objecten’, zoals vensters, menu’s, knoppen, enz. Bovendien is een GUI ‘event driven’. Dit betekent dat een bepaald object slechts geactiveerd wordt als er een ‘bericht’ naar toe gestuurd wordt, zoals bv. een muisklik. Dit zijn allemaal aspecten die in object georiënteerd programmeren aan bod komen. Zodoende zal een OOP-taal, zoals C++, praktisch noodzakelijk zijn in dergelijke omgevingen. Bovendien worden de besturingsystemen qua opbouw meer en meer object georiënteerd. Zo is Windows NT een typisch object georiënteerd systeem, waarin alle elementen van het besturingsysteem als objecten worden gehandeld, nl. de user-interface, de disk-interface en netwerk-interface. Een interessant aspect van de C++ taal is het feit dat ze volledig gebaseerd is op de C-taal. C++ laat toe om object georiënteerd te programmeren zonder de opgedane ervaring in het programmeren in C te verlaten. In dit deel willen we de verschillende uitbreidingen en verberingen van C++ aanhalen t.o.v. C, die niet rechtstreeks te maken hebben met de filosofie van object georiënteerd programmeren. Deze uitbreidingen en verbeteringen zijn dikwijls wel wenselijk om het toevoegen van object georiënteerde principes mogelijk te maken. We kunnen zeggen dat deze uitbreidingen de noodzakelijke steunpilaren zijn waarop de C++ taal rust. Tevens zijn vele verbeteringen aangebracht om het programmeren veiliger en meer bugvrij te maken.
1.2 Het gebruik van de iostream bibliotheek 1.2.1 In- en uitvoer C++ kent een totaal nieuwe manier van werken met in- en uitvoer. Hiervoor zijn ook nieuwe headerfiles beschikbaar. De vertrouwde C-functies blijven evenwel beschikbaar, doch het mengen van beide moet worden afgeraden. De standaard in- en uitvoer bibliotheek van C++ is de iostream bibliotheek.
1.2.2 De iostream bibliotheek 1.2.2.1 Algemeen De iostream bibliotheek is de input/output bibliotheek die standaard bijgeleverd is met de C++ compiler. Deze bibliotheek maakt input en output naar het scherm, naar files en naar geheugen mogelijk. De iostream bibliotheek vervangt de stdio bibliotheek van C.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
9
De iostream bibliotheek is niet ingebouwd in de C++ taal zelf, maar vormt een aparte klassen-bibliotheek. Het voordeel van de iostream bibliotheek is het feit dat ze gebruik maakt van alle C++ eigenschappen. Dit betekent dat we input en output operaties kunnen laten verlopen via operatoren. Bovendien kunnen we, indien noodzakelijk, de functionaliteit van de iostream bibliotheek aan onze eigen wensen aanpassen door gebruik te maken van afgeleide klassen.
1.2.2.2 Het begrip stream We kunnen een ‘stream’ beschouwen als een equivalent van een input/output device. Als dusdanig representeert een stream altijd een ‘medium’, waarnaar we input/output operaties uitvoeren. In C++ is een stream een ‘object’, behorend tot een specifieke streamklasse. De karakteristieken van het stream-object worden bepaald door de klasse waartoe het behoort, en door de eigen toevoegingen aan deze klasse.
1.2.2.3 De verschillende streamklassen Er zijn zes belangrijke streamklassen: •
istream klasse die input verzorgt afkomstig van het toetsenbord
•
ostream klasse die output verzorgt naar het scherm
•
ifstream klasse die input verzorgt afkomstig van files
•
ofstream klasse die output verzorgt naar files
•
istrstream klasse die intput verzorgt afkomstig van het geheugen
•
ostrstream klasse die output verzorgt naar het geheugen
Daarbij is er nog de iostream klasse, die afgeleid is uit de klassen istream en ostream.
1.2.3
Uitvoer De standaard outputstream noemt cout. Gegevens naar de output sturen gebeurt met de insertion operator <<. Voorbeeldprogramma. /* programma P110.cpp */ #include void main() { char c= ‘A’; float f= 12.57; cout << “Hello, world.\n”; cout << “c = “ << c << “\n”; cout << “f = “ << f << “\n”; } Dit kleine programma bevat de twee belangrijkste bouwstenen van de C++ taal: het object en de operator. Het object is cout, dat de ‘standaard output’ representeert. De standaard output is het beeldscherm. Met andere woorden, cout representeert het beeldscherm.
10
De operator is <<, die gebruikt wordt om gegevens naar de standaard output te sturen. Merk op dat in tegenstelling tot de functie printf() het in C++ niet nodig is om te specificeren dat een string, of een integer, of een karakter of een float moet worden uitgeprint op het scherm. De uitvoering van de operator << verschilt naargelang de operator inwerkt op een string, of op een integer, of op een karakter of op een float.
1.2.4 Invoer De standaard inputstream noemt cin. De standaard input is het toetsenbord. Met andere woorde, cin representeert het toetsenbord. Gegevens lezen van de input gebeurt met de extraction operator >>. Voorbeeldprogramma. /* programma P111.cpp */ #include void main() { char c; float f; char sr[30]; cout << “Typ een letter in: \n”; cin >> c; cout << “Typ een gebroken getal in: \n”; cin >> f; cout << “Typ een string in: \n”; cin >> s; cout<<”De ingevoerde letter is “<< c <<endl; cout<<”Het ingevoerde getal is “<< f <<endl; cout<<”De ingevoerde string is “<< s <<endl; } Ook bij cin moet het type van de in te voeren variabele niet worden meegegeven.
1.2.5
I/O manipulatoren
1.2.5.1 Opmaak van getallen Met de manipulatoren dec, hex en oct kan een getal worden weergegeven in respectievelijk decimaal, octaal en hexadecimaal formaat. De functie setbase(int n) bepaalt het talstelsel voor de weergave van het eerst volgende getal. De waarde van n bepaalt het talstelsel: n= 0 (default) : 10-tallig stelsel n= 8
: 8-tallig stelsel
n= 10
: 10-tallig stelsel
n= 16
: 16-tallig stelsel
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
11
De functie setprecision(int n) bepaalt het aantal decimalen bij floating point getallen. /* programma P112.cpp */ #include #include void main() { int i; i= 16; cout << dec << i << endl;
// 16
cout << oct << i << endl;
// 20
cout << hex << i << endl;
// 10
cout << setbase(16) << 16 << ‘\t’ << 16 << endl;
// 10
cout << setprecision(4) << 3.1415926535 << endl;
// 3.1415
16
}
1.2.5.2 Algemene opmaak endl
vervangt “\n”
setw(int n)
dient om de breedte van een veld op te geven
setfill(int n)
dient om het opvulkarakter aan te geven bij gebruik van setw()
1.2.5.3 Uitlijnen Volgend programma lijnt de eerste uit te printen kolom links uit en de tweede kolom wordt rechts uitgelijnd. /* programma P114.cpp */ #include #include void main() { double waarden[] = {1.36, 45.14, 8.124, 4589.44568} char *namen[] = {“Els”, “Jef”, “Ria”, “Jan”}; for (int i= 0; i < 4; i++) { cout << setiosflags(ios::left) << setw(6) << namen[i] << resetiosflags(ios::right) << setw(10) << waarden[i] << endl; } }
12
1.3
Het plaatsen van de declaraties van variabelen In C kan een declaratie van een variabele slechts gebeuren aan het begin van een blok. Een blok is hier de verzameling code binnen de accolades. In C++ is het mogelijk een declaratie te schrijven gelijk waar in een blok. Een variabele moet dus niet vooraan in een blok gedeclareerd worden. De variabele is gekend van de regel van declaratie tot het einde van de scope. Voor een lokale variabele is dat tot het einde van het blok. Voor een globale variabele is dat tot het einde van het programma. Zo is het volgende toegelaten: { double ds1 = sin(1.0); ds1 *= 2; double ds2 = cos(1.0); ds2 *= 2; } Deze eigenschap wordt veel gebruikt in for-loops: for (int i= 0; i < 10; i++) { // code } Echter is een declaratie niet toegelaten in een stuk code dat slechts conditioneel kan uitgevoerd worden: { if (x < 10) int y = 20;
//fout
int y = 10;
//fout
else } Voorbeeld van scope: main() { // x is niet gekend // i is niet gekend // j is niet gekend int x; // x is gekend // i is niet gekend // j is niet gekend
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
13
for (int i= 0; i < 32767; i++) { // x is gekend // i is gekend // j is niet gekend int j; // x is gekend // i is gekend // j is gekend } // x is gekend // i is niet gekend // j is niet gekend }
1.4 Typeconversie 1.4.1 Converteren In C++ plaatst men de te converteren expressie tussen haakjes, zodat dit lijkt op een functie-oproep. De conversie waarbij het type tussen haakjes staat, blijft ook bruikbaar. int i; char c; i = (int) c;
// C notatie
i = int (c);
// C++ notatie
1.4.2 Typedef In C zijn er twee manieren om met een struct te werken. 1)
struct a { …; }; struct a var1; // dit vraagt veel typwerk bij gebruik van struct
2)
typedef struct struct_a { …; }a; a var1; // dit vraagt minder typwerk bij gebruik van struct, maar de // definitie is zwaar
In C++ kan het keyword struct weggelaten worden bij de definitie van variabelen van een bepaald type struct. struct a { …; }; a var1;
// dit vraagt het minst typwerk
14
1.5 Het type const 1.5.1 Het const type Met het type const wordt een variabele gedeclareerd waarvan de waarde constant moet blijven gedurende de hele uitvoering van het programma. De compiler waakt erover dat zo’n variabele geen nieuwe waarde toegekend wordt. In plaats van #define MAXITEMS 10 kan in C++ het volgende: const int MAXITEMS 10; Het voordeel van de nieuwe schrijfwijze t.o.v. de oude is het feit dat het type, in het voorbeeld int, expliciet geverifieerd wordt telkens de variabele wordt gebruikt. const int GROOTTE 30; GROOTTE = 40;
1.5.2
// compiler genereert een fout
Expressies De expressie const int *pci; declareert een pointer die verwijst naar een ‘const int’. Dit betekent dat aan pci wel vrij een adres kan worden toegekend, maar pci verwijst telkens naar een niet te veranderen waarde. Dus:
const int d = 3; pci = &d;
// OK
*pci = 3;
// FOUT
In volgend programmavoorbeeld wordt de pointer p gedeclareerd naar een const int. Dit betekent dat aan de variabele p wel vrij een adres kan worden toegekend, doch dat p telkens wijst naar een niet te veranderen waarde. /* programma P115.cpp */ #include void main() { const int a= 5, b= 10; const int *p= &a; cout<<”Inhoud van p: “ << (void *)p << endl; cout<<”p wijst aan p= &b;
: “ << *p << endl;
// ander vrij adres
cout<<”Inhoud van p: “ << (void *)p << endl; cout<<”p wijst aan
: “ << *p << endl;
}
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
15
De expressie int i; int *const cpi = &i; declareert de constante pointer cpi naar een integer. Deze pointer cpi kan naar niets anders wijzen, maar hetgeen de pointer cpi aanwijst kan wel veranderd worden. Volgend programma declareert een constante pointer p naar een int. De pointer p kan naar niets anders wijzen, maar hetgeen p aanwijst kan wel worden gewijzigd. Dit is een belangrijk verschil met de definitie van p in vorig programma. /* programma P116.cpp */ #include void main() { int a= 5, b= 10; int *const p= &a; cout<<”Inhoud van p: “ << (void *)p << endl; cout<<”p wijst aan a= 10;
: “ << *p << endl;
// p wijst andere waarde aan
cout<<”Inhoud van p: “ << (void *)p << endl; cout<<”p wijst aan
: “ << *p << endl;
}
Men kan const ook gebruiken bij het declareren van een functie om te beletten dat de functie één van zijn parameters zou wijzigen. Veronderstel het volgende prototype: int readonly(const struct Astruct *AreadonlyStruct); Dit prototype belet dat de structuur waarnaar AreadonlyStruct verwijst gewijzigd kan worden. Wil de programmeur dit toch doen, dan genereert de compiler een fout, zoals in volgend voorbeeld: int readonly(const struct Astruct *AreadonlyStruct) { AreadonlyStruct -> element = 3;
//compiler genereert fout
}
1.6
Het type referentie
1.6.1 Probleem Het volgend programma illustreert een probleem dat kan voorkomen bij het toekennen van een waarde aan de parameters van een functie op basis van ‘doorgeven van waarde’ (call by value). Telkens de functie opgeroepen wordt, worden de waarden van de doorgegeven parameters gekopieerd in de lokale parameters van de functie. Het resultaat hiervan is dat, wanneer een doorgegeven parameter wordt gewijzigd, we die wijziging niet terugzien in de aanroepende functie.
16
/* programma P117.cpp */ #include void increment(int); void main() { int i= 3; cout<<”i =
“ << i << endl;
// i = 3
“ << i << endl;
// i = 3
increment(i); cout<<”i = } void increment(int i) { i++; } Het programma geeft als resultaat 3, alhoewel increment(i) de variabele i lokaal heeft gewijzigd in 4. Wanneer de programmeur wenst dat een functie een aantal parameters moet wijzigen, dan moet de programmeur een pointer doorgeven naar die functie (call by address), zoals getoond in volgend programma. /* programma P118.cpp */ #include void increment(int *); void main() { int i= 3; cout<<”i =
“ << i << endl;
// i = 3
increment(&i); cout<<”i =
“ << i << endl;
// i = 4
} void increment(int* pi) { (*pi)++;
// () zijn nodig omdat ++ voorrang heeft op *
} Het vervelende aan bovenstaande constructie is dat expliciet een pointer moet worden doorgegeven aan de functie met behulp van de ‘adres van operator’ &, en dat telkens de dereferentie operator * moet worden aangewend binnen de functie om bewerkingen te kunnen uitvoeren met de waarden waarnaar de pointer verwijst.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
17
1.6.2 Het type referentie 1.6.2.1 Het referentie datatype C++ lost vorig probleem op met het referentie datatype. Eens gedeclareerd zal een referentie fungeren als een alias (een alternatieve naam). Met andere woorden, een referentie gedraagt zich precies zoals de variabele waarnaar hij refereert. Een referentie-variabele is dus een ‘alias’ voor de variabele waarnaar hij refereert, m.a.w. een referentie is een alternatieve naam voor een variabele. Bewerkingen op een referentie refereren altijd naar de gerefereerde variabele. De unaire & operator indentificeert een referentie. Referenties (E: references) worden het meest gebruikt als parameter. Het voordeel is dat geen data getransfereerd wordt zoals bij call by value, en dat het veiliger is dan een call by address. Referenties kunnen worden gebruikt om meer dan één waarde uit een functie terug te geven, waar in C pointers voor nodig zijn.
1.6.2.2 Programmavoorbeeld Volgend programma maakt gebruik van een referentie. /* programma P119.cpp */ #include void main() { int i, *pi; int &ri = i;
// ri is een referentie naar i, geen &i nodig
i = 5;
// gebruik van i via zijn naam
cout<<”waarde van i via zijn naam: cout<<”adres van i: ri = 10;
“ << i << endl;
“ <<&i << endl;
// gebruik van i via zijn referentie
cout<<”waarde van i via zijn referentie: int j= ri;
“ << i << endl;
// i= 10
// j krijgt de waarde van i, nl. j= 10
cout<<”waarde van j:
“ << j << endl;
ri++; cout<<”waarde van i: pi = &ri;
“ << i << endl;
// i= 11
// pi bevat het adres van i
cout<<”inhoud van pi: “ << pi << endl; } In dit programma is ri een alias voor i. Het maakt geen verschil uit of we i of ri gebruiken bij bewerkingen. Echter is een referentie geen kopie van de variabele waarnaar hij verwijst. Het is wel dezelfde variabele onder een andere naam.
1.6.3 Initialisatie van een referentie Een referentie kan niet bestaan zonder een variabele waarnaar gerefereerd wordt. Volgende code kan dus niet: int &refint;
// fout
18
De uitzondering op de regel is wanneer bijvoorbeeld een referentie wordt gedefinieerd als parameter van een functie. Met andere woorden, een reference moet steeds geïnitialiseerd worden bij declaratie, behalve als de referentie als parameter wordt gebruikt. In het volgend programma wordt geen return gebruikt. Merk op dat geen return nodig is om i en ri een nieuwe waarde te geven. /* programma P120.cpp */ #include void increment(int &); void main() { int i= 3; //int &ri;
//fout: ‘ri’: references must be initialized
int &ri= i; cout<<”i = cout<<”ri =
// declaratie van de referentie ri “ << i << endl;
// i = 3
“ << ri << endl; // ri = 3
increment(ri); // referentie wordt doorgegeven cout<<”i = cout<<”ri =
“ << i << endl;
// i = 4, nieuwe waarde uit increment()
“ << ri << endl; //ri = 4, nieuwe waarde uit increment()
//cout<< “ra= “ << ra << endl;
//fout: ‘ra’:undeclared identifier
} void increment(int &ra)
// declaratie van ra zonder initialisatie
{ ra++; cout<< “ra= “ << ra << endl;
// ra= 4
}
1.6.4 Referentie als functieparameter Het referentie datatype is vooral interessant bij het doorgeven van te veranderen parameters naar een procedure. /* programma P121.cpp */ #include void increment(int &); void main() { int i= 3; int &ri= i; cout<<”i = cout<<”ri =
// declaratie van de referentie ri “ << i << endl;
// i = 3
“ << ri << endl; // ri = 3
increment(ri); // referentie wordt doorgegeven cout<<”i = cout<<”ri =
“ << i << endl;
// i = 4
“ << ri << endl; // ri = 4
} void increment(int &ri) { ri++; }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
19
1.6.5 Verschil tussen pointer en referentie Een referentie is eigenlijk niets anders dan een constante pointer, die we kunnen gebruiken zonder dereferentie operator. Zo kan vorig programma P120 geschreven worden met een constante pointer. /* programma P122.cpp */ #include void main() { int i= 3; int *const ri= &i; cout<<”i =
// ri is een constante pointer naar i
“ << i << endl;
cout<<”ri =
// i= 3
“ << ri << endl; // adres van de geheugenplaats van ri
cout<<”*ri =
“ << *ri << endl;
// 3
increment(ri); // pointer ri wordt doorgegeven cout<<”i =
“ << i << endl;
cout<<”ri =
// i= 4
“ << ri << endl; // adres van de geheugenplaats van ri
cout<<”*ri =
“ << *ri << endl;
// 4
} void increment(int *ri) { *ri++; } Bemerk echter dat een referentie niet kan behandeld worden als een pointer. Een aantal zaken zijn niet mogelijk met een referentie, terwijl die wel mogelijk zijn met een pointer. 1) Het adres nemen van een referentie kan. /* programma P123.cpp */ #include void main() { int i= 3; int &ri= i; cout<<”ri =
// declaratie van de referentie ri “ << ri << endl; // ri = 3
cout<<”adres van ri= “ << &ri << endl; int *pi; pi= π
// het adres van pi wordt toegekend aan pi
cout<<”adres van ri= “ << pi << endl; (*pi)++; cout<<”i = cout<<”ri = }
“ << i << endl; “ << ri << endl;
20
2) Een referentie toekennen met een waarde kan niet. /* programma P124.cpp */ #include void main() { int i= 3; //int &ri= 5;
// een referentie toekennen met een waarde // fout: ‘initializing’: cannot convert from ‘const int’ to ‘int &’
int &ri = i; cout<<”ri =
“ << ri << endl;
ri= 5;
// ri= 3 // dit kan wel
cout<<”i =
“ << i << endl;
cout<<”ri =
“ << ri << endl;
// i= 5 // ri= 5
} 3) Twee referenties vergelijken met elkaar kan. /* programma P125.cpp */ #include void main() { int i1= 3, i2= 5; int &ri1= i1; int &ri2= i2; if (ri1 == ri2) cout<<”gelijk”<<endl; else cout<<”ongelijk”<<endl; cout<<endl; if (ri1 > ri2) cout<<”ri1”<<endl; else cout<<”ri2”<<endl; } 4) Twee referenties optellen kan. /* programma P126.cpp */ #include void main() { int i1= 3, i2= 5; int &ri1= i1; int &ri2= i2; cout<<”ri1 + ri2 = “ << ri1+ri2 << endl; }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
21
5) Enkel een referentie aan een constant object kan geïnitialiseerd worden met een rvalue of met een lvalue van een ander type. /* programma P127.cpp */ #include void main() { int i= 1; double dl1= 2.3, dl2= 10.5; // int &ri= 5;
// fout: ‘initializing’: cannot convert from // ‘const int’ to ‘int &’, m.a.w. geen lvalue
// double &rdl1= i;
// fout: ‘initializing’: cannot convert from // ‘int’ to ‘double &’, m.a.w. 2 verschillende types
// double &rdl= dl1 + dl2; const int &ri= 5;
// fout: geen lvalue
// initialisatie met een rvalue
cout<< “ri= “ << ri << endl;
// ri= 5
const double &rdl1= i; // initialisatie met een lvalue van een ander type cout<< “rdl1= “ << rdl1 << endl;
// rdl1= 1
const double &rdl= dl1 + dl2;
// toegelaten
cout<< “rdl= “ << dl1 + dl2<< endl; // rdl= 12.8 } 6) Een referentie wijzigen kan niet. /* programma P128.cpp */ #include void main() { int i1= 3, i2= 5; int &ri1= i1; //int &ri1= i2; //fout: ‘ri1’: redefinition; multiple initialization cout<<”ri1 = “ << ri1 << endl; } 7) Het is mogelijk een referentie (in het voorbeeld de referentie r1), die de formele parameter is van een functie (in het voorbeeld van de functie f1), opnieuw door te geven als actuele parameter (in het voorbeeld de actuele parameter r1 van de functie f2(r1) naar een functie waar de formele parameter opnieuw een referentie is (in het voorbeeld naar de functie f2(int &r2) waar de formele parameter r2 een referentie is). /* programma P129.cpp */ #include void f2(int &r2) { r2*=2; }
22
void f1(int &r1) { f2(r1); } void main() { int i= 7; f1(i); cout<<”i = “ << i << endl;
// i= 4
}
1.6.6 Referenties als terugkeerwaarde Naast het doorgeven van parameters naar een functie, kunnen referenties ook gebruikt worden voor het teruggeven van waarden door een functie. In het volgende programma geeft de functie num() een referentie terug aan EenNummer. We kunnen zeggen dat num() als een alias fungeert voor EenNummer. Met andere woorden, we krijgen hetzelfde resultaat als in main() de functie num() vervangen wordt door EenNummer. /* programma P130.cpp */ #include int EenNummer= 0; int &num() { return EenNummer; } void main() { int i; i= num(); num() = 5; } Functies die een referentie terug geven kunnen als lvalue gebruikt worden, zoals getoond in volgend programma. De functie min(i1, i2) wordt als lvalue gebruikt in min(i1, i2) = min(i1, i2) * 3. Concreet wordt de opdracht: i1= 10 * 3. /* programma P131.cpp */ #include int &min(int &ri1, int &ri2) { return(ri1 < ri2 ? ri1 : ri2);
// ri1 refereert naar i1, waardoor i1 blijft // bestaan als de functie is afgelopen
}
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
1.7 Initialisatie van variabelen Initialisatie is het initieel toekennen van een waarde van een variabele. De variabele krijgt een beginwaarde. Initialiseren gebeurt telkens bij het declareren van de variabele. De syntax voor de initialisatie van een variabele is gevoelig verbeterd in C++ tenopzichte van C. 1) Een eerste verbetering is de mogelijkheid om een variabele te intitialiseren met behulp van een volledige expressie in plaats van met een constante expressie, zoals getoond in volgende code: const double pi = acos(-1.0); const double e = exp(1.0); 2) Een andere verbetering t.o.v. C is de mogelijkheid om de initialisatie te schrijven met behulp van haakjes. Volgende C-code int i = 0; double x = 1.0; kan in C++ als volgt: int i(0); double x(1.0); Deze schrijfwijze is vooral nuttig wanneer men klassen wil initialiseren via hun constructor, zoals Complex c(1.0, 2.0); evenals: Complex c = Complex(1.0, 2.0); 3) Een volgende verbetering is de specificatie van een cast. Waar in C een cast wordt gespecificeerd door volgende code: int i; double d; d = (double) i; kan die in C++ vervangen worden door: int i; double d; d = double( i);
24
1.8
Overloaded functies
1.8.1 Overloading Een veel voorkomend geval is dat de programmeur functies schrijft die hetzelfde uitvoeren, maar die een verschillend aantal argumenten of een verschillend type argumenten aanvaarden. In C is het noodzakelijk om deze functies een verschillende naam toe te kennen. Zo zal volgende notatie noodzakelijk zijn: Complex_i(int x, int y); Complex_d(double x, double y); Complex_c(struct complex c); In C++ is het echter toegelaten hiervoor dezelfde functie-namen te gebruiken. Dit is overloading van functies. Voorwaarde is wel dat de functies onderscheiden kunnen worden door een verschillend aantal argumenten, of door een verschillend type van argumenten. Het vorig voorbeeld kan dus herschreven worden als: Complex(int x, int y); Complex (double x, double y); Complex (struct complex c); Bij een aanroep van Complex() zal de compiler die versie van Complex() oproepen die dezelfde types van parameters heeft als de aanroep. Complex(3, 4);
// roept Complex(int x, int y) op
Complex(4.2, 6.5);
// roept Complex(double x, double y) op
Wanneer er geen exacte overeenkomst van parameters is, dan gelden de standaard conversie-regels voor het vinden van een overeenstemmende routine. Soms levert dit dubbelzinnige situaties op, zoals in volgend voorbeeld: Complex((long) 3, (long) 4);
// fout
De compiler geeft de fout: ‘ambiguous call to overloaded function’, omdat zowel Complex(double x, double y) als Complex(int x, int y) kan worden gebruikt. Dit levert een ambigue (dubbelzinnige, tweeslachtige) situatie op. De compiler weet niet wat gedaan. In C++ zijn twee functies verschillend als hun namen verschillen of als hun aantal parameters verschillen, of als de types in een andere volgorde staan. Dit betekent dat de functie abx(int) en de functie abs(float) twee verschillende functies zijn. Het returntype is niet van belang om te bepalen of een functie-declaratie reeds voorkomt. Bijgevolg zijn ‘int random(void)’ en ‘float random(void)’ dezelfde functies en dit levert een compiler-fout op.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
25
1.8.2 Gebruik Overloading kan het programmeren aanzienlijk vereenvoudigen, zoals getoond in volgend programma. /* programma P132.cpp */ #include int square(int r) { return ( r >= -250 && r <= 250) ? r*r : -1; } long square(long r) { return ( r >= -40263 && r <= 40263) ? r*r : -1; } void main(void) { int i= 20; long lg= 1064; cout<<”i kwadraat = “<<square(i)<<endl; cout<<”lg kwadraat = “<<square(lg)<<endl; }
1.8.3
Functie aanroepen met argumenten van verschillend type
1.8.3.1 Voorbeeld met automatische convertie /* programma P132.cpp */ #include void demo(double); void main(void) { demo(10);
//automatische conversie van int naar double
demo(5.24); } void demo(double a) { cout<< a << “is een double.” << endl; } Resultaat:
10 is een double. 5.24 is een double.
26
1.8.3.2 Voorbeeld met verlies van data /* programma P133.cpp */ #include void demo(int); void main(void) { demo(10);
//warning:’argument’:conversion from ‘const double’ to ‘int’ //possible loss of data
demo(5.24); } void demo(int a) { cout<< a << “ is een int.” << endl; } Resultaat:
10 is een int. 5 is een int.
// verlies van data
1.9 Type-safe linken 1.9.1 Probleem Alhoewel men in C++ verplicht is om een prototype mee te geven bij het aanroepen van een functie, bestaan er toch situaties waarbij de compiler niet kan garanderen dat de correcte argumenten worden doorgegeven aan de aangeroepen functie. Veronderstel bijvoorbeeld dat een bibliotheekfunctie wordt aangeroepen en dat het prototype, meegegeven in het programma, andere types argumenten specificeert dan de bibliotheekfunctie verwacht. Doordat de compiler geen weet heeft van de juiste structuur van de bibliotheekfunctie, zal verkeerde code worden gegenereerd. Bovendien rijst het probleem van overloaded functies. Immers, overloaded functies hebben dezelfde functienaam, maar een verschillend type argumenten. De oplossing voor die problemen is het aanwenden van een ‘type-safe linken’ procedure.
1.9.2 Type-safe linken Type-safe linken is een systeem dat garandeert dat functies gelabeld worden met informatie betreffende de argument-types, zodat bij het linken de correcte functies worden aangeroepen. Concreet betekent dit dat de compiler de naam van de functie intern zal omvormen tot een nieuwe naam, die de informatie bevat betreffende het type argumenten. Bij het linken wordt dan die interne naam gebruikt om de functie in kweste te koppelen aan de functie die in de bibliotheek voorkomt. Wanneer dan de programmeur een fout maakt in het specificeren van het prototype, zal tijdens het linken een fout
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
27
worden gegenereerd, die zegt dat de bedoelde functie met dit type van argumenten niet kan teruggevonden worden in de bibliotheek. Type-safe linken garandeert echter niet dat de functie-terugkeertypes correct zijn, of dat extern gerefereerde variabelen van de correcte types zijn. Voorbeeld: #include<math.h> double afstand(double x1, double y1, double x2, double y2) { double d1 =x2 – x1; double d2 = y2 –y1; } In een andere file wordt de functie afstand() op de volgende manier opgeroepen: #include<stdio.h> void main() { extern int afstand(double, double, double, double); prinft(“%d\n”, afstand(1.0, 1.0, 2.0, 2.0); } De code in de main() zal nonsens teruggeven omdat het prototype voor afstand() liegt betreffende het returntype. De compiler kan de fout niet ontdekken omdat de functie extern gedeclareerd wordt. Ook zal de linker geen fout geven omdat het type-safe linken niet in staat is om het returntype te verifiëren. Het probleem ligt bij het extern declareren van functies en/of variabelen direct in functies. Externe declaraties horen thuis in header files en niet rechtstreeks in de source files.
1.10 Inline functies 1.10.1 Inline specifier en inline functie Een inline functie is een functie die begint met de ‘inline specifier’, zoals bijvoorbeeld: inline MAX(int getal1, int getal2). De inline specifier wordt ook inline specificator genoemd. Het sleutelwoord (E: keyword) ‘inline’ kenmerkt de functie voor inline expansie, nl. een inline functie wordt op de plaats van de aanroep door de compiler tot de functie-body geëxpandeerd.
1.10.2 Functie versus macro Een groot nadeel van een functie is dat ze traag werkt. Immers, bij gebruik van de functie MAX(int getal1, int getal2) moeten de twee argumenten worden gekopieerd, moeten machineregisters worden opgeslagen en moet het programma op een nieuwe locatie instructies selecteren. In plaats van de functie grootste = MAX(10, 50) op te roepen, is de handmatige code “grootste = (a > b) ? a : b;” gewoon sneller.
28
Een macro wordt wel in de code gevoegd. Een macro werkt dus als de handmatige code, maar bij gebruik van een macro worden de argumenten vervangen door hun waarde, en dit kan leiden tot fouten. Volgend programma bepaalt het grootste van twee getallen met behulp van een macro. Waar in C veel gebruik wordt gemaakt van macro’s om korte functies rechtstreeks als code in te voegen in de source file, dit om een expliciete aanroep van een functie te vermijden en dus de snelheid van uitvoering op te drijven, geeft dit soms aanleiding tot problemen, zoals in volgend voorbeeld. /* programma P134.cpp */ #include #define MAX(getal1, getal2)((getal1>getal2)?getal1:getal2)
//macro
void main() { int grootste; grootste = MAX(10, 50); cout<<”Grootste getal is “ << grootste << endl;
// 50
cout<<endl; int getal2 = 50; grootste = MAX(10, getal2++); cout<<”Grootste getal is “ << grootste << endl;
// 51
} In het tweede geval is de waarde van grootste niet juist (het moet 50 zijn). Dit komt omdat de code als volgt werd geëxpandeerd: grootste= ((10 > 51) ? 10 : 51) Om functies te expanderen tot gewone code in een programma en dit op een veilige manier, laat C++ toe ‘inline functies’ te gebruiken, zoals in volgend programma. /* programma P135.cpp */ #include inline MAX(int, int);
//prototype van de inline functie
void main() { int grootste; grootste = MAX(10, 50); cout<<”Grootste getal is “ << grootste << endl;
// 50
cout<<endl; int getal2 = 50; grootste = MAX(10, getal2++); cout<<”Grootste getal is “ << grootste << endl;
// 50
} inline MAX(int getal1, int getal2)
// inline functie
{ return (getal1 > getal2) ? getal1 : getal2; }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
29
Het stukje code ‘grootste = MAX(10, getal2++)’ wordt nu geëxpandeerd tot: { int temp1= 10; int temp2 = getal2++; grootste = temp1 > temp2 ? temp1 : temp2; } Bij int temp2 = getal2++; wordt eerst de inhoud van getal2 toegekend aan temp2 en pas daarna wordt de inhoud van getal2 met 1 verhoogd.
1.10.3 Vraag aan de compiler De inline specifier expandeert niet altijd een functie. De inline specifier fungeert alleen als een hint voor de compiler. De compiler kan kiezen om de functie niet te expanderen, afhankelijk van het nut, bv. wanneer de inline functie te veel regels code bevat zodat het nut ervan teniet gedaan wordt, of in bepaalde omstandigheden waarin de compiler niet kan expanderen. Een inline functie wordt niet geëxpandeerd als die recursief gedefinieerd is, of wanneer een pointer naar de functie genomen wordt. In het volgende voorbeeld zal de eerste aanroep van MAX() geëxpandeerd worden, de tweede niet. void main() { int (*p) (int a, int b); int x = MAX(3, 5);
//MAX wordt geëxpandeerd
p= MAX;
// p krijgt het adres van de functie MAX()
int y = p(3, 5);
//MAX() wordt niet geëxpandeerd omdat de //pointer p naar MAX() is opgenomen in het //programma
}
1.11
Default functie-argumenten
1.11.1 Default functie-argumenten ‘Default functie-argumenten’ betekent dat een functie een default waarde heeft voor argumenten, die niet expliciet geïnitialiseerd worden. Volgende functiedefinitie bevat een default waarde voor de argumenten x2 en y2, die niet expliciet in main() zijn geïnitialiseerd: double distance(double x1, double y1, double x2= 0, double y2= 0) De default argumenten moeten op het einde van de functiedefinitie worden geplaatst. C++ voorziet de mogelijkheid om bij de aanroep van een functie minder argumenten mee te geven dan het aantal parameters bedraagt. In dat geval moeten in de functiedefinitie de default waarden van die argumenten zijn
30
opgenomen. De default argumenten worden gebruikt bij een functieoproep als de daarop volgende argumenten ontbreken. Naast de directe specificatie van de default argumenten in de functiedefinitie is het ook mogelijk om de default argumenten te specificeren in het functieprototype, maar dan moet de functiedefinitie zonder default argumenten worden genoteerd.
1.11.2 Voorbeeld In volgend programma wordt de afstand berekend tussen twee punten. Indien de coördinaten van het tweede punt niet worden meegegeven naar de functie, wordt de afstand berekend t.o.v. het nulpunt. /* programma P136.cpp */ #include #include <math.h> double afstand(double x1, double y1, double x2= 0, double y2= 0) { double d1= x2 – x1; double d2 = y2 – y1; return sqrt(d1*d1 + d2*d2);
//sqrt()= square root (vierkantswortel) // en vereist math.h
cout<<”afstand2= “ << afstand2 << endl; //afstand2= 5 } Wanneer een waarde wordt meegegeven voor x2 en y2, dan worden die waarden gebruikt, anders worden x2 en y2 geïntialiseerd op 0. In het opgegeven programma moet minstens twee argumenten worden meegegeven aan de functie. De default waarden moeten altijd op het einde worden geplaatst.
1.11.3 Default argumenten en functie-overloading Volgend programma toont hoe bij de oproep van de functie lijn(), de default argumenten worden aangewend wanneer in de oproepende code de daaropvolgende argumenten ontbreken. Bij lijn() ontbreken de twee argumenten. De eerste default argumentwaarde is het karakter ‘-‘ voor het argument c, en de tweede default argumentwaarde is de waarde 80 voor het argument i. Bij lijn(‘=’) ontbreekt het tweede argument, dat de default waarde 80 krijgt. Merk op dat de argumentwaarde van de oproepende code de bovenhand haalt op de default argumentwaarde. Bij lijn(‘=’, 20) krijgen de twee argumenten een waarde van de oproepende code. T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
31
Bij lijn(30) treedt er een fout op omdat de doorgegeven argumentwaarde, nl. de getalwaarde 30, niet kan worden toegekend aan het argument char c.
/* programma P137.cpp */ #include void lijn(char c= ‘-‘, int i= 80) { for (int t= 1; t<=i; t++) cout<< c; cout<< endl; } void main() { lijn();
// afdruk van 80 keer het karakter ‘-‘
lijn(‘=’);
// afdruk van 80 keer het karakter ‘=‘
lijn(‘=’, 20);
// afdruk van 20 keer het karakter ‘=‘
lijn(30);
// levert foutief resultaat op
} Met functie-overloading van de functie lijn() met een functie die een integer aanvaardt als parameter, is het foutief resultaat van vorig programma ongedaan gemaakt, en worden alle combinaties mogelijk. /* programma P138.cpp */ #include void lijn(char c= ‘-‘, int i= 80) { for (int t= 1; t<=i; t++) cout<< c; cout<< endl; } void lijn(int i) { for (int t= 1; t<=i; t++) cout<< ‘#’; cout<< endl; } void main() { lijn();
// afdruk van 80 keer het karakter ‘-‘
lijn(‘=’);
// afdruk van 80 keer het karakter ‘=‘
lijn(‘=’, 20);
// afdruk van 20 keer het karakter ‘=‘
lijn(30);
// afdruk van 30 keer het karakter ‘#‘
}
32
1.11.4 Wat kan met functie-overloading en default argumenten? Stel de functie: lijn(char c= ‘-‘, int i= 80), dan: -> lijn(void)
geeft ambiguiteit bij de oproep lijn(), omdat de
eerste functie niet kan worden overloaded met een functie met als argument void. -> lijn(char c)
geeft ambiguiteit bij de oproep lijn(‘=’), omdat van
de eerste functie en van de tweede functie het argument zonder default waarde, nl. char c, van hetzelfde type zijn en in dezelfde volgorde staan. Daardoor kan de eerste functie niet worden overloaden met de functie lijn(char c). -> lijn(int a)
is OK.
-> lijn(int a= 70)
geeft abiguiteit bij de oproep lijn(), omdat het
eerste argument van de eerste en van de tweede functie niet overeenkomen. -> lijn(char c, long lg= 50000) geeft ambiguiteit bij de oproep lijn(‘=’), omdat het tweede argument van de tweede functie een default waarde heeft. -> lijn(char c, int i)
geeft ambiguiteit bij de oproep lijn(‘=’, 70), omdat
van beide functies de argumenten zonder default waarde van hetzelfde type zijn en in dezelfde volgorde staan. -> lijn (int i, char c)
is OK, omdat van beide functies de argumenten niet
in dezelfde volgorde staan. -> lijn(int i, int i)
is Ok, omdat de compiler geen conversie doet en
dus wel onderscheid kan maken. Wanneer de functies lijn(int, int) en lijn(long, long) worden opgeroepen met twee char’s, dan zal de functie lijn(int, int) worden uitgevoerd omdat er een automatische conversie gebeurt naar het grotere type. Gezien het default argument geen deel uitmaakt van de functie, heeft dit volgende consequentie: de defintie void (*p1)(char, int) = &lijn
// is OK
doch de definitie void (*p1)(char) = &lijn
// kan niet omdat het type niet overeen
stemt.
1.12 New en delete 1.12.1 Dynamische geheugenallocatie In C++ wordt uitgebreid gebruik gemaakt van dynamische geheugenallocatie, noodzakelijk bij het alloceren van objecten die een levensduur hebben die langer is dan de procedure waarin ze aangemaakt zijn. Om het alloceren van objecten (het dynamisch toekennen van geheugen voor objecten) gemakkelijker te maken, is in T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
33
C++ de operator new toegevoegd. Dit is de tegenhanger van malloc() in C. Let op: malloc() en new mogen niet samen gebruikt worden, anders leidt het mengen tot problemen. De operator new biedt het voordeel dat er geen probleem meer is met type-casting, noch met het bepalen van de grootte van de te alloceren ruimte. Daarbij is het mogelijk de nieuwe ruimte onmiddellijk te initialiseren. Enkele voorbeelden: int *p; p= new int;
// er wordt geheugen voorzien, maar niet geïnitialiseerd
p= new int(7); // er wordt geheugen voorzien en geïnitialiseerd met de waarde 7 p= new int [10];
// er wordt geheugen voorzien voor een array van 10 // integers zonder initialisatie
De constructie p= new int [10] (7); is niet toegestaan, omdat arrays van int, char, long, float en double (basistypes) niet kunnen worden geïnitialiseerd. De operator new laat toe om objecten of arrays van objecten te alloceren, zonder dat men expliciet de grootte moet doorgeven die het object inneemt in het geheugen. Bovendien wordt bij de new operator een ‘type check’ gedaan, zodat fouten zoals het alloceren van geheugen voor een int en de teruggekregen pointer toekennen aan een double, vermeden worden. De new operator is noodzakelijk bij het dynamisch alloceren van objecten, daar hij een correcte initialisatie van de objecten toelaat.
1.12.2
De-allocatie van geheugen C++ bezit de operator ‘delete’ om geheugen te de-alloceren. De delete operator is de tegenhanger van de operator free uit C. Echter, de free operator kan geen gealloceerde arrays de-alloceren, delete kan dat wel. Let op: gebruik delete en free niet samen! Mengen leidt tot problemen. Ook zal delete, net als free, niet verifiëren of de meegegeven pointer wel degelijk verwijst naar een gealloceerd object. Enkele voorbeelden: int *p_i = new int; // . . . . delete p_i; int *p_array_int = new int[10]; // . . . . delete [] p_array_int;
1.12.3 Dynamische array In het volgend programma wordt met de operator new een dynamische array van ‘aantal’ integer variabelen aangemaakt. De operator new vraagt aan het besturingssysteem een aaneengesloten geheugenblok voor ‘aantal’ integer variabelen. De new operator levert dan het beginadres op van die geheugenblok. Via dat adres krijgt het programma toegang tot dat geheugenblok.
34
/* programma P139.cpp */ #include void main() { cout<<”Aantal getallen: “; int aantal; cin>>aantal; int *p; p= new int[aantal];
//vraag een dynamische geheugenblok aan //en ken het beginadres toe aan de pointer p
int i, som= 0; for (i= 0; i < aantal; i++) { cout<< (i+1) << “e getal: “; cin>> p[i]; } for (i= 0; i < aantal; i++) som+= p[i]; cout<<”Het gemiddelde is “<<double(som)/aantal << endl; delete[] p;
//delete[] maakt het geheugen vrij dat wordt aangewezen // door p //Het is een goede gewoonte om dynamisch geheugen vrij //te geven als dat geheugen niet meer nodig is
}
1.12.4 Overloaden van de new operator De new operator kan opnieuw gedefinieerd worden door gebruik te maken van overloading. In het volgende voorbeeld maakt de nieuwe new operator gebruik van een statische buffer. /* programma P140.cpp */ #include #include <string.h> const int GROOTTE 1000; static char buffer[GROOTTE]; static char buffer_used = 0; void *operator new(size_t size) { cout<<”new opgeroepen”<<endl; if (size > GROOTTE || buffer_used) return NULL; else { buffer_used = 1; return buffer; } }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
35
void operator delete(void *ptr) { cout<<”delete opgeroepen”<<endl; buffer_used = 0; } void main() { char *tekst1 = new char[10]; cout<
1.12.5 Gebruik van de set_new_handler Bij gebruik van de new operator wordt telkens geverifieerd of de teruggegeven pointer niet NULL is. Daarenboven biedt C++ de mogelijkheid om gebruik te maken van de _set _new_handler functie, die de programmeur de mogelijkheid biedt om een foutafhandeling te voorzien die dienst kan doen voor elk geheugenreserveringsprobleem. Deze functie heeft als argument een functie-pointer naar de foutafhandelende functie. Telkens new de aangevraagde hoeveelheid geheugen niet kan toekennen, zal de foutafhandelende functie worden opgeroepen. Het volgend programma illustreert dit. /* programma P141.cpp */ #include #include <string.h> #include //De afhandel-routine moet als argument een size_t parameter hebben en als //return-type int. int afhandel_allocatie_probleem(size_t size) { cerr << “Het geheugenblok van “ << size << “ bytes kan niet gereserveerd worden” << endl; exit(1); return 0; } void main() { _set_new_handler(afhandel_allocatie_probleem); while (1) { char *alloc = new char[1000]; cout << “1000 bytes gereserveerd” << endl; } }
36
1.13 Namespaces 1.13.1 Het concept ‘namespaces’ van C++ Namespaces dienen om ervoor te zorgen dat globale variabelen en routines niet in conflict treden met andere variabelen en routines, die dezelfde naam hebben. Met het concept van ‘namespaces’ kan men definities en variabelen, die tot éénzelfde bibliotheek behoren, groeperen en isoleren van andere bibliotheken om aldus conflicten te vermijden tussen bijvoorbeeld twee routines met dezelfde naam in verschillende bibliotheken. In het volgend voorbeeld is een namespace A gedefinieerd die volgende elementen groepeert: -
de variabele j
-
de variabele str
-
het object EenObj
-
de klasse-definitie EenKlasse
namespace A { int j; char str[30]; class EenKlasse { …. }; class NogEenKlasse; NogEenKlasse EenObj; }
1.13.2 Gebruik van variabelen Er zijn twee mogelijkheden om variabelen te gebruiken, die behoren tot een namespace: 1) Men specificeert expliciet de namespace. Voorbeeld: A::j = 3; strcpy(A..str, “EenString”); 2) Men gebruikt het sleutelwoord ‘using namespace’. Voorbeeld: using namespace A; j = 3; strcpy(str, “EenString”);
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
37
1.14 Commentaar C++ voorziet in een nieuwe stijl van commentaar: hij begint na een dubbele slash en eindigt met enter. Voorbeelden: // Dit is commentaar in C++ int i = 32767; // hoogste waarde van een signed int De nieuwe commentaarstijl is niet bruikbaar om midden in een regel commentaar te schrijven. Volgende regel is fout: for (i= 0; i<32767 //hoogste waarde van een signed int//; i++); De commentaarstijl van C mag nog steeds gebruikt worden, zodat vorige regel dan wel kan: for (i= 0; i<32767 /*hoogste waarde van een signed int*/; i++);
1.15 Oefeningen 1) Overloading. Bereken de oppervlakte van een rechthoek en van een vierkant met behulp van éénzelfde functie oppervlakte(). 2) Referenties. Verwissel de waarde van twee getallen met behulp van de functie verwissel() en met gebruik van referenties. 3) Schrijf een programma dat het getal 65 decimaal, hexadecimaal, octaal, binair en als teken op het scherm zet. 4) Schrijf een programma dat de getallen 1000, 100, 10 en 1 mooi onder elkaar plaatst: eenheden onder elkaar, tientallen onder elkaar, enz…. 5) Default argumenten. Schrijf een programma dat verschillende versies van de functie lijn() bevat zodat onderstaande main() gebruikt kan worden. void main() { lijn(); lijn(33); lijn(33, ‘t’); lijn(‘=’); char dummy[2]; cin.getline(dummy, 2); }
38
2. Klassen en objecten 2.1 Creatie van een nieuw datatype in C++ 2.1.1 Het datatype klasse De taal C beschikt over de structuur (E: structure). Een structuur is de samenhangende verzameling (collectie) van variabelen, die van verschillend type kunnen zijn. Voorbeeld van struct: Stel dat we volgende gegevens van clubleden willen beschrijven: naam en volgnummer. Daartoe declareren we een struct Clublid met de leden (data) naam en volgnummer: struct Clublid { char naam[40]; int volgnummer; }; Een belangrijk nieuw aspect van C++ is dat een structuur, naast de data, nu ook functies, de zgn. lidfuncties, kan bevatten. Dit is het nieuwe datatype: de klasse (E: class). Een klasse is dus een samengesteld datatype zoals een struct, die echter naast de data-elementen, ook lidfuncties bevat. De private data-elementen van een klasse kunnen enkel gemanipuleerd worden door de lidfuncties van die klasse. Voorbeeld van klasse: class Clublid { private: char naam[40]; int volgnummer; public: Clublid(char *lidnaam, int nummer= 0) { strcpy(naam, lidnaam); volgnummer= nummer; } } In een klasse worden data en de toegestane bewerkingen op die data samengevoegd, hetgeen misverstanden en misbruik voorkomt. de verzameling data-elementen representeren de klasse de bewerkingen die toegepast kunnen worden op de data-elementen van die klasse, vormen de verzameling lidfuncties. De lidfuncties worden ook wel de interface van de klasse genoemd.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
39
2.1.2
Programma-voorbeeld 1 en bespreking. /* programma P142.cpp */ #include #include <string.h> class Artikel { char btwcode; //deze data-elementen zijn default private int prijs;
In bovenstaand programma is de klasse Artikel gedefinieerd met een aantal dataelementen en een aantal lidfuncties. Een lidfunctie onderscheidt zich van een dataelement door het gebruik van ronde haakjes. De klasse Artikel kan enkel aangewend worden via objecten van de klasse Artikel. In het programma is art een object van de klasse Artikel. De klasse Artikel is te beschouwen als een model, als een shabloon, terwijl het object art een instantie is van de klasse Artikel. Een zeer beknopte omschrijving van klasse en object is: een klasse is een specificatie voor objecten, terwijl een object een variabele is die gemaakt is volgens deze specificatie. De lidfunctie get_naam() is gedefinieerd in de klassedefinitie. Dergelijke lidfuncties worden automatisch behandeld als inline-functies. Doordat inline-functies alleen nuttig zijn als ze klein zijn, worden enkel kleine functies binnen de klassedefinitie geplaatst. De lidfuncties get_btwpercentage() en get_prijs() zijn gedefinieerd buiten de klassedefinitie. Zo’n functie moet echter wel binnen de klassedefinitie worden gedeclareerd. Zo weet de compiler dat de functiedefinitie nog komt. Een functiedefinitie buiten de klassedefinitie wordt gekenmerkt door vóór de functienaam de klassenaam en de bereikoperator :: op te nemen, bijvoorbeeld: int Artikel::get_prijs(). Deze notatie zorgt ervoor dat de functie get_prijs() wordt gekoppeld aan de klasse Artikel. De data-elementen btwcode, prijs en naam zijn private leden van de klasse Artikel. Dit betekent dat die data-elementen enkel toegankelijk zijn voor lidfuncties van de eigen klasse. Uiteraard moeten dan die lidfuncties public toegankelijk zijn om ze te kunnen gebruiken in main(). Immers, de publieke leden van een klasse vormen de interface tot die klasse, dit is de verbinding met die klasse. Om bijvoorbeeld in main() het data-element prijs een waarde toe te kennen, moet gebruik worden gemaakt van de lidfunctie get_prijs() en wel als volgt: pr = art.get_prijs(); met pr een gewone variabele (niet een privaat data-element van de klasse) en met art een object van de klasse waartoe prijs behoort. Volgende toekenning levert een fout op: pr= art.prijs; //error: ‘prijs’ cannot access ‘private’ member declared in // class ‘Artikel’ Ook kan een lidfunctie niet worden gebruikt zonder een object van zijn klasse. int pr; pr= get_prijs();
// error
Lidfuncties van een klasse onderscheiden zich van gewone functies door volgende kenmerken: 1) Lidfuncties hebben een volledige toegangsprivilege tot zowel de publieke als de private leden van de klasse waartoe ze behoren. Gewone functies hebben enkel toegang tot de publieke leden van een klasse. Uiteraard hebben de lidfuncties van de ene klasse geen toegang tot de leden van een andere klasse. 2) Lidfuncties worden gedefinieerd binnen het bereik van de klasse waartoe ze behoren. Gewone functies worden gedefinieerd binnen het bestandsbereik. Dit betekent dat namen van lidfuncties niet zichtbaar zijn buiten het bereik van de klasse. T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
41
2.1.3 Programma-voorbeeld 2 en bespreking Volgend programma is bedoeld om datums (dag, maand en jaar) te manipuleren. Het programma moet beletten dat een foutieve datum (bv. 51/14/200) kan worden toegekend. Verder moet het mogelijk zijn om de datum-structuur te kunnen wijzigen, bv. short int voor dag i.p.v. int, zonder ons te moeten bekommeren welke functies er mee te maken hebben, die uiteraard ook moeten worden aangepast. De oplossing is functies te definiëren die de variabelen van de datum aanspreken en respectievelijk dag, maand en jaar teruggeven en/of wijzigen, en waarbij we de conventie aannemen dat enkel deze functies mogen gebruikt worden om toegang te hebben tot de verschillende onderdelen van de datum. We definiëren met andere woorden een set van functies die de toegang regelen tot deze variabelen. Daardoor kunnen we checken of de elementen van de datum wel degelijk correct toegekend worden, en bovendien zal bij een verandering van de structuur alleen deze functies dienen te worden veranderd. Dit betekent het gebruik van een klasse. /* programma P143.cpp */ #include class Datum { public: Datum(int dag, int maand, int jaar); void ToonDatum; private: int dag, maand, jaar; }; inline int max(int a, int b) { if (a > b) return a; return b; } inline int min(int a, int b) { if (a < b) return a; return b; } Datum::Datum(int eendag, int eenmaand, int eenjaar) { static int lengte[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; maand= max(1, eenmaand); maand= min(maand, 12); dag= max(1, eendag); dag= min(dag, lengte[maand-1]); jaar= max(1, eenjaar); }
Geboortedatum.ToonDatum(); Verkeerdedatum.ToonDatum(); } Dit programma geeft als resultaat: 21 juli 1966 31 december 1966 Bespreking: Altijd moet gestart worden met de definitie van de klasse (hier Datum). Deze definitie bevat zowel de data-elementen als de lidfuncties die deze klasse uitmaken. In main() worden de objecten gedefinieerd die tot de klasse Datum behoren. Een object is een instantie van een klasse, dit wil zeggen, het is een aanwijsbaar ‘voorwerp’ van een abstracte klasse. In bovenstaand programma is een ‘geboortedatum’ inderdaad een specifieke datum behorend tot de abstracte klasse Datum. Een klasse is een abstract datatype, net zoals een struct. Een object wordt geconstrueerd door middel van een constructor, hier de constructor Geboortedatum(). Deze constructor is een functie die altijd dient opgegeven te worden. Het doel van een constructor is de initialisatie te doen van de data-elementen behorend tot het object. Het is de taak van de constructor om de data-elementen te initialiseren met geldige waarden. Zelfs wanneer we een object willen creëren waarbij we ongeldige variabelen doorgeven, dan moet de constructor ervoor zorgen dat deze toch een geldige waarde krijgen, bv. het object Verkeerdedatum. We spreken niet rechtstreeks de data-elementen van het object aan, maar doen dit onrechtstreeks bij de initialisatie van het object via het oproepen van de constructor. Het rechtstreeks aanspreken van het element wordt vermeden door ze als private te declareren. We zeggen dat de inwendige structuur van het object verborgen blijft voor de gebruiker van het object. De data-elementen van een object worden rechtstreeks aangesproken door de lidfuncties. Het is niet nodig om expliciet de naam van het object mee te geven zoals met structuren wel het geval is. De lidfuncties weten op welk object moet ingewerkt worden. Het aanroepen van de leden van een klasse gebeurt door middel van de ‘.’ operator, net zoals bij structuren. Merk op dat ook de lidfuncties op dezelfde T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
43
manier worden aangesproken. Dit drukt uit dat deze functies inderdaad behoren tot het object, en niet zomaar los staan van het object. Functies die inwerken op de leden van een struct, staan wel los van die struct. De data-elementen van een klasse worden op dezelfde wijze gedeclareerd als gewone variabelen. Echter is de expliciete initialisatie van data-elementen niet toegestaan. In het programma-voorbeeld is volgende expliciete initialisatie van het dataelement dag niet toegelaten: int dag= 12; Een klasse-object kan enkel als data-element worden gedeclareerd als de klassedefinitie ervan reeds is geconstateerd. In die gevallen waarvoor de klassedefinitie nog niet is geconstateerd, kan een vóórdeclaratie van de klasse worden gegeven. Voorbeeld: class Geboortedatum;
//vóórdeclaratie
class Clublid { Geboortedatum *p;
//pointer p naar klasse Geboortedatum
}; Dankzij de vóórdeclaratie kan p gedeclareerd worden.
2.2 Definitie van een klasse en OOP 2.2.1 Definitie van een klasse Een klasse is per definitie een door de programmeur gedefinieerd datatype dat zowel data-elementen als procedurele-elementen (lidfuncties) bevat. In het geheugen is een klasse opgebouwd uit de data-elementen die deze klasse bevat. De functies die tesamen met de klassedefinitie worden opgegeven zijn gewone aan te roepen functies die, weliswaar verborgen, een bijkomende parameter bevatten, nl. de pointer ‘this’ naar het eerste data-element van deze klasse. Volgend schema verduidelijkt dit: Geheugen
Data_1 Proc_1(this, …) Data_2
Proc_2(this, …)
{
{
…
…
}
}
Data_n
Bovenop dit wat simplistisch model biedt de compiler de programmeur bijkomende functionaliteit aan, die toelaten alle aspecten van een klasse te benutten: Informatieverberging (E: data-hiding), die toelaat bepaalde elementen te beschermen tegen ongeoorloofd gebruik. De data die een object nodig heeft, zit opgeslagen in het object zelf (E: encapsulation). Die data zullen meestal niet
44
zichtbaar zijn voor de gebruiker. Die data zal bewerkt worden door lidfuncties, die ook deel uitmaken van het object. De ontwerper van de klasse bepaalt wat voor lidfuncties de gebruiker(programmeur) kan aanwenden en welke niet. De ontwerper bepaalt ook wat voor date de gebruiker rechtstreeks zal kunnen aanwenden en welke niet. Constructors die toelaten objecten te initialiseren. Overerving (E: inheritance), die toelaat eigenschappen over te dragen van een basisklasse naar een afstammeling ervan. Van bestaande klassen kunnen nieuwe klassen worden afgeleid. Deze nieuwe klassen erven de data en lidfuncties van de generieke klasse, ook superklasse genoemd. In de subklasse, ook specifieke klasse genoemd, kunnen we functies herdefiniëren of weglaten. Ook kunnen data-elementen worden toegevoegd aan een subklasse. Polymorfisme (E: polymorphism) of veelvormigheid is het principe waarbij een afgeleid object kan worden aangesproken alsof het een basisobject is. Polymorfisme laat toe om een procedure, lid van een bepaald object, iets anders te laten uitvoeren naargelang het type van het object. Met behulp van de techniek van polymorfisme kan een verzameling van gelijksoortige objecten worden aangelegd, en kan op deze verzameling dan een gemeenschappelijke functie worden uitgevoerd. Elk van de elementen van die verzameling zal dan reageren zoals voor dat type object voorzien is.
2.2.2 OOP 2.2.2.1 OOP versus procedureel programmeren OOP staat voor Object Oriented Progamming, in het Nederlands: object georiënteerd programmeren. OOP maakt gebruik van klassen en objecten. Het verschil tussen OOP en procedureel programmeren (zoals in C) is veel groter dan enkel klassen en objecten, het is een manier van aanpak. Bij procedureel programmeren vertrek je vanuit de bewerkingen die op data moeten worden uitgevoerd. Je giet die bewerkingen in functies op basis van het top-down mechanisme. Bij OOP vertrek je vanuit de objecten en ga je na wat voor data die objecten gebruiken en wat voor bewerkingen van toepassing zijn op dat object.
2.2.2.2 De principes van OOP 1) Abstractie. Abstractie houdt in dat de gebruiker kan werken met objecten zonder zich te moeten bekommeren om de implementatiedetails van het object. Een object is een instantie van een klasse. De implementatiedetails van een klasse zijn voor rekening van de ontwerper van die klasse, niet voor rekening van de gebruiker van die klasse. De programmeur die een klasse ontwerpt en objecten van die klasse gebruikt in een programma, is terzelfder tijd ontwerper en gebruiker. 2) Data-verberging en inkapseling. 3) Overerving. 4) Polymorfisme. Meer uitleg staat in deel 8.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
45
2.3 Opbouw van een klasse Bij de definitie van een klasse worden de lidfuncties en lidvariabelen (dataelementen) gespecificeerd voor deze klasse. Eens gedefinieerd, wordt een klasse behandeld alsof het een nieuw type is. Beschouwen we volgend voorbeeld: class auto { public: int snelheid;
// lidvariabele
int verbruik;
// lidvariabele
int leeftijd;
// lidvariabele
void starten();
// lidfunctie
void rijden();
// lidfunctie
void remmen();
// lidfunctie
int BerekenVerbruik(); // lidfunctie }; Het creëren van een object behorend tot de klasse auto gebeurt zoals voor een ander datatype. Enkele voorbeelden: auto Volkswagen; auto Oldtimers[10[; auto *AutoPtr = &Volkswagen; Het public gedeelte in de klassedefinitie specificeert dat de lidvariabelen en lidfuncties toegankelijk zijn voor de gebruikers van de klasse. Volgende assignatie van publieke lidvariabelen is toegelaten: Volkswagen.snelheid = 70; Volkswagen.leeftijd = 5; Oldtimer[1].leeftijd = 40; AutoPtr -> verbruik = 12; Publieke lidfuncties kunnen met dezelfde schrijfwijze als de lidvariabelen worden aangeroepen, zoals volgende voorbeelden tonen: AutoPtr -> remmen(); Oldtimer[3].starten(); Volkswagen.rijden(); Vóór de definitie van een lidfunctie moet de klasse worden meegegeven. Zo wordt aangegeven dat deze functie-definitie behoort tot de aangeduide klasse. Beschouwen we volgend voorbeeld: void Auto::starten(void) { Draai_contactsleutel_om(); Zet_in_eerste_versnelling(); Geef_gas(); } Het paar ‘dubbele punten’ (::) wordt de bereikoperator (scope resolution operator) genoemd. Deze operator laat immers toe het bereik (namelijk dat van de klasse)
46
van de functie aan te duiden. Indien we dus Auto:: laten voorafgaan aan de functiedefinitie, dan indiceren we dat de functie lid is van de klasse Auto. Merk op dat we onmiddellijk gebruik kunnen maken van de gedefinieerde variabelen in de definitie van lidfuncties. We kunnen dus het volgende schrijven: int Auto::BerekenVerbruik() { if (snelheid > 90) verbruik = 10; else verbruik = 8; } Het is dus niet noodzakelijk om de operator -> of . te gebruiken, zoals dit noodzakelijk was bij het gebruiken van een structuur. Binnen een definitie van een lidfunctie strekt de zichtbaarheid van de variabelen uit over alle leden van de klasse waartoe de lidfunctie behoort. Met andere woorden, het aanroepen van de functie BerekenVerbruik() van een object zal automatisch de juiste data-elementen hanteren. Bijvoorbeeld: Auto Mercedes, Ford;
//objecten Mercedes en Ford van de klasse Auto
1: Mercedes.BerekenVerbruik(); 2: Ford.BerekenVerbruik(); De functie BerekenVerbruik() op lijn 1 zal inwerken op de data-elementen snelheid en verbruik van het object Mercedes, en op lijn 2 op de data-elementen van het object Ford.
2.4 Toegangscontrole tot klasseleden Om de toegang tot leden van een klasse te regelen heeft de programmeur de beschikking over drie mgelijkheden: 1) Een ‘private’ lid is enkel toegankelijk voor lidfuncties van dezelfde klasse als van het private lid en voor friends van die klasse. Dit is de meest strikte toegangsvorm. Zelfs via een object van die klasse is een privaat lid niet toegankelijk. 2) Een ‘protected’ lid is enkel toegankelijk voor lidfuncties van dezelfde klasse als van het protected lid, voor friends van de klasse en ook voor lidfuncties van publiek afgeleide klassen. Zelfs via een object van die klasse is een beschermd lid niet toegankelijk. 3) Een ‘public’ lid is toegankelijk voor iedereen. De publieke leden vormen als het ware de interface tot deze klasse. Met behulp van de publieke leden communiceren we met het ‘inwendige’ van het object. Het inwendige van het object wordt op zijn beurt gevormd door de private leden van het object. Deze private leden zijn niet zichtbaar van buiten uit. Ze zijn enkel toegankelijk via de publieke leden van het object. Een beschermd lid vormt de middenweg tussen een privaat lid en een publiek lid. Een beschermd lid is niet toegankelijk, maar kunnen in afgeleide klassen wel aangesproken worden. T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
47
Private data-elementen worden gewoonlijk voorbehouden voor data die moet beschermd worden tegen foutieve initialisaties, bv. voor data die alleen een bepaalde set van waarden kan aannemen. De toegang tot zo’n data-element wordt gewoonlijk verzorgd door een publiek lidfunctie, die de initialisatie van het dataelement controleert. Ook elementen die intern geïnitialiseerd worden, bv. met behulp van de constructor, worden privé gehouden. Herschrijven we vorige klasse auto, gebruik makend van de toegangscontrole: class auto { private: int verbruik;
// niet zelf te bepalen
int leeftijd;
// dus private
int snelheid;
// wel zelf te bepalen
public: int kleur; auto(); void starten();
// functies die extern
void rijden();
// aangeroepen worden
void remmen(); int BerekenVerbruik; ~auto();
//~ = alt 126, staat voor destructor
inline void ZetVerbruik(int EenVerbruik); inline void ZetLeeftijd(int EenLeeftijd); inline int GeefVerbruik(); inline int GeefLeeftijd(); }; inline void auto::ZetVerbruik(int EenVerbruik) { if (EenVerbruik < 1) verbruik= 1; else if (EenVerbruik > 30) verbruik= 30; else verbruik = EenVerbruik; } inline void auto::ZetLeeftijd(int EenLeeftijd) { if (EenLeeftijd < 0) leeftijd= 0; else if (EenLeeftijd > 30) leeftijd= 30; else leeftijd = EenLeeftijd; } inline int auto::GeefVerbruik() { return verbruik; } inline int auto::GeefLeeftijd() { return leeftijd; }
48
We kunnen dan het volgende schrijven: void MijnAuto(void) { auto Ford;
//creatie van object Ford
Ford.kleur = grijs;
//OK: kleur is public
Ford.snelheid = 60; Ford.leeftijd = 100;
//fout: leeftijd is private
Ford.ZetLeeftijd(100);
//OK: toegang via lidfunctie
int EenLeeftijd = Ford.GeefLeeftijd(); Ford.ZetVerbruik(10); } In bovenstaand voorbeeld is gebruik gemaakt van de functies ZetLeeftijd() en ZetVerbruik() om de private data-elementen een waarde toe te kennen. Deze functies bieden het voordeel dat ze expliciet de doorgegeven parameter verifiëren op hun correctheid en eventueel correctieve acties uitvoeren. Voor het aanspreken van de private data-elementen wordt gebruik bemaakt van de functies GeefLeeftijd() en GeefVerbruik().
2.5 Constructoren en destructor 2.5.1 Constructor Een constructor is een speciale lidfunctie die -
dezelfde naam heeft als de klasse waar hij lid van is
-
automatisch wordt uitgevoerd zodra een nieuw object van die klasse gemaakt wordt
-
zorgt voor de initialisatie van een object
-
geen return-type heeft, zelfs geen void, m.a.w. een constructor heeft geen return value (terugkeerwaarde)
Er mogen meerdere constructoren in een programma voorkomen. De constructor is verantwoordelijk voor het creëren van het object. Met andere woorden, telkens een constructor wordt opgeroepen zal er ruimte in het geheugen worden voorzien voor het object. Dit betekent ook dat het verboden is om rechtstreeks vanuit een lidfunctie een constructor op te roepen. Meestal is de belangrijkste, en enige taak van een constructor het geven van een beginwaarde aan de data-leden(initialiseren). Omdat constructoren vrij eenvoudige taken uitvoeren, worden ze dikwijls inline gedefinieerd. Voorbeeld: class Datum { private: int dag; int maand; int jaar;
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
49
public: //constructor Datum() { dag= 1; maand= 1; jaar= 2002; } // … overige lidfuncties }; De constructor Datum() van de klasse Datum stelt de datum in op 1 januari 2002. Bij elk object van de klasse Datum, dat wordt gedeclareerd, wordt de constructor Datum() aangeroepen. Dus, na de declaratie Datum d1, d2; Zijn zowel d1 als d2 automatisch geïnitialiseerd met 1 januari 2002. Programmavoorbeeld: De klasse Telgids laat toe om gegevens van een telefoongids te combineren. /* programma P144.cpp */ #include #include <string.h> class Telgids { public: Telgids(const char *na, int num)
// constructor
{ naam = new char[strlen(na)+1]; strcpy(naam, na); nummer= num; } void print() { cout<
// impliciete constructoraanroep
50
De private leden naam en nummer kunnen enkel worden aangewend door de lidfuncties van de klasse Telgids, nl. door de constructor Telgids en de lidfunctie print(). Omdat de lidfuncties publiek zijn (gelukkig), kunnen ze worden aangeroepen buiten de klasse Telgids. In het voorbeeld is dit in main(). In main() wordt het object t1 gecreërd van het type klasse Telgids. De constructor Telgids() zorgt ervoor dat het object t1 geïnitialiseerd wordt met de opgegeven string “Jansens” en de integer 3456. Merk op dat volgende aanroepen van de constructor Telgids() fout zijn, omdat de constructor Telgids() twee argumenten verwacht: Telgids t1;
// fout
Telgids t1(3456);
//fout
Via t1 kan de publieke lidfunctie print() van de klasse Telgids() worden aangeroepen. Dit houdt in dat de afdruk van de naam en het nummer vast ligt en volledig bepaald is door de lidfunctie print().
2.5.2 Default constructor De constructor Datum() in voorbeeld van 2.5.1 heeft geen argumenten. Een constructor zonder argumenten heeft een speciale naam: de default constructor. De default constructor wordt aangeroepen bij het creëren van een object, als bij de creatie van dat object geen initiële waarden zijn opgegeven. Dit geldt ook bij het aanmaken (alloceren) van een array van objecten, waarbij voor elk array-element de default constructor wordt aangeroepen. Wanneer een klasse geen enkele constructor heeft, zal de compiler zelf een default constructor aanmaken. Zo’n default constructor, gegenereerd door de compiler, doet niets anders dan een object construeren. /* programma P145.cpp */ #include #include <string.h> class Telgids { public: Telgids()
// default constructor
{ naam = new char[8]; strcpy(naam, “niemand”); nummer= -1; } void print() { cout<
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
Telgids afdelingen[3]; // 3 x de default constructor kantoor.print(); for (int i= 0; i < 3; i++) afdelingen[i].print(); } Het programma geeft volgend resultaat: niemand
-1
niemand
-1
niemand
-1
niemand
-1
2.5.3 Constructor met argumenten Een constructor kan argumenten hebben, zodat bij de declaratie van een object dat object meteen kan geïnitialiseerd worden met vrij gekozen waarden. Voorbeeld: Volgende klasse heeft een constructor met drie argumenten. class Datum { private: int dag; int maand; int jaar; public: //constructor met drie argumenten Datum( int d, int m, int j) { dag= d; maand= m; jaar= j; } // … overige lidfuncties }; Een voorbeeld van mogelijke declaratie: Datum d1(10, 12, 2002); Deze declaratie kan als volgt worden gelezen: Declareer een object d1 van de klasse Datum, en geef d1 de initiële waarden 10, 12 en 2002 met behulp van de constructor van deze klasse.
52
Vervelend is nu wel dat het niet meer mogelijk is een object te declareren zonder initiële waarden. Een declaratie als Datum d2; levert een foutmelding op omdat de constructor drie argumenten heeft en daarom drie initiële waarden verwacht. Er zijn twee oplossingen voor dit probleem: 1) Definitie van een tweede constructor (constructor overloading). 2) De drie argumenten van de constructor een default waarde geven.
2.5.4 Constructor overloading Als een klasse meer dan één constructor heeft, is er sprake van constructor overloading. Constructor overloading wordt veel toegepast omdat het flexibele declaraties van objecten mogelijk maakt, bijvoorbeeld declaraties zonder en met initiële waarden. Voorbeeld: class Datum { private: int dag; int maand; int jaar; public: //constructor zonder argumenten (default constructor): Datum() { dag= 1; maand= 1; jaar= 2002; } //overloaded constructor met drie argumenten: Datum( int d, int m, int j) { dag= d; maand= m; jaar= j; } // … overige lidfuncties }; Het is nu mogelijk objecten te declareren met en zonder initiële waarden: Datum d1;
// aanroep constructor zonder argumenten
Datum d2(12, 10, 2000);
//aanroep constructor met 3 argumenten
Voor constructor overloading gelden dezelfde regels als voor het overloaden van functies, nl. de compiler moet op grond van het aantal argumenten en hun type (de zgn signatuur van de functie) eenduidig kunnen beslissen voor één van de constructoren. Als er geen eenduidigheid is, dan heet dat ambiguïteit.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
53
Volgend voorbeeld is ambigu. class Datum
// dit voorbeeld is niet correct
{ private: int dag; int maand; int jaar; public: Datum(int d) { dag= d; } Datum(int m) { maand= m; } // … overige lidfuncties }; Datum d(5);
// compiler weet niet welke constructor te gebruiken.
Omdat beide constructoren één argument hebben die een int-waarde accepteren, kan de compiler niet beslissen welke van de twee constructoren hij zal aanvragen. Dit leidt tot een foutmelding.
2.5.5 Constructor met default argumenten Net zoals bij functies, kan men een constructor default argumenten geven. Die default argumenten worden dan gebruikt als bij de declaratie van een object één of meer initiële waarden ontbreken. Een constructor met default argumenten is ook een default constructor. Voor de klasse Datum zou een constructor met default argumenten bijvoorbeeld kunnen zijn: Datum(int d= 1, int m= 1, int j= 2002) { dag= d; maand= m; jaar= j; } Volgende declaraties zijn dan geldige declaraties: Datum d1;
// 1 januari 2002
Datum d2(5);
// 5 januari 2002
Datum d2(5, 10);
// 5 oktober 2002
Datum d2(5, 10, 2001)
// 5 oktober 2001
54
2.5.6 Constructor overloading en default argumenten 2.5.6.1 Probleem en oplossing Als tegelijk constructor overloading en default argumenten worden toegepast, ontstaat er vlug ambiguïteit. Volgend voorbeeld toont dit aan. // constructor zonder argumenten, dus een default constructor Datum() { dag= 1; maand= 1; jaar= 2000; } // constructor met drie default argumenten, ook een default constructor Datum(int d= 1; int m= 1, int j= 2000) { dag= d; maand= m; jaar= j; } Alhoewel de constructor met drie argumenten er anders uitziet dan de constructor zonder argumenten, treedt toch ambiguïteit op, omdat bij een declaratie zonder initiële waarden, zoals bijvoorbeeld Datum d6;, de compiler niet kan beslissen welke constructor te nemen. Immers, er zijn twee default constructoren, nl. twee constructoren die zonder argumenten kunnen worden gebruikt, en dit leidt tot ambiguïteit. Een oplossing voor het probleem van ambiguïteit is één constructor weglaten. Een andere oplossing is het aantal default argumenten in de tweede constructor te verminderen, als volgt: //constructor met drie argumenten, waarvan één default: Datum(int d, int m, int j= 2000) { dag= d; maand= m; jaar= j; } Om deze constructor te kunnen gebruiken, moeten er in elk geval twee initiële waarden worden opgegeven, nl. één voor de dag en één voor de maand.
2.5.6.2 Programma voorbeeld /* programma P146.cpp */ #include #include class Datum { private: int dag; int maand; int jaar; public: T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
55
// default constructor: Datum(int d= 1; int m= 1, int j= 2000) { dag= d; maand= m; jaar= j; } // overige lidfuncties: void verander(int d, int m, int j) { dag= d; maand= m; jaar= j; } void print(); }; void Datum::print() { cout << setfill(‘0’) <<setw(2) << dag << ‘-‘ << setw(2) << maand << ‘-‘ << setw(4) << jaar << endl; } void main() { // declaratie en initialisatie met 1 januari 2000: Datum d1; d1.print(); d1.verander(12, 8, 2001); d1.print(); // declaratie en initialisatie met 4 juni 1980: Datum d2(4, 6, 1980); d2.print(); } De uitvoer is: 01-01-2000 12-08-2001 04-06-1980
2.5.7 Destructor De destructor zorgt ervoor dat de nodige opkuis gebeurt telken een object wordt vernietigd. Wanneer een object wordt vernietigd, dan wordt de destructor automatisch opgeroepen en zorgt voor de opkuis, nl. het de-alloceren van het toegewezen geheugen. Een globaal gedefinieerd object wordt vernietigd als het programma wordt verlaten. Dynamisch gealloceerde objecten worden vernietigd met delete.
56
Objecten die binnen een functie zijn geïnstalleerd worden vernietigd als die functie wordt verlaten. De naam van een destructor is gelijk aan de naam van zijn klasse, echter wel voorafgegaan door ~ (tilde, Alt 126). Een destructor heeft geen terugkeertype en geen argumenten en kan dus niet worden overloaden. In tegenstelling tot constructoren, kan slechts één destructor worden gedefinieerd voor een klasse. In volgend programma wordt naargelang het aantal argumenten een andere constructor aangeroepen. Bij het verlaten van main(), dus bij exit programma, wordt de destructor drie keer aangeroepen in omgekeerde volgorde van de aanroepen van de constructoren. /* programma P147.cpp */ #include #include <string.h> class Telgids { private: char *naam; int nummer; int zone; public: Telgids(const char *na, int num)
// constructor1
{ cout<<”constructor1 aangeroepen”<<endl; naam = new char[strlen(na)+1]; strcpy(naam, na); nummer= num; zone= -1;
// ongeldige zone
} Telgids();
// declaratie van constructor2
Telgids(const char *na, int num, int znr)
// constructor3
{ cout<<”constructor3 aangeroepen”<<endl; naam = new char[strlen(na)+1]; strcpy(naam, na); nummer= num; zone= znr; } void print() { cout<<”naam
= “<
cout<<”nummer
= “<
cout<<”zone
= “<
} }; T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
2.6 Object als argument van een lidfunctie Wanneer een lidfunctie de gegevens van meerdere objecten nodig heeft, moet die lidfunctie de bedoelde objecten als argument hebben. Volgend programma maakt de som van de inhoud van twee spaarbussen. In elke spaarbus steken stukken van éénEuro en van tweeEuro. De som wordt gemaakt in de lidfunctie som() en moet worden toegekend aan het object totaal. De lidfunctie som() wordt als volgt opgeroepen: totaal.som(bus1, bus2); Dit betekent: stuur naar het object totaal het bericht dat hij de som moet maken van bus1 en bus2. Het resultaat wordt opgeslagen in totaal. De lidfunctie som() moet dus twee argumenten hebben, nl. twee objecten van het type Spaarbus: bus1 en bus2. Programma: /* programma P148.cpp */ #include class Spaarbus { private: int éénEuro, tweeEuro, totaal;
58
public: // constructor met default argumenten: Spaarbus(int stuk1= 0, int stuk2= 0) { éénEuro= stuk1; tweeEuro= stuk2; bereken(); } // overige lidfuncties: void bereken(); void print(); void som(Spaarbus, Spaarbus); }; void Spaarbus::bereken() { totaal= éénEuro * 1 + tweeEuro * 2; } void Spaarbus::print() { cout<<”totaal aantal stukken van éénEuro: “<<éénEuro<<endl; cout<<”totaal aantal stukken van tweeEuro: “<
59
De objecten b1 en b2 zijn value-argumenten en bevatten dus kopieën van bus1, respectievelijk bus2. De constructor van de klasse Spaarbus roept de lidfunctie bereken() aan, zonder dat er een object wordt opgegeven. Van welk object wordt de inhoud berekend? Van het object dat op dat moment door de constructor gemaakt wordt, dus op basis van de declaratie Spaarbus bus1(15, 10) wordt vanuit de constructor de lidfunctie bereken() toegepast op het object bus1.
2.7 Constante objecten en constante lidfuncties 2.7.1
Constante objecten Bij het declareren van een object kan men gebruik maken van het sleutelwoord const, net zoals dat mogelijk is bij het declareren van een gewone variabele. Een declaratie van een object met const betekent dat het object constant is en dus niet kan gewijzigd worden. Bijvoorbeeld: const Datum geboortedatum(24, 5, 1980); In het voorbeeld is geboortedatum een constant object dat niet kan gewijzigd worden.
2.7.2 Constante lidfuncties Doordat constante objecten niet gewijzigd kunnen worden, verbiedt de compiler om een lidfunctie van dat object aan te roepen, omdat die lidfunctie eventueel de dataelementen van het object zou kunnen wijzigen. Echter zijn soms lidfuncties vereist die enkel de waarde van een data-element moeten teruggeven en die geen data-elementen wijzigen. Een dergelijke functie wordt gedeclareerd als const. Voorbeeld: class Datum { public: Datum(int Eendag, int Eenmaand, int Eenjaar); //constructor int GeefDag() const;
In het beschouwde voorbeeld staat na elke functie die de inhoud van het object datum leest, het sleutelwoord const. Op die manier is het toch mogelijk die lidfuncties aan te roepen, zoals in volgend voorbeeld: int i; const datum Geboortedatum(21, 7, 1970); i= Geboortedatum.GeefDag(); Geboortedatum.ZetJaar(1985);
//fout: ZetJaar niet constant
2.8 Objecten als functiewaarde In het programma P148.cpp wordt de som van de inhoud van de twee spaarbussen gemaakt door de objecten bus1 en bus2 te gebruiken als argumenten van de lidfunctie som(), nl. totaal.som(bus1, bus2). Die som kan ook worden berekend door een lidfunctie, die een object aflevert met als inhoud de som. De oproep van die functie kan als volgt: totaal = bus1.som(bus2); Betekenis: stuur aan het object bus1 het bericht om de som te berekenen van zijn eigen inhoud en van de inhoud van bus2, en lever het resultaat af als functiewaarde. De lidfunctie som() heeft één argument (bus2) van het type Spaarbus, en levert een object af van datzelfde type. De declaratie van som() is dus: Spaarbus som(Spaarbus b); Echter, opdat som() een object zou kunnen afleveren, moet dat object eerst gemaakt worden. Daartoe is in de lidfunctie som() de declaratie van dat object vereist. Dat object heeft als taak de uitgerekende waarden in zich op te bergen en vervolgens af te leveren. Zo’n tijdelijk object (tempory object) wordt veelal temp genoemd, doch de aanduiding met ‘hulp’ is ook geschikt. De definitie van de lidfunctie som() wordt dus: Spaarbus Spaarbus::som(Spaarbus b) { Spaarbus temp; temp.éénEuro= éénEuro + b.éénEuro; temp.tweeEuro= tweeEuro + b.tweeEuro; temp.bereken(); return temp; } Spaarbus Spaarbus::som(Spaarbus b) De eerste Spaarbus geeft het type aan van de functiewaarde. De tweede Spaarbus geeft aan bij welke klasse de functie hoort. Bij aanroep van de functie som() met totaal= bus1.som(bus2) wordt de inhoud van bus2 gekopieerd naar het value-argument b. De regel temp.éénEuro= éénEuro + b.éénEuro;
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
61
moet als volgt worden gelezen: tel het aantal stukken van éénEuro van bus1 op bij die van bus2 en berg de som op in temp.éénEuro. Het volledige programma is als volgt: /* programma P149.cpp */ #include class Spaarbus { private: int éénEuro, tweeEuro, totaal; public: // constructor met default argumenten Spaarbus(int stuk1= 0, int stuk2= 0) { éénEuro= stuk1; tweeEuro= stuk2; bereken(); } // overige lidfuncties Spaarbus som(Spaarbus b); void bereken(); void print(); }; Spaarbus Spaarbus::som(Spaarbus b) { Spaarbus temp; temp.éénEuro= éénEuro + b.éénEuro; temp.tweeEuro= tweeEuro + b.tweeEuro; temp.bereken(); return temp; } void Spaarbus::bereken() { totaal= éénEuro * 1 + tweeEuro * 2; } void Spaarbus::print() { cout<<”totaal aantal stukken van éénEuro: “<<éénEuro<<endl; cout<<”totaal aantal stukken van tweeEuro: “<
62
2.9 Object als referentie argument We weten hoe een object kan optreden als een ‘const reference argument’ van een functie. Die functie kan dan de inhoud van het betreffende object niet wijzigen. Door een referentie argument te gebruiken zonder const kan die functie wel de waarde van de leden van het object wijzigen. Voorbeeld: Stel dat de inhoud van twee spaarbussen moet gestopt worden in een derde spaarbus. De eerste twee spaarbussen komen dus leeg. Het programma is als volgt: /* programma P150.cpp */ #include class Spaarbus { private: int éénEuro, tweeEuro, totaal; void ledigen();
Bespreking van de lidfunctie opbergen(). De referentie argumenten van opbergen() zorgen ervoor dat de argumenten b1 en b2 een alias worden voor bus1 en bus2. De functie opbergen() maakt gebruik van andere lidfuncties. Merk op dat voor bereken() geen object staat en dat de beide aanroepen van ledigen() worden voorafgegaan door een object. Op welk object wordt bereken() toegepast? Op het object waarop de functie opbergen() wordt toegepast en dit is bus3. De programmalijnen bereken(); b1.ledigen(); b2.ledigen(); moeten dus als volgt worden geïnterpreteerd: bus3.bereken(); bus1.ledigen(); bus2.ledigen();
•
Bespreking van de lidfunctie ledigen(). De lidfunctie ledigen() is privaat gedeclareerd in de klasse Spaarbus. Dit betekent dat ledigen() enkel kan worden opgeroepen door lidfuncties van dezelfde klasse als van ledigen(), hier van de klasse Spaarbus. Hoewel private lidfuncties minder worden gebruikt dan private data-elementen, kan er soms toch een goede reden zijn om een lidfunctie privaat te declareren. In het beschouwde programmavoorbeeld is het leeg maken van een spaarbus een dermate ingrijpende handeling dat deze voorbehouden wordt aan een private lidfunctie. Op die manier wordt voorkomen dat vanuit een willekeurige plaats in het programma een bus geledigd kan worden.
•
Bespreking van void print() const. Const achter de functienaam print() betekent dat de functie print() het object waarmee hij wordt aangeroepen, bijvoorbeeld bus1.print(), niet kan wijzigen. Met andere woorden, bij volgende oproep: bus1.print(); is het object bus1 een constante voor de lidfunctie print(). Wanneer print() toch zou pogen bus1 te wijzigen, zal een foutmelding optreden.
64
Merk op dat const niet alleen in de declaratie van de lidfunctie print() moet staan, maar ook in de definitie van print().
2.10 Oefeningen 1) Schrijf een programma dat gebruik maakt van de klasse Getal. De klasse Getal bevat het private data-element g van het type int, en de publieke lidfuncties print() en invoer(). De gebruiker kan een waarde ingeven voor g en de uitvoer is als volgt: ingevoerde waarde: 10 g = 10 2) Schrijf een programma dat gebruik maakt van de klasse Getallenreeks. De gebruiker moet vijf getallen invoeren. Het programma toont de ingevoerde getallen, de gemiddelde waarde van die vijf getallen en de maximale waarde. Gebruik voor elke handeling een lidfunctie: invoer(), tonen(), gemiddelde() en maximum(). 3) Schrijf een programma dat gebruik maakt van de klasse Rechthoek. De gebruiker moet hoogte en breedte kunnen invoeren. Het programma berekent de omtrek en de oppervlakte van de rechthoek. Gebruik een lidfunctie voor de invoer, voor de berekening van de omtrek, voor de berekening van de oppervlakte en voor het tonen van de resultaten. 4) Schrijf een programma dat gebruik maakt van de klasse Spaarbus. De gebruiker moet het aantal stukken van éénEuro en van tweeEuro voor twee bussen kunnen invoeren. Het programma toont het aantal stukken van elke bus. Gebruik een lidfunctie voor het vullen van de spaarbussen en voor het tonen van de inhoud van de spaarbussen.
3. Conversie, klassen en operatoren 3.1 Algemeen Voor de standaardtypes (char, int, long, double) en voor de pointers bezit C++ volgende voorzieningen: 1) de operatoren +, -, * en /, die bewerkingen uitvoeren met waarden van de standaardtypes; 2) indien nodig, de conversie van het ene naar het andere type. Conversie kan automatisch gebeuren, zoals in double getal= 3;
//conversie van int 3 naar double 3.00
of kan afgedwongen worden met type-casting, zoals in double deling= double (11)/2; // resultaat: deling= 11.5 i.p.v. 11 Voor zelfgedefinieerde klassen bezit C++ bovenstaande voorzieningen niet. Wel kan de programmeur die voorzieningen creëren via: 1) operator overloading (= meervoudig gebruik van operatoren). Dit betekent dat de programmeur een nieuwe betekenis kan geven aan standaard operatoren, zodat die operatoren ook met objecten kunnen werken.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
65
Zo’n overloaden operator (overladen operator) behoudt wel zijn oorspronkelijke betekenis. Afhankelijk van de operanden (= de waarden waarop de operator werkt) zal de compiler voor de standaard of voor de nieuwe betekenis kiezen. Operator overloading biedt het voordeel dat bewerkingen met objecten op een natuurlijke wijze kunnen worden genoteerd. Dit maakt programma’s beter leesbaar en begrijpbaar. 2) zelf definiëren van de conversie van een standaardtype naar een klassetype of omgekeerd. Constructoren spelen hierbij een belangrijke rol.
3.2 De copy constructor 3.2.1 Algemeen De copy constructor wordt aangewend om een object x van een klasse gelijk te stellen aan een object y van diezelfde klasse. In volgend voorbeeld wordt het object y van klasse T gelijkgesteld aan het object x van klasse T. class T { private: int i, j; public: T(T& x1) { i= x1.i; j= x1.j; }; }; void main() } T x; T y(x); //kan in bepaalde gevallen ook: T y= x; (zie merk op) … } In bovenstaand voorbeeld is de copy constructor T(T&) voorzien. De opdracht T y(x) roept de copy constructor op. Een copy constructor heeft als enig argument de referentie naar de klasse waartoe hij behoort, in het voorbeeld de referentie T&. In het algemeen geldt dat, als T de naam is van een klasse, dan de copy constructor T(T&) zorgt voor de initialisatie van een object van dezelfde klasse T, m.a.w. de copy constructor zorgt voor het maken van een correcte kopie. Een copy constructor wordt gebruikt in elke situatie waar een object moet worden gelijk gemaakt aan een ander object van dezelfde klassen.
66
Dit is in volgende gevallen: •
Initialisatie van een nieuw object met een bestaand object.
•
Een ‘call by value’ aanroep van een functie. De parameter van de aangeroepen functie wordt geïnitialiseerd met het object dat wordt doorgegeven naar die functie.
•
Een functie geeft als returnwaarde een object (klasse-variabele) terug.
Merk op. Alhoewel de notatie T y= 10; lijkt op een toekenning, is het geenzins een toekenning. Een dergelijke notatie kan enkel met behulp van constructoren met één argument. In het voorbeeld van T y= 10; gaat de compiler op zoek naar een constructor met één argument van het type int. Dat argument krijgt de waarde 10 en dan pas wordt de constructor uitgevoerd om het object te maken. Een constructor met één argument zorgt voor de conversie van een standaardtype (in het voorbeeld: int) naar een klassetype (in het voorbeeld: T).
3.2.2 Programmavoorbeeld In volgend programma wordt het object t gelijk gesteld (geïnitialiseerd) met het object s met behulp van de copy constructor String(String&). /* programma P151.cpp */ #include #include<string.h> class String { private: char *p; int len; public: String(const char *ps)
// constructor1
{ p= new char[(len= strlen(ps))+1]; strcpy(p, ps); } String(String& str)
// copy constructor
{ p= new char[(len= str.len)+1]; strcpy(p, str.p); } void print() const { if (p) cout<
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
cout<<”Inhoud van s = “; s.print(); cout<<”Inhoud van t = “; t.print(); }
3.2.3 Default copy constructor Door verschillende constructoren te definiëren met argumenten van verschillend type of met een verschillend aantal argumenten, creëert de programmeur een gereedschap (E: tool) dat flexibel inzetbaar is. In volgend voorbeeld worden vijf objecten (p1 tot en met p5) gedeclareerd, die elk met een andere constructor worden geïnitialiseerd. /* programma P152.cpp */ #include #include<string.h> class Personeel { private: char naam[40]; int volgnummer; public: Personeel()
// default constructor
{ strcpy(naam, “geen naam”); volgnummer = 0; } // constructor met één argument van het type char* Personeel(char *n) { strcpy(naam, n); volgnummer = 0; } // constructor met één argument van het type int Personeel(int nr) { strcpy(naam, “geen naam”); volgnummer = nr; } // constructor met twee argumenten Personeel(char *n, int nr) { strcpy(naam, n); volgnummer = nr; }
68
void print() const { cout<
3.3 Constructoren en conversie 3.3.1 De conversie constructor De conversie constructor is een constructor die een object van een bepaald type converteert naar een object van een ander type. In volgend programma is de conversie constructor Complex(int i) gedefinieerd en zijn in de functie main() de objecten c1 en c2 gedeclareerd van het type Complex en geïnitialiseerd met respectievelijk de waarden 4 en 2.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
69
Alhoewel het normaal gezien niet mogelijk is om aan het complexe getal c enkel een int-waarde toe te kennen, kan dit toch met behulp van de conversie constructor Complex(int i). De compiler herkent de code c1= 4 als een conversie van een int naar een object van het type Complex, en zoekt in de klassedefinitie naar een conversie constructor, die dan de conversie maakt. De code c2(2) roept expliciet de conversie constructor op. /* programma P153.cpp */ #include class Complex { public: int a, b; Complex(int i)
// conversie constructor
{ a= i; b= 0; } }; void main(à { Complex c1= 4, c2(2); cout<<”complex getal c1 = (“<
3.3.2 Conversie door een constructor met default argumenten In het deel ‘Algemeen’ van de copy constructor (zie 3.2.1) hebben we geleerd dat een constructor met één argument zorgt voor de conversie van een standaardtype naar een klassetype. Behalve een constructor met één argument, kan ook een constructor met default argumenten zo’n conversie uitvoeren, op voorwaarde dat het mogelijk is die constructor aan te roepen met één argument van het bewuste standaardtype. Dit gebeurt in het volgend programma in de regel: Personeel(char *n, int nr= 0); Het programma maakt gebruik van de zelf geschreven copy constructor Personeel(Personeel &p). Deze definitie heeft enkel als doel om met de cout-opdracht duidelijk te tonen wanneer de copy constructor wordt geactiveerd. Anders heeft hier de zelf gedefinieerde copy constructor weinig zin, omdat hij eigenlijk precies hetzelfde werk doet als de default copy constructor, nl. het kopiëren van de inhoud van het ene object (het argument) naar het andere object (het nieuw te maken object).
70
We weten dat de compiler automatisch een default copy constructor genereert bij afwezigheid van een zelf gedefinieerde copy constructor. Toch zijn er situaties waarin het wel degelijk zinvol is om zelf een copy constructor te definiëren (zie verder). /* programma P154.cpp */ #include #include<string.h> class Personeel { private: char naam[40]; int volgnummer; public: Personeel(char *n, int nr= 0) { strcpy(naam, n); volgnummer = nr; cout<<”char* constructor”<endl; } Personeel(int nr) { strcpy(naam, “geen naam”); volgnummer = nr; cout<<”int constructor”<endl; } Personeel(Personeel &p) { strcpy(naam, p.naam); volgnummer = p.volgnummer; cout<<”copy constructor”<endl; } void print() const { cout<
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
71
De output van het programma is: De drie initialisaties: char* constructor int constructor copy constructor De drie objecten: 0, Els 5, geen naam 5, geen naam Bespreking van het programma: 1) Bij de initialisatie p1=”Els” zoekt de compiler een conversie die een string omzet naar een Personeel. Dit is de char* constructor, waarvan het tweede argument een default argument is. 2) Bij de initialisatie p2=5 zoekt de compiler een conversie die een int omzet naar een Personeel. Dit is de int constructor. 3) Bij de initialisatie p3=p2 zoekt de compiler een conversie die een Personeel kopieert naar een Personeel. Dit is de copy constructor.
3.3.3 Voorwaardelijke compilatie Voorwaardelijk compileren betekent dat opdrachten slechts worden gecompileerd als voldaan is aan een voorwaarde. Voorwaardelijk compileren gebeurt met de preprocessor-opdrachten #ifdef en #endif. De voorwaarde is dat de identifier, die staat achter #ifdef, is gedefinieerd met #define. Voorbeeld: #define LaatZien // . . . #ifdef LaatZien cout<<”constructor zonder argumenten”<<endl; #endif Met #define LaatZien wordt de opdracht tussen #ifdef LaatZien en #endif gecompileerd en kan worden uitgevoerd. Er mogen meerdere opdrachten worden geplaatst. Zonder #define LaatZien worden alle opdrachten tussen #ifdef LaatZien en #endif tijdens de compilatie genegeerd.
3.3.4 Conversie na de initialisatie In de vorige programmavoorbeelden worden de constructoren aangewend bij de declaratie en initialisatie van objecten. Initialisatie is de belangrijkste taak van een constructor. Een constructor kan ook na de initialisatie van een object nuttig zijn.
72
Volgend programma maakt gebruik van constructoren na de declaratie. Ook is voorwaardelijke compilatie toegepast. /* programma P155.cpp */ #include #include<string.h> #define LaatZien class Personeel { private: char naam[40]; int volgnummer; public: Personeel() { strcpy(naam, “geen naam”); volgnummer = 0; #ifdef LaatZien cout<<”default constructor”<endl; #endif } Personeel(char *n, int nr= 0) { strcpy(naam, n); volgnummer = nr; #ifdef LaatZien cout<<”char* constructor”<endl; #endif } Personeel(int nr) { strcpy(naam, “geen naam”); volgnummer = nr; #ifdef LaatZien cout<<”int constructor”<endl; #endif } Personeel(const Personeel &p) { strcpy(naam, p.naam); volgnummer = p.volgnummer; #ifdef LaatZien cout<<”copy constructor”<endl; #endif } void print() const { cout<
73
void main() { Personeel p1, p2, p3; p1= 5; p2= “Els”; p3= p2; cout<<endl; cout<<”De drie objecten: “<<endl; p1.print(); p2.print(); p3.print(); } De output van het programma is: default constructor default constructor default constructor int constuctor char* constructor De drie objecten: 5, geen naam 0, Els 0, Els Bespreking van het programma: 1) De copy constructor Personeel(const Personeel &p) moet worden aangeroepen met een argument dat een object is van de klasse Personeel. De referentie &p zorgt voor een efficiënte functie-aanroep en const zorgt ervoor dat het originele object niet kan gewijzigd worden. 2) De default constructor (de constructor zonder argumenten) wordt aangeroepen voor de objecten p1, p2 en p3. Die objecten worden dus geïnitialiseerd met de string “geen naam” en met volgnummer 0. 3) De int constructor wordt aangeroepen door het statement p1= 5. Omdat p1 reeds een bestaand object is, moet eigenlijk voor p1 geen constructor meer aan het werk gaan. Dat toch de int-constructor gebruikt wordt, komt door het feit dat de compiler op zoek gaat naar een constructor die het int-getal 5 wil converteren naar Personeel. De compiler vindt een dergelijke constructor en roept die aan, nl. de int constructor. De int constructor maakt een tijdelijk en naamloos object aan met als volgnummer de waarde 5. Vervolgens wordt de inhoud van het tijdelijke object gekopieerd in p1. Dit gebeurt niet met de copy constructor, maar met de toekenningsoperator, die standaard bij elke klasse is geleverd. Na het kopiëren wordt het tijdelijke object vernietigd. 4) Bij uitvoering van de opdracht p2=5 gebeurt hetzelfde als bij p1=”Els”, doch nu met gebruik van de char* constructor om een tijdelijk object te maken. 5) Bij p3=p2 wordt de inhoud van p2 gewoon gekopieerd in p3 met behulp van de standaard toekenningsoperator.
74
3.3.5 Conversie met andere constructoren We weten dat een constructor met één argument van een standaardtype zorgt voor de conversie van dat standaardtype naar een klassetype. De beperking van één argument kan omzeild worden door een constructor expliciet aan te roepen. Voorbeeld: Stel de declaratie van volgend object: Personeel p1; . De constructor met twee argumenten, nl. Personeel(char *n, int nr) kan als volgt worden aangeroepen: p1= Personeel(“Rik”, 15); De constructor maakt dan een tijdelijk object met de naam “Rik” en volgnummer 15. Vervolgens wordt de inhoud van het tijdelijk object gekopieerd in p1 met behulp van de standaard toekenningsoperator. Dit mechanisme kan ook toegepast worden met andere constructoren: p1= Personeel(“Ria”); // char* constructor en standaard toekenning p1= Personeel();
// default constructor en standaard toekenning
3.4 Overzicht en samenvatting constructoren Constructoren zijn lidfuncties die een belangrijke rol spelen bij het maken en initialiseren van objecten. Een klasse X heeft een copy constructor X(const X&), die tot taak heeft een object te maken door er een te kopiëren. Door zelf constructoren te overloaden wordt het mogelijk een hele reeks conversiemogelijkheden te maken, die in heel wat situaties beschikbaar zijn, zowel tijdens de initialisatie van objecten als in toekenningsopdrachten. Initialisatie: gebruik van constructoren tijdens de declaratie Constructor met 0 argumenten: Personeel p; Personeel p= Personeel(); Constructor met 1 argument: Personeel p(“Els”); Personeel p=”Els”; Personeel p= Personeel(“Els”); Constructor met 2 argumenten: Personeel p(“Els”, 5); Personeel p= Personeel(“Els”, 5); Na de declaratie van een object y, waarbij bijvoorbeeld de default constructor is gebruikt en waarbij al of niet initialisatie is gebeurd, kan aan dat object y waarden worden gegeven met behulp van constructoren.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
75
Toekenning: gebruik van constructoren na de declaratie. Constructor met 0 argumenten: y= Personeel(); Constructor met 1 argument: y= “Els”; y= Personeel(“Els”); Constructor met 2 argumenten: y= Personeel(“Els”, 5);
3.5 Operator overloading 3.5.1 Algemeen De taal C++ laat het gebruik toe van operator-functies. Een operator-functie laat de programmeur toe om bepaalde functies te schrijven in algebraïsche notatie, zoals bij het optellen van twee getallen: a + b . Het gebruik van de operator ‘+’ is conceptueel eenvoudiger en leesbaarder dan de functie optellen(int a, int b). In C zijn de operatoren, zoals +, -, *, /, standaard voorzien in de taal. In verband met klassen zijn in C++ slechts twee operatoren op objecten standaard gedefinieerd: de toekenningsoperator ‘=’ en de adres-van-operator ‘&’. De toekenningsoperator kopieert het ene object lid voor lid in het andere object. De adres-van-operator levert het adres op van het object. Daarentegen laat C++ toe om expliciet operatoren te definiëren die kunnen inwerken op reeds gedefinieerde klassen. Het is dus mogelijk om de operator ‘+’ te definiëren voor het optellen van twee getallen, die behoren tot een klasse. Dergelijke zelf gedefinieerde operatoren zijn volledig equivalent met gewone functies. Zelf gedefinieerde operatoren zijn gewoonweg een andere en soms betere notatie van een functie. Het is raadzaam enkel operatoren zelf te definiëren als ze een zinvolle betekenis hebben. In de taal C zijn bepaalde operatoren zoals +, -, *, /, gedefinieerd als overladen operatoren (E: overloaded operators). Dit betekent dat bijvoorbeeld de operator ‘+’ kan worden gebruikt voor het optellen van twee integers, maar ook kan worden gebruikt voor het optellen van twee floats. In C++ moet de programmeur zelf het overladen van operatoren definiëren.
3.5.2 Lidfunctie die de som berekent van twee objecten Volgend programma maakt gebruik van de klasse Voorraad, waarin de aantallen worden bijgehouden van kleine en grote artikelen. Verder bezit de klasse Voorraad de lidfunctie som(), die de som berekent van twee objecten van de klasse Voorraad.
76
/* programma P156.cpp */ #include class Voorraad { private: int klein, groot; public: Voorraad(int k=0, int g=0) { klein= k; groot= g; } Voorraad som(Voorraad); void print() const; }; Voorraad Voorraad::som(Voorraad v) { Voorraad hulp; hulp.klein= klein + v.klein; hulp.groot= groot + v.groot; return hulp; } void Voorraad::print() const { cout<
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
77
2) De lidfunctie som() wordt aangeroepen door totaal= v1.som(v2); Betekenis: stuur aan v1 de boodschap dat hij de som moet maken van zichzelf en van v2. Het resultaat, de functiewaarde dus, moet in totaal worden opgeslagen. Omdat de functiewaarde een object is met de leden klein en groot, waarvan eerst de waarde moet worden bepaald, is in de functie som() het object hulp gedeclareerd. De som wordt aldus als volgt berekend: hulp.klein = klein + v.klein; Omdat de functie som() is aangeroepen met v1.som(v2), moet vorige berekening als volgt worden gelezen: hulp.klein = v1.klein + v2.klein;
3.5.3 Tijdelijk object door een constructor laten maken In vorig programma is het object hulp gedeclareerd in de functie som(). In het object hulp worden een paar waarden opgeslagen. Die twee bewerkingen, namelijk -
het maken van het object hulp
-
en het opbergen van waarden in het object hulp,
zijn eigenlijk typisch werk voor een constructor. En de klasse Voorraad heeft hiervoor een geschikte constructor. Die constructor moet alleen worden opgeroepen met de juiste waarden voor de elementen, namelijk als volgt: Voorraad(klein + v.klein, groot + v.groot); Hiermee wordt een object gemaakt, dat de som bevat van de twee voorraden. Dit is precies het object dat som() als functiewaarde moet afleveren. Daarom is volgende definitie van som() beter: Voorraad Voorraad::som(Voorraad v) { return Voorraad(klein + v.klein, groot + v.groot) } Het is beter gebruik te maken van een constructor wanneer een tijdelijk object nodig is. Het vorig programma wordt dus: /* programma P157.cpp */ #include class Voorraad { private: int klein, groot; public:
78
Voorraad(int k=0, int g=0) { klein= k; groot= g; } Voorraad som(Voorraad); void print() const; }; Voorraad Voorraad::som(Voorraad v) { return Voorraad(klein + v.klein, groot + v.groot); } void Voorraad::print() const { cout<
3.5.4 De operator + in plaats van de somfunctie In vorig programma wordt de lidfunctie som() als volgt aangeroepen: totaal = v1.som(v2); Hierbij zijn drie objecten betrokken: v1, v2 en totaal. De notatie totaal = v1 + v2; is mogelijk door de naam ‘som’ te vervangen door ‘operator +’. Dus ‘Voorraad som(Voorraad)’ wordt ‘Voorraad operator+(Voorraad)’ en ‘totaal= v1.som(v2)’ wordt ‘totaal= v1.operator+(v2)’. De ‘operator+’ is een voorbeeld van operator-functie, namelijk een operator die als functie genoteerd wordt: operator+(). Nu mag de notatie ‘operator+()’ worden vervangen door het +teken, dus: v1.operator+(v2) wordt v1 + v2. Het programma met de operator-functie operator+() is als volgt:
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
79
/* programma P158.cpp */ #include class Voorraad { private: int klein, groot; public: Voorraad(int k=0, int g=0) { klein= k; groot= g; } Voorraad::operator+(Voorraad v) { return Voorraad(klein + v.klein, groot + v.groot); } void print() const; }; void Voorraad::print() const { cout<
3.5.5 Operatoren voor overloading Enkel bestaande operatoren kunnen overladen worden, dit is een nieuwe betekenis geven aan de operator. Volgende operatoren kunnen worden overladen: +
-
*
/
%
^
&
|
~
!
=
<
>
+=
-=
*=
/=
%=
^=
&=
|=
<<
>>
>>=
<<=
==
!=
<=
>=
&&
||
++
--
.
->*
->
()
[]
new
delete
80
Voor de overladen operatoren gelden volgende zaken zoals voor de originele operatoren: -
de prioriteit
-
de associativiteit
-
de ronde haakjes
-
het aantal operanden
Volgende operatoren kunnen zowel met één operand als met twee operanden voorkomen: +, -, * en &. Het overladen van een operator met één operand vereist een operator-functie zonder argument. Voorbeeld: Voorraad operator-() { return Voorraad(-klein, -groot); } De return-opdracht maakt gebruik van de constructor van de klasse Voorraad. Een operator met twee operanden krijgt een operator-functie met één argument. Voorbeeld: Voorraad operator+(Voorraad v) { return Voorraad(klein + v.klein, groot + v.groot); }
3.5.6 De this-pointer 3.5.6.1 De this-pointer Bij aanroep van een lidfunctie levert C++ automatisch een pointer op naar het object waarmee de functie wordt aangeroepen. Deze pointer heet altijd ‘this’. De this-pointer is in elke lidfunctie, dus ook in de operator-functies, te gebruiken. Voorbeelden: •
Bij aanroep van de lidfunctie print() met x.print(), waarbij x een object is, zal de pointer this wijzen naar het object x.
•
Bij aanroep van de lidfunctie som(y) via de opdracht x.som(y), zal de thispointer wijzen naar x.
•
Bij aanroep van de operator-functie operator+=() via de opdracht x+=y, zal de pointer this wijzen naar x.
Als de this-pointer wijst naar een object, dan is uiteraard de uitdrukking *this dat object zelf. Dus kan *this gebruikt worden wanneer de operator-functie een object als functiewaarde moet opleveren.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
81
Voorbeeld: Voorraad& operator+=(Voorraad v) { klein += v.klein; groot += v.groot; return *this;
// gebruik van de this-pointer
}
3.5.6.2 Overladen van een assignment-operator De this-pointer is nodig om een toekenningsoperator zoals += correct te kunnen overladen, namelijk dat de zelf gedefinieerde operator += (operator-functie) volledig het gedrag van de originele operator += nabootst. Voorbeeld: Voorraad& operator+=(Voorraad v) { klein += v.klein; groot += v.groot; return *this; } Merk op dat Voorraad& een ‘return by reference’ moet zijn. Immers, met een ‘return by value’, zoals in volgende definitie: Voorraad operator+=(Voorraad v)
// zonder &
{ klein += v.klein; groot += v.groot; return *this; } wordt er een tijdelijk object aangemaakt dat geïnitialiseerd wordt met het object dat achter return staat. Voor deze initialisatie zorgt de copy constructor. Voorbeeld: Voorraad v1(8, 15), v2(5, 7), v3(6, 2); (v1 += v2) += v3; De uitdrukking (v1 += v2) levert een tijdelijk object af dat wel dezelfde inhoud heeft als het object v1, maar niet het object v1 is. Met (v1 += v2) += v3 wordt vervolgens bij dat tijdelijk object de inhoud van v3 opgeteld. Daarna gaat het tijdelijk object verloren. Dit betekent dat de uitdrukking (v1 += v2) += v3 enkel als resultaat geeft dat de inhoud van v1 is vermeerderd met de inhoud van v2, maar daarmee is het ook gedaan. Met gebruik van ‘return by reference’ wordt de uitdrukking (v1 += v2) in zijn geheel een alias voor v1. Dit betekent dat in de uitdrukking (v1 += v2) += v3 eerst v2 wordt opgeteld bij v1 en dat vervolgens bij v1 de inhoud van v3 wordt opgeteld.
82
3.6 Friend operatoren 3.6.1 Friend De C++ compiler weigert toegang tot klasse-elementen die ‘private’ of ‘protected’ zijn gedeclareerd. Deze stricte regel zorgt soms voor problemen wanneer gewone procedures private elementen van een bepaalde klasse nodig hebben. Wanneer deze situatie zich voordoet, kan de programmeur gebruik maken van het concept ‘friend’. Friend laat toe om een gecontroleerde toegang mogelijk te maken tot de private elementen van een klasse. Een procedure die gebruik wil maken van de private elementen van een klasse, wordt als friend gedeclareerd in die klasse. Voorbeeld: class X { friend int ToegangPriveElement(void); friend class Y; private: int PriveElement; }; De procedure ToegangPriveElement() is gedeclareerd als een friend zodat ze toegang krijgt tot het privaat data-element PriveElement. Naast gewone procedures, kunnen ook lidfuncties van een andere klasse en zelfs een volledige klasse (zie class Y in het voorbeeld) als friend gedeclareerd worden. Wanneer een klasse als friend gedeclareerd wordt, dan zijn alle leden van die klasse friends van de andere klasse.
3.6.2 Probleem Volgend programma maakt de vermenigvuldiging van de inhoud van de klasse Voorraad met de int-waarde 4, zonder gebruik van de friend-operator. Het probleem is dat ‘resultaat= 4 * v’ niet kan. /* programma P159.cpp */ #include class Voorraad { private: int klein, groot; public: Voorraad(int k=0, int g=0) { klein= k; groot= g; }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
83
Voorraad operator*(int i);
// operator*()
{ return Voorraad(klein * i, groot * i); } void print() const; }; void Voorraad::print() const { cout<
3.6.3 De friend operator De friend operator is geen lid van een klasse, maar een operator die als ‘bevriende’ operator van de klasse wordt gedefinieerd. Bevriend wil zeggen dat de operator toegang krijgt tot de private leden van de klasse. De friend operator*() met als taak een int-waarde te vermenigvuldigen met een Voorraad, heeft twee argumenten: 1) het eerste argument is van het type int 2) het tweede argument is van het type Voorraad. Dus:
friend Voorraad operator*(int i, Voorraad v) { return Voorraad(i * v.klein, i * v.groot); }
84
Met de friend operator*() wordt de uitdrukking resultaat= 4 * v; geïnterpreteerd als operator*(4, v), dus als een operator met twee argumenten. Volgend programma maakt de vermenigvuldiging van de inhoud van de klasse Voorraad met de int-waarde 4, met gebruik van de friend operator*(), zodat de uitdrukkingen res1 = v * 4; en
res2 = 4 * v;
beide mogelijk zijn. /* programma P160.cpp */ #include class Voorraad { private: int klein, groot; public: Voorraad(int k=0, int g=0) { klein= k; groot= g; } // operator*() als lid Voorraad operator*(int i); { return Voorraad(klein * i, groot * i); } // operator*() als friend friend Voorraad operator*(int i, Voorraad v); { return Voorraad(i * v.klein, i * v.groot); } void print() const { cout<
85
Meestal is een friend operator nodig als de operator een operand heeft van een standaardtype zoals int, long, double of char*.
3.7 Conversie van een klasse naar een standaardtype 3.7.1 De conversie-functie We weten reeds hoe met behulp van constructoren een conversie wordt gerealiseerd van een standaardtype naar een klassetype. Voor de conversie van een klasse naar een standaardtype moet een speciale conversie-functie geschreven worden. Voorbeeld: class Personeel { private: char naam[40]; int volgnummer; public: // . . . //declaratie in de klasse Personeel van de conversie-functie voor //conversie van een object Personeel naar een string, dus conversie //naar het standaardtype char*: operator char*() { return naam; } //declaratie in de klasse Personeel van de conversie-functie voor //conversie van een object Personeel naar een int: operator int() { return volgnummer; } //. . . }; Een conversie-functie heeft de naam van het standaardtype waar hij naar converteert, voorafgegaan door het woord operator. Een conversie-functie heeft nooit argumenten en heeft nooit een return-type. De naam van de functie geeft eigenlijk het return-type reeds aan. Volgend programma maakt gebruik van conversie van een klasse naar standaardtype. /* programma P161.cpp */ #include #include<string.h>
3.7.2 Impliciete en expliciete aanroep van een conversie-functie Bij uitvoering van strcpy(str, p1); zoekt de compiler naar een mogelijkheid om het object p1 om te zetten naar een char*. De compiler vindt die in de conversie-functie operator char*. Op die manier wordt de conversie-functie impliciet aangeroepen. Een expliciete aanroep is als volgt: strcpy(str, (char*) p1);
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
87
3.8 Conversie tussen klassen Volgend programma maakt de conversie van de klasse Lid naar de klasse Bestuurslid en omgekeerd. /* programma P162.cpp */ #include #include<string.h> class Bestuurslid;
//declaratie van de naam van de klasse
class Lid { private: char lidnaam[40]; int lidnummer; public: Lid(char*, int); operator Bestuurslid(); //declaratie van de naam van de operator void print() const; }; class Bestuurslid
Bestuurslid::Bestuurslid(char* naam= “geen naam”, int nr= 0) { strcpy(bestuurslidnaam, naam); bestuurslidnummer= nr; } Bestuurslid::operator Lid() { return Lid(bestuurslidnaam, bestuurslidnummer); } void Bestuurslid::print() { cout<
//conversie van lid naar bestuurslid
cout<<”Het bestuur: “<<endl; b1.print(); b2.print(); Lid ld3 = b2;
//conversie van bestuurslid naar lid
cout<<”De leden: “<<endl; ld1.print(); ld2.print(); ld3.print(); } De output van het programma is: Het bestuur: 6, Rik 8, Ria De leden: 5, Els 6, Rik 8, Ria Bespreking van het programma: 1) Voor b1=ld2 is de conversie van Lid naar Bestuurslid nodig. Dit kan gerealiseerd worden met een conversie-functie in de vorm van een operator, die als lidfunctie van de klasse Lid wordt gedeclareerd: operator Bestuurslid() { return Bestuurslid(lidnaam, lidnummer); }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
89
Deze operator converteert wel een Lid naar Bestuurslid, doch de compiler, die de tekst van het programma van boven naar beneden leest, kent de klasse Bestuurslid nog niet op het moment dat hij bij de klasse Lid is. Dit probleem wordt opgelost door vóór de definitie van de klasse Lid de declaratie van de klasse Bestuurslid te plaatsen. Dit bebeurt met: class Bestuurslid; . De compiler neemt kennis van de naam Bestuurslid en rekent erop dat de definitie van de klasse Bestuurslid verderop in het programma staat. 2) In de conversie-functie operator Bestuurslid() { return Bestuurslid(lidnaam, lidnummer); } wordt gebruik gemaakt van de constructor van de klasse Bestuurslid, en deze constructor wordt pas verderop gedefinieerd. Dit probleem wordt opgelost door in de klasse Lid enkel de declaratie van de conversie-functie, nl. operator Bestuurslid(), op te nemen, en de definitie van de conversie-functie, waarin de aanroep van de constructor staat, na de definitie van alle klassen te plaatsen. Buiten de klassen doet de volgorde van de lidfuncties er niet toe, omdat de compiler in eerste instantie afgaat op de declaraties.
3.9 Oefeningen 1) Schrijf een programma met een klasse voor een voertuig. De klasse bevat de data-elementen ‘soort voertuig’ en ‘aantal wielen’. Het programma moet vier verschillende constructoren bevatten, evenals de declaratie van acht objecten, waarvoor zowel bij initialisatie als bij toekenningen, diverse constructoren worden gebruikt. Voorzie de output van het programma met commentaar. 2) Schrijf een programma dat gebruik maakt van de objecten X, Y en Z van de klasse Voorraad, waarin de aantallen worden bijgehouden van de kleine en grote artikelen. Het programma moet volgende bewerkingen kunnen verrichten:
A = B – C; A = B * 1.5;
3) Schrijf een programma dat gebruik maakt van de klasse Tijd, die de int-leden uur, minuten en seconden bevat. Het programma moet twee tijden kunnen optellen (t3 = t1 + t2) en moet de berekening t1 += t2 (betekenis: t1 = t1 + t2) kunnen uitvoeren met objecten van de klasse Tijd.
90
4. Overerving 4.1 Overerving (E: inheritance) 4.1.1 Basisklasse en afgeleide klasse De grote kracht van een OO-taal is de mogelijkheid om vanuit een bestaande klasse (genoemd de basisklasse, E: base class) een nieuwe klasse (genoemd de afgeleide klasse, E: derived class) af te leiden die een meer gespecialiseerde taak uitvoert dan de basisklasse. De afgeleide klasse erft alle eigenschappen van zijn basisklasse, en kan ook nog eigenschappen van zichzelf hebben. Onder ‘eigenschappen’ worden alle dataelementen en lidfuncties verstaan. Voorbeeld: De klasse Rechthoek is een afgeleide klasse van de klasse Veelhoek. Immers, een rechthoek is een speciale veelhoek, namelijk een veelhoek met vier rechte hoeken. De klasse Vierkant is een afgeleide klasse van de klasse Rechthoek. Immers, een vierkant is een speciale rechthoek, namelijk een rechthoek met gelijke lengte en breedte. Het voorbeeld toont aan dat een afgeleide klasse op haar beurt de basisklasse kan zijn van een andere klasse.
4.1.2 Doelstellingen van overerving Overerving heeft twee doelstellingen: 1) Overerving laat de programmeur toe om de hiërarchische natuur van veel problemen op een elegante manier te implementeren in de OO-taal. In die zin is overerving een middel om het concept van specialisatie te verwezenlijken. Het concept van specialisatie stopt de algemene aspecten van een probleem in een basisklasse en kent meer gespecialiseerde eigenschappen toe aan afgeleide klassen. 2) Overerving laat toe om bestaande klassen aan te passen aan eigen noden. Voorbeeld: Een bibliotheekfunctie voldoet niet altijd volledig aan de wensen van de programmeur. In C++ kan dit elegant opgelost worden door een nieuwe klasse te maken, afgeleid van de bestaande bibliotheekklasse en vervolgens aan deze nieuwe klasse bijkomende lidfuncties en/of data-elementen toe te voegen zodat de afgeleide klasse beantwoordt aan alle criteria die de programmeur beoogt.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
91
4.2 De basisklasse Volgend programma maakt gebruik van de klasse Rechthoek, met als dataelementen ‘breedte’ en ‘hoogte’. Een constructor met default argumenten zorgt voor de constructie van het object R en de lidfunctie print() zet het object R op het scherm met behulp van sterretjes en spaties. /* programma P163.cpp */ #include class Rechthoek { private: int hoogte; int breedte; public: Rechthoek(int h= 1, int b= 1); void print(); }; Rechthoek::Rechthoek(int h, int b) { hoogte= h; breedte= b; } void Rechthoek::print() { for (int r= 0; r < hoogte; r++) { for (int k= 0; k < breedte; k++) cout<<”* “; cout<<endl; } } void main() { Rechthoek R(3,5); R.print(); cin.get(); } De output van het programma is: ***** ***** ***** Een voor-de-hand-liggende uitbreiding van bovenstaand programma is de mogelijkheid voor de gebruiker om de afmetingen van de rechthoek zelf te kunnen bepalen binnen bepaalde grenzen.
92
In plaats van de klasse Rechthoek te wijzigen met bijkomende lidfuncties, wordt de klasse Rechthoek beschouwd als de basisklasse en wordt van de klasse Rechthoek een afgeleide klasse gemaakt met daarin de nodige bijkomende lidfuncties.
4.3 Afgeleide klasse 4.3.1 Constructie van een afgeleide klasse Het construeren van een afgeleide klasse bestaat erin om bepaalde elementen aan een klasse toe te voegen en/of te wijzigen. Op die manier wordt de reeds bestaande code van de basisklasse herbruikt, maar wel aangepast aan de eisen opgelegd door de afgeleide klasse. De procedure voor het afleiden van een klasse B (Base class) bestaat erin een nieuwe klasse D (Derived class) als volgt te creëren: class D : public B { // declaratie van de D-leden }; Hiermee is een afgeleide klasse D gecreëerd die publiek afgeleid is van de klasse B. Met andere woorden, D is een afgeleide klasse van B, en B is de basisklasse van D, of nog: D erft van B. Het hele mechanisme heet overerving.
4.3.2 Geheugenlayout van een afgeleid object geheugen afgeleid object leden basisklasse unieke leden van de afgeleide klasse
In het geheugen vormen de leden, afkomstig van de basisklasse, het eerste deel van een afgeleid object. Het tweede deel van dat afgeleide object bevat de nieuwe leden. Uit de geheugenlayout van een afgeleid object kunnen we besluiten dat we altijd een afgeleide klasse kunnen gebruiken daar waar een basisklasse verwacht wordt. Immers, de afgeleide klasse bezit alle leden van de basisklasse. Dus, elke functie die inwerkt op een basisklasse, kan ook inwerken op de afgeleide klasse.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
93
Uiteraard moet vóór de definitie van de afgeleide klasse D (zie procedure in 4.3.1) de compiler reeds de definitie van de klasse B gelezen hebben. Dit geldt ook voor de declaratie van een object behorend tot de klasse D.
4.3.3 Programmavoorbeeld /* programma P164.cpp */ #include const int KleinsteBreedte= 1, KleinsteHoogte= 1, GrootstBreedte= 39, GrootsteHoogte= 24; class Rechthoek
// basisklasse
{ protected:
// protected zodat de leden toegankelijk zijn
int hoogte;
// vanuit de afgeleide klasse
int breedte; public: Rechthoek(int h= KleinsteHoogte, int b= KleinsteBreedte); void print(); }; class FlexRechthoek : public Rechthoek
// afgeleide klasse
{ public: void breder(); void hoger(); }; // lidfuncties van Rechthoek Rechthoek::Rechthoek(int h, int b) { hoogte= h; breedte= b; } void Rechthoek::print() { for (int r= 0; r < hoogte; r++) { for (int k= 0; k < breedte; k++) cout<<”* “; cout<<endl; } } // lidfuncties van FlexRechthoek void FlexRechthoek :: breder() { if (breedte < GrootsteBreedte) breedte++; }
94
void FlexRechthoek :: hoger() { if (hoogte < GrootsteHoogte) hoogte++; } void main() { FlexRechthoek R; R.print(); cout<<endl; R.breder(); R.breder(); R.hoger(); R.print(); cin.get(); } De output van het programma is: * *** *** Merk op: 1) In de definitie van de basisklasse Rechthoek zijn de data-elementen ‘protected’ in plaats van ‘private’. Dit is om de data-elementen van de basisklasse toegankelijk te maken vanuit de afgeleide klasse FlexRechthoek. 2) De afgeleide klasse heeft geen eigen constructor. Toch wordt de opdracht ‘FlexRechhoek F’ correct uitgevoerd (zie verder). 3) De afgeleide klasse FlexRechthoek erft de lidfunctie print() van de basisklasse Rechthoek. De afgeleide klasse heeft dus zelf geen eigen functie print().
4.3.4 Private, public en protected Wat erft de afgeleide klasse FlexRechthoek in vorig programma precies? In principe alle leden van de basisklasse Rechthoek, behalve de constructor. Dit betekent echter niet dat objecten van de afgeleide klasse automatisch toegang hebben tot alle leden van de basisklasse. Vanuit de afgeleide klasse FlexRechthoek is er enkel toegang tot de publieke ( public) en tot de beschermde (protected) leden, niet tot de private (private) leden. Leden van een basisklasse die protected zijn, zijn wel toegangelijk vanuit afgeleide klassen, maar niet van buitenaf, bijvoorbeeld vanuit main().
4.3.5 Default-constructor van de basisklasse In bovenstaand programma heeft de afgeleide klasse FlexRechthoek geen constructor. Hoe kan dan toch een object van FlexRechthoek gemaakt worden met de opdracht FlexRechthoek R ? Eerst wordt de default-constructor van de basisklasse Rechthoek aangeroepen om een object van Rechthoek te maken. De default-constructor van Rechthoek zorgt er T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
95
dus voor dat de data-leden ‘hoogte’ en ‘breedte’ de waarde 1 krijgen. En FlexRechthoek erft deze leden met hun waarde. Omdat deze leden ‘protected’ zijn in de basisklasse, is hun toegankelijkheid in de afgeleide klasse gegarandeerd. Dat de default-constructor van de basisklasse wordt aangeroepen is te controleren door een cout-opdracht te plaatsen in de constructor, zoals volgt: Rechthoek::Rechthoek(int h, int b) { hoogte= h; breedte= b; cout<<”Default-constructor van de basisklasse.”<<endl; } Een default-constructor is een constructor zonder argumenten, of zoals in het beschouwde voorbeeld, een constructor die zonder argumenten kan worden aangeroepen omdat alle argumenten default-waarden hebben. De default-waarden staan in de declaratie, namelijk: Rechthoek(int h= KleinsteHoogte, int b= KleinsteBreedte); met KleinsteHoogte en KleinsteBreedte gedefinieerd als constanten met een waarde 1. Algemene regel: De regel is dat altijd eerst de default-constructor van de basisklasse wordt aangeroepen om een object van de basisklasse te maken. Pas daarna komt een eventuele aanwezige constructor van de afgeleide klasse aan de beurt.
4.3.6 Eigen constructor van een afgeleide klasse Omdat in vorig programma de afgeleide klasse FlexRechthoek geen eigen constructor heeft, is het niet mogelijk om het volgende te declareren: FlexRechthoek R(3, 4); . Immers, er is geen passende constructor. Van de basisklasse wordt enkel de default-constructor automatisch aangeroepen. Wel is het mogelijk om elke afgeleide klasse één of meer eigen constructoren te geven. Een geschikte constructor voor FlexRechthoek is: FlexRechthoek(int ht, int br) { hoogte= ht, breedte= br; }
4.3.7 Initializer Het werk dat de constructor van FlexRechthoek doet, kan ook gedaan worden door de constructor van de basisklasse, namelijk: Rechthoek(int h, int b) { hoogte= h, breedte= b; }
96
Daartoe moet de constructor van de basisklasse expliciet worden aangeroepen vanuit de afgeleide klasse, en wel als volgt: FlexRechthoek(int ht, int br) : Rechthoek(ht, br) {} De constructor van de basisklasse wordt aangeroepen vóór de accolades van de body van de constructor. Deze manier van aanroepen van de constructor van de basisklasse heet een ‘initializer’. In het opgegeven voorbeeld van initializer staat er niets tussen de accolades omdat er niets moet gedaan worden. In het algemeen kunnen tussen de accolades opdrachten geplaatst worden.
4.4 Functie-overriding 4.4.1 Functie-overriding In vorig programma zet de lidfunctie print() de rechthoek op het scherm met behulp van sterretjes en spaties. Om de rechthoek te kunnen opbouwen met een willekeurig teken (symbool), moet de afgeleide klasse FlexRechthoek uitgebreid worden met het data-element ‘symbool’, waarin het teken wordt opgeslagen waaruit de rechthoek moet worden opgebouwd. class FlexRechthoek : public Rechthoek { private: char symbool; public: // declaratie van de constructor voor FlexRechthoek FlexRechthoek(int ht, int br, char symb= ‘*’); void breder(); void hoger(); }; De constructor voor FlexRechthoek moet een deel van het werk zelf doen, en kan voor een deel terecht bij Rechthoek. De definitie van de constructor is als volgt: FlexRechhoek::FlexRechthoek(int ht, int br, char symb):Rechthoek(ht, br) { symbool= symb; } Omdat de functie print() van Rechthoek enkel sterretjes en spaties afdrukt, moet FlexRechthoek zijn eigen print-functie hebben. De print-functie van de basisklasse wordt daarmee overridden.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
97
De print-functie van FlexRechthoek is als volgt: void FlexRechthoek::print() { for (int r= 0; r < hoogte; r++) { for (int k= 0; k < breedte; k++) cout<<symbool<<’ ‘; cout<<endl; } }
4.4.2 Programma met overriding van de print-functie /* programma P165.cpp */ #include const int KleinsteBreedte= 1, KleinsteHoogte= 1, GrootstBreedte= 39, GrootsteHoogte= 24; class Rechthoek
// basisklasse
{ protected:
// protected zodat de leden toegankelijk zijn
int hoogte;
// vanuit de afgeleide klasse
int breedte; public: Rechthoek(int h= KleinsteHoogte, int b= KleinsteBreedte); void print(); }; class FlexRechthoek : public Rechthoek
// afgeleide klasse
{ private: char symbool; public: // declaratie van de constructor voor FlexRechthoek FlexRechthoek(int ht, int br, char symb= ‘*’); void breder(); void hoger(); void print(); };
// lidfuncties van Rechthoek Rechthoek::Rechthoek(int h, int b) { hoogte= h; breedte= b; }
// overridden
98
void Rechthoek::print() { for (int r= 0; r < hoogte; r++) { for (int k= 0; k < breedte; k++) cout<<”* “; cout<<endl; } } // lidfuncties van FlexRechthoek FlexRechhoek::FlexRechthoek(int ht, int br, char symb):Rechthoek(ht, br) { symbool= symb; } void FlexRechthoek :: breder() { if (breedte < GrootsteBreedte) breedte++; } void FlexRechthoek :: hoger() { if (hoogte < GrootsteHoogte) hoogte++; } void FlexRechthoek::print() { for (int r= 0; r < hoogte; r++) { for (int k= 0; k < breedte; k++) cout<<symbool<<’ ‘; cout<<endl; } } void main() { FlexRechthoek R(3, 5, ‘#’; R.print(); cout<<endl; R.breder(); R.breder(); R.hoger(); R.print(); cin.get(); }
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
99
4.4.3 Functie overriding versus functie overloading Niettegenstaande zowel bij functie overriding als bij functie overloading het gaat over verschillende functies met dezelfde naam, zijn overriding en overloading wel verschillende zaken. •
Bij overloading moeten de functies (met dezelfde naam) een verschillend aantal argumenten hebben, of moeten de argumenten van de functies van verschillend type zijn.
•
Bij overriding hebben de functies in de basisklasse en in de afgeleide klasse niet alleen dezelfde naam, maar ook nog precies dezelfde argumenten.
Merk op: In literatuur over C++ wordt overriding toch soms aangeduid met ‘overloaden van een functie’. Men heeft het dan over ‘specialisatie’ van een klasse. Specialisatie van een klasse betekent het modifieren van een bestaande klasse naar nieuwe vereisten van de programmeur. Dit kan gebeuren door functies uit een basisklasse te ‘overloaden’ met nieuwe functies. Overloading van een lidfunctie wordt vooral gebruikt wanneer we een ‘uitzondering op de regel’ tegenkomen, dit wil zeggen wanneer een afgeleide klasse zich volledig gedraagt volgens de basisklasse, uitgezonderd voor één functie die anders dient te worden geïmplementeerd. Voorbeeld: class Vogel { private: int poten; int vleugels; int standVleugels, standPoten; public: Vogel() { poten= 2; vleugels= 2; standVleugels= RUST; standPoten= RUST; } void voortbewegen() { standVleugels= OP_EN_NEER; standPoten= RUST; } };
100
class Struisvogel : public Vogel { void voortbewegen() { standVleugels= RUST; standPoten= HEEN_EN_WEER; } }; In het beschouwde voorbeeld ‘is een’ struisvogel een vogel, maar bij het voortbewegen gebruikt de struisvogel niet zijn poten. Dit komt tot uitdrukking in de afgeleide klasse door het herdefiniëren van de lidfunctie voortbewegen().
4.4.4 Overridden functie aanroepen vanuit de afgeleide klasse De oorspronkelijke print-functie van de basisklasse blijft bruikbaar door ze aan te roepen vanuit een lidfunctie van de afgeleide klasse, voorafgegaan door de naam van de basisklasse en de scope-operator. Voorbeeld:
Rechthoek :: print();
4.5 Toegangsregels 4.5.1 Algemeen De woorden private, protected en public heten ‘access specifiers’. Ze specificeren de toegang (access) tot de leden die in het gedeelte achter de access specifier in de klasse staan. Voorbeeld: class X { private: int a:
// enkel toegankelijk voor functies van de eigen // klasse X
protected: int b;
// toegankelijk voor functies van de eigen klasse X // en voor ‘friends’
public: int c;
// toegankelijk voor alle functies
}; Daarbij wordt de toegang tot een lid niet enkel bepaald door de access specifier alleen, maar ook door de plaats van waaruit dat lid benaderd wordt. Tenslotte kan een klasse ofwel privaat, ofwel publiek afgeleid worden. Dit verschil in afleiding manifesteert zich ook in de toegang tot de leden van de basisklasse.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
101
4.5.2 Toegangsregels van een privaat afgeleide klasse Het privaat afleiden van een klasse gebeurt als volgt: class Dprivaat : private B { ... }; met B de basisklasse en Dprivaat de privaat afgeleide klasse van B. In een privaat afgeleide klasse gelden de volgende toegangsregels voor de overgeërfde leden van de basisklasse: 1) Ontoegankelijke leden van de basisklasse blijven ontoegankelijk in de afgeleide klasse. ‘Ontoegankelijke leden’ slaat op het feit dat de basisklasse reeds afgeleid kan zijn van een andere klasse, met als gevolg dat bepaalde leden van de basisklasse niet meer toegankelijk zijn. Voor een root-klasse, dit is een klasse die geen ‘voorouders’ heeft, gelden slechts drie toegangsniveau’s: privaat, beschermd en publiek. Voor een afgeleide klasse worden de toegangsniveau’s uitgebreid met ‘ontoegankelijk’. 2) Private leden van de basisklasse zijn ontoegankelijk voor de afgeleide klasse. 3) Beschermde en publieke leden van de basisklasse worden privaat in de afgeleide klasse. Een privaat afgeleide klasse is een zeer veilige afleiding. Immers, alle leden van de basisklasse worden ofwel ontoegankelijk, ofwel privaat in de afgeleide klasse. Bemerk dat een afgeleide klasse default privaat afgeleid wordt.
4.5.3 Toegangsregels voor een publiek afgeleide klasse Het publiek afleiden van een klasse gebeurt als volgt: class Dpubliek : public B { ... } met B de basisklasse en Dpubliek de publiek afgeleide klasse van B. Voor een publiek afgeleide klasse gelden volgende regels: 1) Ontoegankelijke leden van de basisklasse blijven ontoegankelijk in de afgeleide klasse. 2) Private leden van de basisklasse zijn ontoegankelijk voor de afgeleide klasse. 3) Beschermde leden van de basisklasse blijven beschermd in de afgeleide klasse. 4) Publieke leden van de basisklasse blijven publiek in de afgeleide klasse. Een afgeleide klasse wordt meestal publiek afgeleid. Een publiek afgeleide klasse behoudt immers alle toegangsregels die gelden voor de basisklasse, uitgezonderd voor private basisleden.
102
4.5.4 Voorbeeld class X { // Elke lidfunctie van een klasse heeft toegang tot alle andere leden // van dezelfde klasse, ongeacht de access specifier. private: int a; void func_a(); protected: int b; void func_b(); public: int c; void func_c(); }; class Y : public X { // Lidfuncties van Y hebben toegang tot de protected en public leden van X, // dus toegang tot b, func_b, c en func_c. ... }; class Z : private X { // Lidfuncties van Z hebben geen toegang tot de protected en // public leden van X. ... }; Van buitenaf zijn enkel de publieke leden c en func_c toegankelijk. Voorbeeld: void main() { X objx; cout<
// kan niet, want a is privaat
objx.func_a();
// kan niet, want func_a() is privaat
cout<
// kan niet, want b is beschermd (protected)
objx.func_b();
// kan niet, want func_b() is beschermd
cout<
// kan wel
objx.func_c();
// kan wel
} Een klasse die is afgeleid met de access specifier private (class Z : private X) erft wel alle leden van de basisklasse, doch heeft enkel toegang tot de publieke leden van de basisklasse. Een privaat afgeleide klasse heeft dus geen speciale privileges met betrekking tot de toegang.
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
4.5.6 Data hiding Een belangrijk principe in OOP is data hiding. Dit is het afschermen van de gegevens van een klasse. Het afschermen gebeurt met de access specifier ‘private’. Enkele overwegingen: 1) Met afgeleide klassen kan het zinvol zijn om bepaalde gegevens uit de basisklasse ‘protected’ te maken. Het zijn die gegevens die gebruikt worden in één of meer lidfuncties van de afgeleide klasse. 2) Data-elementen (data-leden) worden zelden ‘public’ gedeclareerd, omdat daarmee het principe van data hiding te veel wordt aangetast. 3) Veel lidfuncties zijn public gedeclareerd, doch in bepaalde gevallen is dat niet wenselijk. Functies die een helpende rol spelen bij het werk van andere lidfuncties, mogen van buitenaf niet kunnen worden aangeroepen om fouten te vermijden. Dergelijke help-functies worden het best ‘private’ of ‘protected’ gedeclareerd, zodat ze niet van buitenaf kunnen worden aangeroepen. 4) Als een klasse wel lidfuncties heeft, maar geen enkele afgeleide klasse, moet er tenminste één functie ‘public’ zijn, omdat anders geen enkele functie van de klasse kan worden aangewend.
4.5.7 Invloed van overerving op de bescherming van leden van een klasse Bij de overerving van een basisklasse naar een afgeleide klasse, kan de afgeleide klasse aangeven wat er moet gebeuren met de public, protected en private gedeelten van de klasse. Door het opgeven van de keywords private, protected en public vóór de naam van de basisklasse, geeft de afgeschermde klasse aan wat de minimale afscherming van de klasse-leden moet zijn. •
public overerving: alle public leden blijven public, de protected leden blijven protected en de private leden zijn voor de lidfuncties van de afgeleide klasse
104
toegankelijk. Public overerving is de enige vorm van overerving die leidt tot een ‘is een’ relatie. •
protected overerving: alle public leden worden protected, de protected leden blijven protected en de private leden zijn niet voor de leden van de afgeleide klasse toegankelijk.
•
private overerving: alle public leden worden privaat, de protected leden worden privaat en de private leden zijn voor de leden van de afgeleide klasse niet toegankelijk.
Overzicht: overerving
public
protected
private
leden
leden
leden
public
protected
private
protected
protected
private
private
private
private
public overerving protected overerving private overerving
4.5.8 Een ‘is een’ – relatie Overerving is een manier om een ‘is een’- relatie of een ‘gelijkend op’- relatie uit te drukken. Zo kunnen we zeggen: “Een krokodil is een reptiel”. Een krokodil heeft eigenschappen die gemeenschappelijk zijn aan alle reptielen. Een krokodil heeft die eigenschappen geërfd van de reptielen. Andere voorbeelden: een rechthoek is een veelhoek, een mus is een vogel, een auto is een voertuig. Een veel gemaakte fout is dat men een afgeleide klasse wil construeren om een ‘deel van’ - relatie of een ‘heeft een’- relatie uit te drukken. Een krokodil is duidelijk geen ‘deel van’ een reptiel. Als een ‘heeft een’ – relatie tussen twee objecten vereist is, dan moet het ene object als attribuut van het andere object worden gespecificeerd.
4.5.9 Beschermende interface via een afgeleide klasse Door van een basisklasse een afgeleide klasse te maken, die zelf geen extra dataleden toevoegt, kan op een eenvoudige wijze een beschermend omhulsel omheen de basisklasse worden gelegd. Zo’n omhulsel kan de lidfuncties van de basisklasse verbergen. Voorbeeld: De basisklasse Rekening biedt de mogelijkheid een bedrag op een rekening te storten en/of een bedrag op te nemen. Omdat de bankwereld eist dat elke transactie moet worden vastgelegd, is het niet voldoende de basisklasse als volgt te definiëren:
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
105
class Rekening { private: long rekeningnummer; ... protected: float saldo; ... }; en vervolgens afgeleide klassen te creëren met interfaces voor het storten en opnemen van geld. Immers, op die manier wordt de verantwoordelijkheid voor de transactieregistratie gelegd bij de afgeleide klassen en het kan best dat de maker van de afgeleide klassen, die niet noodzakelijker wijze de maker is van de basisklasse, vergeet deze registratie te implementeren. Een mogelijke oplossing is: class Rekening { private: long rekeningnummer; float saldo; protected: Rekening(long reknr, float start= 0) : saldo(start), rekeningnummer(reknr) {} public: void storten(float bedrag) { saldo += (bedrag > 0 ? bedrag : 0); // log de transactie } void opname(float bedrag) { if (saldo > bedrag) { saldo -= (bedrag > 0 ? bedrag : 0); } // log de transactie } float haalsaldo() const { // log info-verzoek } }; De registratie is nu een onderdeel van de klasse Rekening. Echter, niemand kan de klasse Rekening gebruiken omdat de constructor protected is. Enkel afgeleide klassen kunnen wél de constructor gebruiken.
106
Een voorbeeld van afgeleide klasse van de klasse Rekening is de hypotheeklening (class HypoRekening). Op deze rekening mag enkel kunnen worden gestort, nooit van opgenomen. Het opnemen gebeurt slechts éénmalig via de constructor van de afgeleide klasse HypoRekening. Om het onmogelijk te maken de basisklasse Rekening te gebruiken zonder transactielogging, volstaat het de klasse HypoRekening privaat af te leiden van de basisklasse Rekening. class HypoRekening : private Rekening { private: float stortingskosten(float f) { ... } void storten(float bedrag) { float x= stortingskosten(bedrag); Rekening::storten(bedrag – x); } public: HypoRekening(long rek, float start) : Rekening(rek, - start) {} void aflossing(float bedrag) { storten(bedrag); } Rekening :: getsaldo; };
4.6 Meer dan één afgeleide klasse 4.6.1 Schematische voorstelling Uit een basisklasse kan meer dan één afgeleide klasse worden gemaakt. Zo kan bijvoorbeeld uit de basisklasse Persoon de afgeleide klassen Student en Werknemer worden gemaakt. Dit kan schematisch als volgt worden voorgesteld: Persoon
Student
Werknemer
De pijl betekent: ‘is afgeleid van’. De klasse Student is afgeleid van de klasse Persoon, en ook de klasse Werknemer is afgeleid van de klasse Persoon. T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
107
De schematische voorstelling geeft volgende reële situatie weer: een student is een persoon en ook een werknemer is een persoon.
4.6.2 Waar komt wat? In de basisklasse komen de data-elementen en de lidfuncties die gemeenschappelijk zijn voor studenten als voor werknemers, zoals naam, adres, geboortedatum en een print-functie om die gegevens op het scherm af te drukken. In de basisklasse komen dus de globale, algemene kenmerken. In de afgeleide klasse komen de meer specifieke kenmerken. Een student is bijvoorbeeld ingeschreven in een bepaalde school, en een werknemer is ingeschreven in een welbepaald bedrijf.
4.6.3 Programmavoorbeeld In het volgende programma worden de klasse Student en Werknemer afgeleid uit de basisklasse Persoon. Het algemeen kenmerk is ‘naam’ en de specifieke kenmerken zijn ‘school’ (Student) en ‘bedrijf’ (Werknemer). /* programma P166.cpp */ #include class Persoon { private: char naam[30]; public: Persoon(); void print; }; class Student : private Persoon { private: char school[30]; public: Student(); void print(); }; class Werknemer : private Persoon { private: char bedrijf[30]; public: Werknemer(); void print(); };
108
Persoon :: Persoon() { cout<<”Naam: “; cin>>naam; } void Persoon :: print() { cout<>school; } void Student :: print() { Persoon :: print(); cout<<” is student van “<<school<<endl; } Werknemer :: Werknemer() { cout<<”Bedrijf: “; cin>>bedrijf; cin.get(); } void Werknemer :: print() { Persoon :: print(); cout<<” is werknemer van “<
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
109
2) De klassen Student en Werknemer zijn privaat afgeleid van de basisklasse Persoon. Dit betekent dat het data-lid ‘naam’ van Persoon niet rechtstreeks toegankelijk is vanuit de afgeleide klassen Student en Werknemer, doch enkel via de publieke lidfuncties Persoon() en print() van de klasse Persoon. In het algemeen is het een goede strategie om zoveel mogelijk leden en afleidingen privaat te maken. Op die manier blijft het principe van data hiding het best gewaarborgd. 3) De drie klassen hebben elk hun eigen print-functie. Dit is een typisch voorbeeld van overriding. In de print-functie van de afgeleide klassen wordt gebruik gemaakt van de print-functie van de basisklasse. Dit gebeurt door de naam van de basisklasse te plaatsen, gevolgd door de scope-operator en de naam van de functie, hier print(), en wel als volgt: Persoon :: print(); . 4) In de constructor van Student en van Werknemer wordt niet naar een naam gevraagd. Toch moet wel degelijk een naam worden ingevoerd voor Student en ook voor Werknemer. Dit komt omdat bij de declaratie van een object van een afgeleide klasse, bijvoorbeeld Student S, eerst automatisch de default-constructor van de basisklasse wordt aangeroepen, in het voorbeeld Persoon(). Eigenlijk ligt dat gebeuren wel voor de hand aangezien de afgeleide klasse voortkomt uit de basisklasse. Dus, pas als de constructor van de basisklasse zijn werk gedaan heeft, wordt de body van de constructor van de afgeleide klasse uitgevoerd.
4.6.4 Constructoren en afgeleide klassen Bij het maken van objecten van een afgeleide klasse geldt in C++ de basisregel dat eerst automatisch de default-constructor van de basisklasse wordt aangeroepen, voordat de passende constructor van de afgeleide klasse wordt uitgevoerd. Echter kan met die basisregel niet alle gevallen correct worden beschreven, zoals in volgende twee situaties: 1) Wat gebeurt er als de basisklasse geen default-constructor heeft? Als de basisklasse geen enkele constructor heeft, maakt de compiler een default-constructor aan, die dan een object van de basisklasse maakt en verder niets. Als de basisklasse wel één of meer constructoren heeft, maar geen defaultconstructor, treedt er een foutmelding op. Die fout kan worden opgeheven door ofwel een constructor zonder argumenten toe te voegen aan de basisklasse, ofwel door alle argumenten van een bestaande constructor in de basisklasse een default-waarde te geven, zodat een default-constructor ontstaat. 2) In een aantal situaties wordt de default-constructor van de basisklasse niet eerst aangeroepen. Dit gebeurt wanneer de constructor van de afgeleide klasse via een initializer expliciet een constructor, niet de default constructor, van de basisklasse aanroept.
110
4.7 Afgeleide klasse van een afgeleide klasse 4.7.1 Schematische voorstelling Het is mogelijk een klasse af te leiden van een klasse, die zelf een afgeleide klasse is. Zo kan bijvoorbeeld de klasse Vierkant worden afgeleid van de klasse Rechthoek, die zelf afgeleid is van de klasse Veelhoek. Dit kan schematisch als volgt worden voorgesteld: Veelhoek
Rechthoek
Vierkant
De richting van de pijl loopt van de afgeleide klasse naar de basisklasse. De klasse Vierkant is een afgeleide klasse van Rechthoek, terwijl Rechthoek zelf een afgeleide klasse is van Veelhoek. Immers, een vierkant is een speciale rechthoek, namelijk een rechthoek met gelijke lengte en breedte, en een rechthoek is een speciale veelhoek, namelijk een met vier rechte hoeken. Men gebruikt volgende terminologie: De klasse Rechthoek is een directe basisklasse van Vierkant. Zo ook is de klasse Veelhoek een directe basisklasse van Rechthoek. De klasse Veelhoek daarentegen is een indirecte basisklasse van Vierkant.
4.7.2 Programmavoorbeeld Het volgende programma maakt gebruik van de klasse Veelhoek, van de klasse Rechthoek, die is afgeleid van Veelhoek, en van de klasse Vierkant, die is afgeleid van Rechthoek. /* programma P167.cpp */ #include class Veelhoek { private: int nthoeken; public: Veelhoek(int nthoek) { nthoeken= nthoek; } T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
111
void print() { cout<<”Aantal hoeken is “<
112
3) De uitvoer levert op: “Aantal hoeken is 4”. Vierkant heeft Rechthoek als basisklasse en Rechthoek heeft Veelhoek als basisklasse. Ook nu geldt de vuistregel dat automatisch eerst de defaultconstructor van de basisklasse wordt aangeroepen, te beginnen met de bovenste klasse, hier Veelhoek. Hoewel Veelhoek zelf geen default-constructor heeft, leidt het aanroepen van Veelhoek niet tot een foutmelding. Dit komt doordat Rechthoek via een initializer de constructor met een argument van de basisklasse Veelhoek aanroept, waardoor het aantal hoeken op vier wordt gezet, namelijk: Rechthoek(int lt= 0, int br= 0) : Veelhoek(4) { lengte= lt; breedte= br; } 4) Bij de declaratie
Vierkant V(20);
zoekt de compiler naar een geschikte constructor van de klasse Vierkant en vindt die met Vierkant(int zijde) { lengte= zijde; breedte= zijde; } Omdat Vierkant een afgeleide klasse is, wordt deze constructor nog niet direct uitgevoerd, maar gaat de compiler nog eerst op zoek naar de defaultconstructor van de directe basisklasse, hier Rechthoek. En Rechthoek heeft een constructor met enkel default-argumenten (= defaultconstructor): Rechthoek(int lt= 0, int br= 0) : Veelhoek(4) { lengte= lt; breedte= br; } Ook deze constructor zou nog niet worden uitgevoerd omdat ook Rechthoek een afgeleide klasse is. Normaal gaat de compiler nu op zoek naar een defaultconstructor van de basisklasse Veelhoek, doch nu gebeurt dat niet omdat hier een andere constructor van Veelhoek direct wordt aangeroepen. Dus wordt hier volgende constructor wel direct uitgevoerd: Rechthoek(int lt= 0, int br= 0) : Veelhoek(4) { lengte= lt; breedte= br; } 5) De constructoren worden uitgevoerd in omgekeerde volgorde van aanroepen: -
eerst Veelhoek(4): deze zet het aantal hoeken op vier
-
dan Rechthoek(0, 0): deze zet lengte= 0 en breedte= 0
-
tenslotte Vierkant(20): deze zet lengte= 20 en breedte= 20
T:\Modules\SYLLABUS\Programmeur X51\Programmeren in C++\020819 Programmeren in C++.doc
113
6) Om te vermijden dat via Rechthoek(0, 0) eerst lengte en breedte op nul worden gezet, kan in de constructor van Vierkant via een initializer de constructor van Rechthoek worden opgeroepen met de jusite waarde voor lengte en breedte. De constructor van Vierkant wordt dan: Vierkant(int zijde) : Rechthoek(zijde, zijde) {} De body van deze constructor is leeg omdat het volledige werk al gedaan is door de aanroep van Rechthoek(zijde, zijde). Wel is een body verplicht, maar die mag leeg zijn. Merk op: In de definitie van een constructor kan enkel een initializer voor een directe basisklasse worden opgenomen. In de constructor Vierkant kan Rechthoek worden aangeroepen omdat Rechthoek de directe basisklasse is van Vierkant. Echter kan in de constructor Vierkant met een initializer de constructor van Veelhoek niet worden aangeroepen, omdat Veelhoek niet een directe basisklasse is van Vierkant.
4.8 Meervoudige overerving (multiple inheritance) 4.8.1 Algemeenheden Meervoudige overerving betekent dat een klasse eigenschappen erft van meer dan één basisklasse, met andere woorden, een klasse wordt afgeleid uit twee of meer basisklassen. Meervoudige overerving wordt ook aangeduid met ‘meervoudige basisklasse’. Een meervoudige basisklasse is een klasse die erft van meerdere basisklassen. Een meervoudige basisklasse bevat zowel karakteristieken van de ene basisklasse als van de andere basisklasse. Men kan zeggen dat een klasse die erft van meerdere basisklassen, een meervoudige ‘is een’- relatie onderhoudt met de basisklassen. We kunnen de meervoudig afgeleide klasse gebruiken daar waar één van zijn basisklassen verwacht wordt. Voorbeeld: Een kat is enerzijds een huisdier en anderzijds een katachtige. Zowel huisdieren als katachtigen zijn zoogdieren. Dit komt overeen met volgende erfelijkheidsrelatie: Zoogdier
Katachtige
Huisdier
Kat
114
Meervoudige overerving wordt enkel gebruikt waar het werkelijk verantwoord is, omdat meervoudige overerving gemakkelijk leidt tot ingewikkelde erfelijkheidsrelaties met het gevaar van het creëren van contradicties in de ‘stamboom’ van klassen.
4.8.2 Programmavoorbeeld In volgend programma is de klasse X meervoudig afgeleid van de klassen A en B. Schematisch kan dit als volgt worden voorgesteld: