21.09.2013 Views

Dictaat - Harry Broeders - De Haagse Hogeschool

Dictaat - Harry Broeders - De Haagse Hogeschool

Dictaat - Harry Broeders - De Haagse Hogeschool

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

Inhoudsopgave.<br />

Inleiding. ..................................................................... 3<br />

Een terugblik op C. .............................................................. 3<br />

1 Inleiding van C naar C++. .................................................... 6<br />

1.1 Commentaar met //. .................................................... 6<br />

1.2 Plaats van variabelen definities. ............................................ 6<br />

1.3 Constante waarden met const. ............................................ 7<br />

1.4 Het type bool. ........................................................ 7<br />

1.5 Standaard include files. ................................................. 7<br />

1.6 Input en output met >. ............................................ 8<br />

1.7 Het type string. ....................................................... 9<br />

1.8 Het type vector. ..................................................... 10<br />

1.9 Function name overloading. ............................................. 10<br />

1.10 <strong>De</strong>fault parameters. ................................................... 11<br />

1.11 Naam van struct. ..................................................... 11<br />

1.12 C++ als een betere C. .................................................. 12<br />

2 Objects and classes. ....................................................... 14<br />

2.1 Object Oriented <strong>De</strong>sign (OOD) en Object Oriented Programming (OOP). ............. 14<br />

2.2 ADT’s (Abstract Data Types). ........................................... 17<br />

2.3 Voorbeeld class Breuk (eerste versie). ...................................... 20<br />

2.4 Constructor Breuk. ................................................... 22<br />

2.5 Constructors en type conversies. ......................................... 23<br />

2.6 Initialisation list van de constructor. ........................................ 24<br />

2.7 <strong>De</strong>fault copy constructor. .............................................. 24<br />

2.8 <strong>De</strong>fault assignment operator. ............................................ 24<br />

2.9 const memberfuncties. ................................................. 25<br />

2.10 Class invariant. ...................................................... 26<br />

2.11 Voorbeeld class Breuk (tweede versie). ..................................... 26<br />

2.12 Operator overloading. ................................................. 27<br />

2.13 this pointer. ........................................................ 29<br />

2.14 Reference variabelen. .................................................. 29<br />

2.15 Reference parameters. ................................................. 30<br />

2.16 const reference parameters. ............................................. 31<br />

2.17 Parameter FAQ. ..................................................... 31<br />

2.18 Reference return type. ................................................. 32<br />

2.19 Reference return type (deel 2). ........................................... 33<br />

2.20 Operator overloading (deel2). ............................................ 33<br />

2.21 operator+ FAQ. ...................................................... 34<br />

2.22 Operator overloading (deel 3). ............................................ 35<br />

2.23 Overloaden operator++ en operator--. ...................................... 36<br />

2.24 Conversie operatoren. ................................................. 37<br />

2.25 Voorbeeld class Breuk (derde versie). ...................................... 37<br />

2.26 friend functions. ..................................................... 39<br />

2.27 Operator overloading (deel 4). ............................................ 40<br />

2.28 Voorbeeld separate compilation van class MemoryCell. .......................... 41<br />

3 Templates. .............................................................. 42<br />

3.1 Template functies. .................................................... 42<br />

3.2 Template classes. .................................................... 45<br />

3.3 Voorbeeld template class Dozijn. .......................................... 46<br />

3.4 Template details. ..................................................... 48<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

1


2<br />

3.5 Standaard Templates. ................................................. 48<br />

3.6 std::vector. ......................................................... 48<br />

4 Inheritance. ............................................................. 50<br />

4.1 <strong>De</strong> syntax van inheritance. .............................................. 51<br />

4.2 Polymorfisme. ...................................................... 52<br />

4.3 Memberfunctie overriding. .............................................. 53<br />

4.4 Abstract base class. .................................................. 55<br />

4.5 Constructors bij inheritance. ............................................. 55<br />

4.6 protected members. .................................................. 55<br />

4.7 Voorbeeld: ADC kaarten. ............................................... 56<br />

4.7.1 Probleemdefinitie. ............................................... 56<br />

4.7.2 Een gestructureerde oplossing. ...................................... 56<br />

4.7.3 Een oplossing door middel van een ADT. ............................... 58<br />

4.7.4 Een object georiënteerde oplossing. ................................... 60<br />

4.7.5 Een kaart toevoegen. ............................................. 63<br />

4.8 Overloading en overriding van memberfuncties. ............................... 64<br />

4.9 Slicing problem. ..................................................... 67<br />

4.10 Voorbeeld: Opslaan van polymorfe objecten in een vector. ........................ 68<br />

4.11 Voorbeeld: Impedantie calculator. ......................................... 70<br />

4.11.1Weerstand, spoel en condensator. .................................... 70<br />

4.11.2Serie- en parallelschakeling. ........................................ 72<br />

4.11.3Een grafische impedantie calculator. .................................. 74<br />

4.12 Inheritance details. ................................................... 74<br />

5 Dynamic memory allocation en destructors. ...................................... 74<br />

5.1 Dynamische geheugen allocatie (new en delete). ............................... 74<br />

5.2 <strong>De</strong>structor ~Breuk. ................................................... 75<br />

5.3 <strong>De</strong>structors bij inheritance. ............................................. 77<br />

5.4 Virtual destructor. .................................................... 77<br />

5.5 Voorbeeld class Array. ................................................ 79<br />

5.6 explicit constructor. .................................................. 81<br />

5.7 Copy constructor en default copy constructor. ............................... 81<br />

5.8 Overloading operator=. ................................................ 83<br />

5.9 Wanneer moet je zelf een destructor, copy constructor en operator= definiëren. ........ 84<br />

5.10 Voorbeeld template class Array. .......................................... 84<br />

6 Losse flodders. .......................................................... 86<br />

6.1 static class members. ................................................. 86<br />

6.2 Constante pointers met const. ........................................... 87<br />

6.2.1 const * ....................................................... 88<br />

6.2.2 * const ....................................................... 88<br />

6.2.3 const * const .................................................. 88<br />

6.3 Constanten in een class. ................................................ 88<br />

6.4 inline memberfuncties. ................................................. 89<br />

6.5 Namespaces. ....................................................... 90<br />

6.6 Exceptions. ........................................................ 92<br />

6.7 Casting en runtime type information. ....................................... 99<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


Inleiding.<br />

Dit is het dictaat: "Object Georiënteerd Programmeren in C++". Dit dictaat kan zonder boek gebruikt<br />

worden. Als je meer achtergrondinformatie of diepgang zoekt kun je gebruik maken van het boek:<br />

”Thinking in C++ 2nd Edition, Volume 1" van Bruce Eckel (2000 Pearson Education). Dit dictaat is<br />

zoals alle mensenwerk niet foutloos, verbeteringen en suggesties zijn altijd welkom!<br />

Halverwege de jaren '70 werd steeds duidelijker dat de veel gebruikte software ontwikkelmethode<br />

structured design (ook wel functionele decompositie genoemd) niet geschikt is om grote uitbreidbare en<br />

onderhoudbare software systemen te ontwikkelen. Ook bleken de "onderdelen" van een applicatie die<br />

met deze methode is ontwikkeld, meestal niet herbruikbaar in een andere applicatie. Het heeft tot het<br />

begin van de jaren '90 geduurd voordat een alternatief voor structured design het zogenaamde, object<br />

oriented design (OOD), echt doorbrak. Object georiënteerde programmeertalen bestaan al sinds het<br />

begin van de jaren '70. <strong>De</strong>ze manier van ontwerpen (OOD) en programmeren (OOP) is echter pas in het<br />

begin van de jaren '90 populair geworden nadat in het midden van de jaren '80 de programmeertaal C++<br />

door Bjarne Stroustrup was ontwikkeld. <strong>De</strong>ze taal voegt taalconstructies toe aan de op dat moment in de<br />

praktijk meest gebruikte programmeertaal C. <strong>De</strong>ze object georiënteerde versie van C heeft de naam C++<br />

gekregen en heeft zich in korte tijd (de eerste release van Borland C++ was in 1990 en de eerste release<br />

van Microsoft C++ was in 1992) ontwikkeld tot één van de meest gebruikte programmeertalen van dit<br />

moment. C++ is echter geen pure OO taal (zoals bijvoorbeeld smalltalk) en kan ook gebruikt worden als<br />

procedurele programmeertaal. Dit heeft als voordeel dat de overstap van C naar C++ eenvoudig te<br />

maken is maar heeft als nadeel dat C++ gebruikt kan worden als een soort geavanceerd C zonder gebruik<br />

te maken van OOP.<br />

Een terugblik op C.<br />

We starten deze onderwijseenheid met met de overgang van C naar C++. Ik ga er van uit dat je de taal<br />

C zoals behandeld in de propedeuse beheerst. Misschien is het nodig om deze kennis op te frissen.<br />

Vandaar dat dit dictaat begint met een terugblik op C (wordt verder in de les niet behandeld). We zullen<br />

dit doen aan de hand van een voorbeeldprogramma dat een lijst met gewerkte tijden (in uren en minuten)<br />

inleest vanaf het toetsenbord en de totaal gewerkte tijd (in uren en minuten) bepaalt en afdrukt.<br />

#include <br />

struct Tijdsduur { /* Een Tijdsduur bestaat uit: */<br />

int uur; /* een aantal uren en */<br />

int min; /* een aantal minuten. */<br />

};<br />

/* <strong>De</strong>ze functie drukt een Tijdsduur af */<br />

void drukaf(struct Tijdsduur td) {<br />

if (td.uur==0)<br />

printf(" %2d minuten\n", td.min);<br />

else<br />

printf("%3d uur en %2d minuten\n", td.uur, td.min);<br />

}<br />

/* <strong>De</strong>ze functie drukt een rij met n gewerkte tijden af */<br />

void drukafRij(struct Tijdsduur trij[], int n) {<br />

int teller;<br />

for (teller=0;teller


4<br />

}<br />

int teller;<br />

struct Tijdsduur s;<br />

s.uur=s.min=0;<br />

for (teller=0;teller


som definieert de lokale variabele s van het type struct Tijdsduur om de som van de<br />

rij gewerkte tijden te berekenen. <strong>De</strong> twee datavelden van de lokale variabele s worden eerst<br />

gelijk gemaakt aan nul en vervolgens worden de gewerkte tijden uit trij hier één voor één<br />

bij opgeteld. Twee gewerkte tijden worden bij elkaar opgeteld door de uren bij elkaar op te<br />

tellen en ook de minuten bij elkaar op te tellen. Als alle gewerkte tijden op deze manier zijn<br />

opgeteld, kan de waarde van s.min groter dan 59 zijn geworden. Om deze reden wordt de<br />

waarde van s.min/60 opgeteld bij s.uur. <strong>De</strong> waarde van s.min moet dan gelijk worden<br />

aan de resterende minuten. Het aantal resterende minuten kunnen we bereken met<br />

s.min=s.min%60. Of in verkorte notatie s.min%=60. Het return type van de functie<br />

som is van het type struct Tijdsduur. Aan het einde van de functie som wordt de<br />

waarde van de lokale variabele s teruggegeven (return s).<br />

• Vervolgens wordt met de preprocessor directive #define de constante MAX gedefinieerd met als<br />

waarde 5.<br />

• Tot slot wordt de hoofdfunctie main gedefinieerd. <strong>De</strong>ze functie zal bij het starten van het programma<br />

aangeroepen worden. In de functie main wordt een array rij aangemaakt met MAX<br />

elementen van het type struct Tijdsduur. Tevens wordt de integer variabele aantal<br />

gebruikt om het aantal ingelezen gewerkte tijden te tellen. Elke gewerkte tijd bestaat uit twee<br />

integers: een aantal uren en een aantal minuten. In de do while lus worden gewerkte tijden<br />

vanaf het toetsenbord ingelezen in de array rij. <strong>De</strong> getallen worden ingelezen met de standaard<br />

leesfunctie scanf. <strong>De</strong>ze functie bevindt zich in de standaard C library en het prototype van de<br />

functie is gedeclareerd in de headerfile stdio.h. <strong>De</strong> eerste parameter van scanf specificeert<br />

wat er ingelezen moet worden. In dit geval twee integer getallen. <strong>De</strong> volgende parameters specificeren<br />

de adressen van de variabelen waar de ingelezen integer getallen moeten worden opgeslagen.<br />

Met de operator & wordt het adres van een variabele opgevraagd. <strong>De</strong> functie scanf geeft<br />

een integer terug die aangeeft hoeveel velden ingelezen zijn. <strong>De</strong>ze return waarde wordt opgeslagen<br />

in de variabele gelezen. <strong>De</strong> conditie van de do while lus is zodanig ontworpen dat de do<br />

while lus uitgevoerd wordt zo lang de return waarde van scanf gelijk is aan 2 én ++aantal


6<br />

Het volgen van deze onderwijseenheid zonder voldoende kennis en begrip van C is vergelijkbaar met het<br />

bouwen van een huis op drijfzand!<br />

1 Inleiding van C naar C++.<br />

<strong>De</strong> ontwerper van C++ Bjarne Stroustrup geeft op de vraag:"Wat is C++?" het volgende antwoord 1 :<br />

"C++ is a general-purpose programming language with a bias towards systems programming that<br />

• is a better C,<br />

• supports data abstraction,<br />

• supports object-oriented programming, and<br />

• supports generic programming."<br />

In dit hoofdstuk bespreken we eerst enkele kleine verbeteringen van C++ ten opzichte van zijn voorganger<br />

C. Grotere verbeteringen zoals data abstractie, object georiënteerd programmeren en generiek<br />

programmeren komen in de volgende hoofdstukken uitgebreid aan de orde. C++ is een zeer uitgebreide<br />

taal en dit onderwijsdeel moet dan ook zeker niet gezien worden als een cursus C++. Wij behandelen<br />

slechts de meest belangrijke delen van C++. <strong>De</strong> verbeteringen die in dit hoofdstuk worden besproken<br />

zijn slechts kleine verbeteringen. <strong>De</strong> meeste worden in de les niet behandeld.<br />

1.1 Commentaar met //.<br />

<strong>De</strong> eerste uitbreiding die we bespreken is niet erg ingewikkeld maar wel erg handig. Je bent gewend om<br />

in C programma's commentaar te beginnen met /* en te eindigen met */. In C++ kun je naast de oude<br />

methode ook commentaar beginnen met // dit commentaar eindigt dan aan het einde van de regel. Dit is<br />

handig (minder typewerk) als je commentaar wilt toevoegen aan een programmaregel.<br />

1.2 Plaats van variabelen definities. (Zie eventueel TICPP 2 chapter03.html#Heading126.)<br />

Je bent gewend om in C programma’s, variabelen te definiëren aan het begin van een blok (meteen<br />

na { ). In C++ kun je een variabele overal in een blok definiëren. <strong>De</strong> scope van de variabele loopt van<br />

het punt van definitie tot het einde van het blok waarin hij gedefinieerd is. Het is zelf mogelijk om de<br />

besturingsvariabele van een for lus in het for statement zelf te definiëren 3 . In C++ is het gebruikelijk<br />

om een variabele pas te definiëren als de variabele nodig is en deze variabele dan meteen te initialiseren.<br />

Dit maakt het programma beter te lezen (je hoeft niet als een jojo op en neer te springen) en de kans dat<br />

je een variabele vergeet te initialiseren wordt kleiner.<br />

Voorbeeld van het definiëren en initialiseren van een lokale variabele in een for loop:<br />

/* Hier bestaat i nog niet */<br />

for (int i=0; i


1.3 Constante waarden met const. (Zie eventueel TICPP Chapter03.html#Heading134)<br />

Je bent gewend om in C programma's symbolische constanten te definiëren met de preprocessor directive<br />

#define. Het is in ANSI C 4 en in C++ ook mogelijk om constanten te definiëren met behulp van<br />

een const qualifier gebruiken 5 .<br />

Voorbeeld met #define:<br />

#define aantalRegels 80<br />

Hetzelfde voorbeeld met const:<br />

const int aantalRegels=80;<br />

Omdat je bij een const qualifier het type moet opgeven kan de compiler meteen controleren of de<br />

initialisatie klopt met dit opgegeven type. Bij het gebruik van de preprocessor directive #define blijkt<br />

dit pas bij het gebruik van de constante en niet bij de definitie. <strong>De</strong> fout is dan vaak moeilijk te vinden. In<br />

een volgend hoofdstuk (blz. 25) zul je zien dat het met een const qualifier ook mogelijk wordt om<br />

constanten te definiëren van zelfgemaakte types.<br />

Een constante moet je initialiseren:<br />

const int k; // Error: Constant variable 'k' must be initialized<br />

Een constante mag je (vanzelfsprekend) niet veranderen:<br />

aantalRegels=79; // Error: Cannot modify a const object<br />

1.4 Het type bool. (Zie eventueel TICPP Chapter03.html#Heading119.)<br />

C++ kent in tegenstelling tot C 6 het keyword bool. Met dit keyword kun je booleanse variabelen<br />

definiëren van het type bool. Een variabele van dit type heeft slechts twee mogelijke waarden: true<br />

en false. In C89 (en ook in oudere versies van C++) wordt voor een booleanse variabele het type int<br />

gebruikt met daarbij de afspraak dat een waarde 0 (nul) false en een waarde ongelijk aan nul true<br />

betekent. Om C++ zoveel mogelijk compatibel te houden met C wordt een bool indien nodig omgezet<br />

naar een int en vice versa. Het gebruik van het type bool maakt meteen duidelijk dat het om een<br />

booleanse variabele gaat.<br />

1.5 Standaard include files. (Zie eventueel TICPP Chapter02.html#Heading78 en<br />

Chapter02.html#Heading86)<br />

<strong>De</strong> ISO/ANSI standaard bevat ook een uitgebreide library. Als we de functies, types enz. uit deze<br />

standaard library willen gebruiken dan moeten we de definities van de betreffende functies, types, enz.<br />

4 Het gebruik van symbolische constanten maakt het programma beter leesbaar en onderhoudbaar.<br />

5 In ANSI C kun je de qualifier const ook gebruiken. Maar in het in de P fase bij elektrotechniek<br />

gebruikte boek “<strong>De</strong> taal C van PSD tot C programma” van Daryl McAllister maakt hier echter geen<br />

gebruik van.<br />

6 In ANSI C99 (de laatste versie van de C standaard) kun je ook het type bool gebruiken. Je moet dan<br />

wel de include file gebruiken, in C++ is bool een keyword en hoef je dus niets te<br />

includen.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

7


8<br />

in ons programma opnemen door de juiste header file te includen 7 . <strong>De</strong> C++ standaard header files<br />

eindigen niet zoals bij C op .h maar hebben helemaal geen extensie. Dus #include in<br />

plaatst van #include 8 . Dit heeft als voordeel dat (delen van) oude C programma's<br />

waarin C headerfiles gebruikt worden nog steeds met de nieuwe ANSI/ISO C++ compiler vertaald<br />

kunnen worden. Bij het schrijven van nieuwe programma's gebruiken we uiteraard de C++ include files.<br />

<strong>De</strong> standaard C++ library bevat ook alle functies, types enz. uit de standaard C library. <strong>De</strong> headerfiles<br />

die afkomstig zijn uit de “oude” C library beginnen in de C++ library allemaal met de letter c. Dus<br />

#include in plaats van #include .<br />

Om het mogelijk te maken dat de standaard library met andere (oude en nieuwe) libraries in één programma<br />

kan worden gebruikt zijn in ANSI/ISO C++ namespaces opgenomen. Als je een symbool uit de<br />

C++ standaard library wilt gebruiken moet je dit symbool laten voorafgaan door std::. Als je bijvoorbeeld<br />

de som van de sinus en de cosinus van de variabele x wilt berekenen in een C++ programma dan<br />

kun je dit als volgt doen:<br />

#include <br />

// ...<br />

y=std::sin(x)+std::cos(x);<br />

In plaats van elk symbool uit de C++ standaard library voorafgaan te laten gaan door std:: kun je ook<br />

na het inluden de volgende regel in het programma opnemen:<br />

using namespace std;<br />

<strong>De</strong> som van de sinus en de cosinus van de variabele x kun je dus in C++ ook als volgt berekenen:<br />

#include <br />

using namespace std;<br />

// ...<br />

y=sin(x)+cos(x);<br />

1.6 Input en output met > 9 . (Zie eventueel TICPP Chapter02.html#Heading90)<br />

Je bent gewend om in C programma's de functies uit de stdio bibliotheek te gebruiken voor input en<br />

output. <strong>De</strong> meest gebruikte functies zijn printf en scanf. <strong>De</strong>ze functies zijn echter niet "type veilig"<br />

omdat de inhoud van de als eerste argument meegegeven format string pas tijdens het uitvoeren van het<br />

programma verwerkt wordt. <strong>De</strong> compiler merkt het dus niet als de "type aanduidingen" zoals %d die in<br />

de format string gebruikt zijn niet overeenkomen met de typen van de volgende argumenten. Tevens is<br />

het niet mogelijk om een eigen format type aanduiding aan de bestaande toe te voegen. Om deze redenen<br />

is in de ISO/ANSI C++ standaard naast de oude stdio bibliotheek (om compatibel te blijven) ook een<br />

nieuwe I/O library iostream opgenomen. Bij het ontwerpen van nieuwe software kun je het best van<br />

deze nieuwe library gebruik maken. <strong>De</strong> belangrijkste output faciliteiten van deze library zijn de stan-<br />

7 Headerfiles kunnen we op twee verschillende manieren includen: #include nu worden<br />

alleen de standaard include directories doorzocht om de include file naam te vinden en #include<br />

"naam" nu wordt eerst de huidige directory en pas daarna de standaard include directories doorzocht<br />

om naam te vinden.<br />

8 Headerfiles die je zelf maakt krijgen nog wel steeds de extensie .h.<br />

9 <strong>De</strong>ze paragraaf heb je al gelezen als je practicum opgave 1 al hebt gedaan.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


daard output stream cout (vergelijkbaar met stdout) en de bijbehorende > operator.<br />

Voorbeeld met stdio:<br />

#include <br />

// ...<br />

double d;<br />

scanf("%d",d); // deze regel bevat twee fouten!<br />

printf("d=%lf\n",d);<br />

Dit programmadeel bevat 2 fouten die niet door de compiler gesignaleerd worden en pas bij executie<br />

blijken.<br />

Hetzelfde voorbeeld met iostream:<br />

#include <br />

// ...<br />

double d;<br />

std::cin>>d; // lees d in vanaf het toetsenbord<br />

std::cout


10<br />

#include <br />

#include <br />

// ...<br />

std::string str;<br />

std::cin>>str;<br />

if (str=="Hallo") {<br />

// invoer is Hallo<br />

}<br />

1.8 Het type vector. (Zie eventueel TICPP Chapter02.html#Heading96.)<br />

In de ANSI/ISO C++ standaard library is ook het type vector opgenomen. Dit type is bedoeld om het<br />

oude type array [] te vervangen. In een volgend hoofdstuk komen we hier uitgebreid op terug.<br />

1.9 Function name overloading. (Zie eventueel TICPP Chapter07.html.)<br />

In C mag elke functienaam maar 1 keer gedefinieerd worden. Dit betekent dat twee functies om de<br />

absolute waarde te bepalen van variabelen van de types int en double een verschillende naam moeten<br />

hebben. In C++ mag een functienaam meerdere keren gedefinieerd worden (function name overloading).<br />

<strong>De</strong> compiler zal aan de hand van de gebruikte argumenten de juiste functie selecteren. Dit maakt deze<br />

functies eenvoudiger te gebruiken omdat de gebruiker (de programmeur die deze functies aanroept)<br />

slechts 1 naam hoeft te onthouden. 12 Dit is vergelijkbaar met het gebruik van ingebouwde operator + die<br />

zowel gebruikt kan worden om integers als om floating point getallen op te tellen.<br />

Voorbeeld zonder function name overloading:<br />

int abs_int(int i) {<br />

if (i


1.10 <strong>De</strong>fault parameters. (Zie eventueel TICPP Chapter07.html#Heading242.)<br />

Het is in C++ mogelijk om voor de laatste parameters van een functie default waarden te definiëren. Stel<br />

dat we een functie print geschreven hebben waarmee we een integer in elk gewenst talstelsel kunnen<br />

afdrukken. Het prototype van deze functie is als volgt:<br />

void print(int i, int talstelsel);<br />

Als we nu bijvoorbeeld de waarde 5 in het binaire (tweetallig) stelsel willen afdrukken dan kan dit als<br />

volgt:<br />

print(5, 2); // uitvoer: 101<br />

Als we de waarde 5 in het decimale (tientallig) stelsel willen afdrukken dan kan dit als volgt:<br />

print(5, 10); // uitvoer: 5<br />

In dit geval is het handig om de laatste parameter een default waarde mee te geven (in het prototype)<br />

zodat we niet steeds het talstelsel 10 hoeven op te geven als we een variabele in het decimale stelsel<br />

willen afdrukken.<br />

void print(int i, int talstelsel=10);<br />

Als de functie nu met maar één parameter wordt aangeroepen wordt als tweede parameter 10 gebruikt<br />

zodat het getal in het decimale talstelsel wordt afgedrukt. <strong>De</strong>ze functie kan als volgt worden gebruikt:<br />

print(5, 2); // uitvoer: 101<br />

print(5); // uitvoer: 5<br />

print(5, 10); // uitvoer: 5<br />

<strong>De</strong> default parameter maakt de print functie eenvoudiger te gebruiken omdat de gebruiker (de programmeur<br />

die deze functies aanroept) niet steeds de tweede parameter hoeft mee te geven als zij/hij<br />

getallen decimaal wil afdrukken. 13<br />

1.11 Naam van struct.<br />

In C is de naam van een struct géén typenaam. Als de struct Tijdsduur bijvoorbeeld als volgt<br />

gedefinieerd is:<br />

struct Tijdsduur { /* Een Tijdsduur bestaat uit: */<br />

int uur; /* een aantal uren en */<br />

int min; /* een aantal minuten. */<br />

};<br />

dan kunnen variabelen van het type struct Tijdsduur als volgt gedefinieerd worden:<br />

struct Tijdsduur td1;<br />

Het is in C gebruikelijk om met de volgende type definitie:<br />

typedef struct Tijdsduur TTijdsduur;<br />

13 Je kan echter ook zeggen dat default parameters het analyseren een programma moeilijker maakt<br />

omdat nu niet meer meteen duidelijk is welke waarde als tweede parameter wordt meegegeven. Om<br />

deze reden is het niet verstandig het gebruik van default parameters te overdrijven.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

11


12<br />

een typenaam (in dit geval TTijdsduur) te declareren voor het type struct Tijdsduur. Variabelen<br />

van dit type kunnen dan volgt gedefinieerd worden:<br />

TTijdsduur td2;<br />

In C++ is de naam van een struct meteen een typenaam. Je kunt variabelen van het type struct<br />

Tijdsduur dus eenvoudig als volgt definiëren:<br />

Tijdsduur td3;<br />

In practicum opgave 2 zul je zien dat C++ het mogelijk maakt om op een veel betere manier een tijdsduur<br />

te definiëren (als abstract data type).<br />

1.12 C++ als een betere C.<br />

Als we de verbeteringen die in C zijn doorgevoerd bij de definitie van C++ toepassen in het voorbeeld<br />

programma van blz. 3 dan ontstaat het volgende C++ programma:<br />

#include <br />

#include <br />

using namespace std;<br />

struct Tijdsduur { // Een Tijdsduur bestaat uit:<br />

int uur; // een aantal uren en<br />

int min; // een aantal minuten.<br />

};<br />

// <strong>De</strong>ze functie drukt een Tijdsduur af<br />

void drukaf(Tijdsduur td) {<br />

if (td.uur==0)<br />

cout


}<br />

int aantal=0;<br />

do {<br />

coutrij[aantal].uur>>rij[aantal].min;<br />

}<br />

while (cin && ++aantal


14<br />

2 Objects and classes.<br />

In dit hoofdstuk worden de belangrijkste taalconstructies die C++ biedt voor het programmeren van<br />

ADT’s (Abstract Data Types) besproken. Een ADT is een user-defined type dat voor een gebruiker niet<br />

te onderscheiden is van ingebouwde types (zoals int).<br />

2.1 Object Oriented <strong>De</strong>sign (OOD) en Object Oriented Programming (OOP).<br />

In deze paragraaf zullen we eerst ingaan op de achterliggende gedachten van OOD en OOP en pas<br />

daarna de implementatie in C++ bespreken.<br />

Een van de specifieke problemen bij het ontwerpen van software is dat het nooit echt af is. Door het<br />

gebruik van de software ontstaan bij de gebruikers weer ideeën voor uitbreidingen en/of veranderingen.<br />

Ook de veranderende omgeving van de software (bijvoorbeeld: operating system en hardware) zorgen<br />

ervoor dat software vaak uitgebreid en/of veranderd moet worden. Dit aanpassen van bestaande software<br />

wordt software maintenance genoemd. Er werken momenteel meer software engineers aan het onderhouden<br />

van bestaande software dan aan het ontwikkelen van nieuwe applicaties. Ook is het bij het ontwerpen<br />

van (grote) applicaties gebruikelijk dat de klant zijn specificaties halverwege het project aanpast. Je<br />

moet er bij het ontwerpen van software dus al op bedacht zijn dat deze software eenvoudig aangepast en<br />

uitgebreid kan worden (design for change).<br />

Bij het ontwerpen van hardware is het vanzelfsprekend om gebruik te maken van (vaak zeer complexe)<br />

componenten. <strong>De</strong>ze componenten zijn door anderen geproduceerd en je moet er dus voor betalen, maar<br />

dat is geen probleem want je kunt het zelf niet (of niet goedkoper) maken. Bij het gebruik zijn alleen de<br />

specificaties van de component van belang, de interne werking (implementatie) van de component is<br />

voor de gebruiker van de component niet van belang. Het opbouwen van hardware met bestaande<br />

componenten maakt het ontwerpen en testen eenvoudiger en goedkoper. Als bovendien voor standaard<br />

interfaces wordt gekozen kan de hardware ook aanpasbaar en uitbreidbaar zijn (denk bijvoorbeeld aan<br />

een PC met PCI bus). Bij het ontwerpen van software is het niet gebruikelijk om gebruik te maken van<br />

componenten die door anderen ontwikkeld zijn (en waarvoor je dus moet betalen). Vaak worden wel<br />

bestaande datastructuren en algoritmen gekopieerd maar die moeten vaak toch aangepast worden. Het<br />

gebruik van functie libraries is wel gebruikelijk maar dit zijn in feite eenvoudige componenten. Een<br />

voorbeeld van een complexe component zou bijvoorbeeld een editor (inclusief menu opties: openen,<br />

opslaan, opslaan als, print, printer set-up, bewerken (knippen, kopiëren, plakken), zoeken en zoek&vervang<br />

en inclusief een knoppenbalk) kunnen zijn. In alle applicaties waarbij je tekst moet kunnen invoeren<br />

kun je deze editor dan hergebruiken. Er zijn geen databoeken vol met software componenten die we<br />

kunnen kopen en waarmee we vervolgens zelf applicaties kunnen ontwikkelen. Dit heeft volgens mij<br />

verschillende redenen:<br />

• Voor een hardware component moet je betalen. Bij een software component vindt men dit niet<br />

normaal. Zo hoor je vaak zeggen: “Maar dat algoritme staat toch gewoon in het boek van ... dus<br />

dat kunnen we toch zelf wel implementeren.”<br />

• Als je van een bepaalde hardware component 1000 stuks gebruikt dan moet je er ook 1000 maal<br />

voor betalen. Bij een software component vindt men dit niet normaal en er valt eenvoudiger mee<br />

te sjoemelen.<br />

• Programmeurs denken vaak: maar dat kan ik zelf toch veel eenvoudiger en sneller programmeren.<br />

• <strong>De</strong> omgeving (taal, operating system, hardware enz.) van software is divers. Dit maakt het produceren<br />

van software componenten niet eenvoudig. Heeft u deze component ook in C++ voor Linux<br />

in plaats van in Visual Basic voor Windows? 14<br />

14 Er zijn de laatste jaren diverse alternatieven ontwikkeld voor het maken van taalonafhankelijke componenten.<br />

Microsoft heeft voor dit doel de .NET architectuur ontwikkeld. <strong>De</strong>ze .NET componenten<br />

kunnen in diverse talen gebruikt worden maar zijn wel gebonden aan het Windows platform. <strong>De</strong><br />

CORBA (Common Object Request Broker Architecture) standaard van de OMG (Object Management<br />

Group) maakt het ontwikkelen van taal en platform onafhankelijke componenten mogelijk.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


<strong>De</strong> object georiënteerde benadering is vooral bedoeld om het ontwikkelen van herbruikbare softwarecomponenten<br />

mogelijk te maken. In deze onderwijseenheid zul je kennismaken met deze object<br />

georiënteerde benadering. Wij hebben daarbij gekozen voor de taal C++ omdat dit één van de meest<br />

gebruikte object georiënteerde programmeertalen is. Het is echter niet de bedoeling dat je na deze<br />

onderwijseenheid op de hoogte bent van alle aspecten en details van C++. Wel zul je na afloop van deze<br />

onderwijseenheid de algemene ideeën achter OOP begrijpen en die toe kunnen passen in C++. <strong>De</strong> object<br />

georiënteerde benadering is een (voor jou) nieuwe manier van denken over hoe je informatie in een<br />

computer kunt structureren.<br />

<strong>De</strong> manier waarop we problemen oplossen met object georiënteerde technieken lijkt op de manier van<br />

probleem oplossen die we in het dagelijks leven gebruiken. Een object georiënteerde applicatie is<br />

opgebouwd uit deeltjes die we objecten noemen. Elk object heeft zijn eigen taak. Een object kan aan een<br />

ander object vragen of dit object wat voor hem wil doen, dit gebeurt door het zenden van een “message”<br />

aan dat object. Aan deze message kunnen indien nodig argumenten worden meegegeven. Het is de<br />

verantwoordelijkheid van het ontvangende object (de “receiver”) hoe het de message afhandelt. Zo kan<br />

dit object de verantwoordelijkheid afschuiven door zelf ook weer een message te versturen naar een<br />

ander object. Het algoritme dat de ontvanger gebruikt om de message af te handelen wordt de “method”<br />

genoemd. Het object dat de message verstuurt is dus niet op de hoogte van de door de ontvanger gebruikte<br />

method. Dit wordt “information hiding” genoemd. Het versturen van een message lijkt in eerste<br />

instantie op het aanroepen van een functie. Toch zijn er enkele belangrijke verschillen:<br />

• Een message heeft een bepaalde receiver.<br />

• <strong>De</strong> method die bij de message hoort is afhankelijk van de receiver.<br />

• <strong>De</strong> receiver van een message kan ook tijdens runtime worden bepaald. (Late binding between the<br />

message (function name) and method (code))<br />

In een programma kunnen zich meerdere objecten van een bepaald object type bevinden, vergelijkbaar<br />

met meerdere variabelen van een bepaald variabele type. Zo’n object type wordt “class” genoemd. Elk<br />

object behoort tot een bepaalde class, een object wordt ook wel een “instance” van de class genoemd.<br />

Bij het definiëren van een nieuwe class hoef je niet vanuit het niets te beginnen maar je kunt deze<br />

nieuwe class afleiden (laten overerven) van een al bestaande class. <strong>De</strong> class waarvan afgeleid wordt,<br />

wordt de “base class” genoemd en de class die daarvan afgeleid wordt, wordt “derived class” genoemd.<br />

Als van een afgeleide class weer nieuwe classes worden afgeleid ontstaat een hiërarchisch netwerk van<br />

classes. Een derived class overerft alle eigenschappen van een base class (overerving = “inheritance”).<br />

Als een object een message ontvangt wordt de bijbehorende method als volgt bepaald (“method binding”):<br />

• zoek in de class van het receiver object,<br />

• als daar geen method gevonden wordt zoek dan in de base class van de class van de receiver,<br />

• als daar geen method gevonden wordt zoek dan in de base class van de base class van de class van<br />

de receiver,<br />

• enz.<br />

Een method in een base class kan dus worden geherdefinieerd (“overridden”) door een method in de<br />

derived class. Naast de hierboven beschreven vorm van hergebruik (inheritance) kunnen objecten ook als<br />

onderdeel van een groter object gebruikt worden (“composition”). Inheritance wordt ook wel generalisatie<br />

(“generalization”) genoemd en leidt tot een zogenaamde “is een” relatie. Je kunt een class Bakker<br />

bijvoorbeeld afleiden door middel van overerving van een class Winkelier. Dit betekent dan dat een<br />

Bakker minimaal dezelfde messages ondersteunt als Winkelier (maar hij kan er wel zijn eigen methods<br />

aan koppelen!). We zeggen: “Een Bakker is een Winkelier” of “Een Bakker erft over van Winkelier” of<br />

“Een Winkelier is een generalisatie van Bakker”. Composition wordt ook wel “containment” of “aggregation”<br />

genoemd en leidt tot een zogenaamde “heeft een” relatie. Je kunt in de definitie van een class<br />

Auto vijf objecten van de class Wiel opnemen. Dit betekent dan dat een Auto deze Wielen gebruikt om<br />

de aan Auto gestuurde messages uit te voeren. We zeggen: “een Auto heeft Wielen”. Welke relatie in<br />

een bepaald geval nodig is, is niet altijd eenvoudig te bepalen. We komen daar later nog uitgebreid op<br />

terug.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

15


16<br />

<strong>De</strong> bij object georiënteerd programmeren gebruikte technieken komen niet uit de lucht vallen maar<br />

sluiten aan bij de historische evolutie van programmeertalen. Sinds de introductie van de computer zijn<br />

programmeertalen steeds abstracter geworden. <strong>De</strong> volgende abstractie technieken zijn achtereenvolgens<br />

ontstaan:<br />

• Functies en procedures.<br />

In talen zoals Basic, Fortran, C, Pascal, Cobol enz. kan een stukjes logisch bij elkaar behorende<br />

code geabstraheerd worden tot een functie of procedure. <strong>De</strong>ze functies of procedures kunnen<br />

lokale variabelen bevatten die buiten de functie of procedure niet zichtbaar zijn. <strong>De</strong>ze lokale<br />

variabelen zitten ingekapseld in de functie of procedure, dit wordt “encapsulation” genoemd. Als<br />

de specificatie van de functie bekend is kun je de functie gebruiken zonder dat je de implementatie<br />

details hoeft te kennen of te begrijpen. Dit is dus een vorm van information hiding. Tevens voorkomt<br />

het gebruik van functies en procedures het dupliceren van code, waardoor het wijzigen en<br />

testen van programma’s eenvoudiger wordt. <strong>De</strong> programmacode wordt korter, maar dit gaat ten<br />

koste van de executietijd.<br />

• Modules.<br />

In programmeertalen zoals Modula 2 (en zeer primitief ook in C) kunnen een aantal logisch bij<br />

elkaar behorende functies, procedures en variabelen geabstraheerd worden tot een module. <strong>De</strong>ze<br />

functies, procedures en variabelen kunnen voor de gebruikers van de module zichtbaar (=public)<br />

of onzichtbaar (=private) gemaakt worden. Functies, procedures en variabelen die private zijn<br />

kunnen alleen door functies en procedures van de module zelf gebruikt worden. <strong>De</strong>ze private<br />

functies, procedures en variabelen zitten ingekapseld in de module. Als de specificatie van de<br />

public delen van de module bekend is kun je de module gebruiken zonder dat je de implementatie<br />

details hoeft te kennen of te begrijpen. Dit is dus een vorm van data en information hiding.<br />

• Abstract data types (ADT’s).<br />

In programmeertalen zoals Ada kan een bepaalde datastructuur met de bij deze datastructuur<br />

behorende functies en procedures ingekapseld worden in een ADT. Het verschil tussen een module<br />

en een ADT is dat een module alleen gebruikt kan worden en dat een ADT geïnstantieerd kan<br />

worden. Dit wil zeggen dat er meerdere variabelen van dit ADT (=type) aangemaakt kunnen<br />

worden. Als de specificatie van het ADT bekend is kun je dit type op dezelfde wijze gebruiken als<br />

de ingebouwde typen van de programmeertaal (zoals char, int en double) zonder dat je de<br />

implementatie details van dit type hoeft te kennen of te begrijpen. Dit is dus weer een vorm van<br />

information hiding. Op het begrip ADT komen we nog uitgebreid terug.<br />

• Generieke functies en data types.<br />

In C++ kunnen door het gebruik van templates generieke functies en data types gedefinieerd<br />

worden. Een generieke functie is een functie die gebruikt kan worden voor meerdere parameter<br />

types. In C kan om een array met int’s te sorteren een functie geschreven worden. Als we vervolgens<br />

een array met double’s willen sorteren moeten we een nieuwe functie definiëren. In C++<br />

kunnen we met behulp van templates een generieke functie definiëren waarmee array’s van elk<br />

willekeurig type gesorteerd kunnen worden. Een generiek type is een ADT dat met verschillende<br />

andere types gecombineerd kan worden. In C++ kunnen we bijvoorbeeld een ADT array definieren<br />

waarin we variabelen van het type int kunnen opslaan. Door het gebruik van een template<br />

kunnen we een generiek ADT array definiëren waarmee we dan array’s van elk willekeurig type<br />

kunnen gebruiken. Op generieke functies en data types komen we nog uitgebreid terug.<br />

• Classes.<br />

In programmeertalen zoals SmallTalk en C++ kunnen een aantal logisch bij elkaar behorende<br />

ADT’s van elkaar worden afgeleid door middel van inheritance. Door nu programma’s te schrijven<br />

in termen van base classes en de in deze base classes gedefinieerde messages in afgeleide<br />

(Engels: derived) classes te implementeren kan je deze base classes gebruiken zonder dat je de<br />

implementatie van deze classes hoeft te kennen of te begrijpen en kun je eenvoudig van implementatie<br />

wisselen. Tevens kun je code die alleen messages aanroept van de base class zonder<br />

opnieuw te compileren hergebruiken voor alle direct of indirect van deze base class afgeleide<br />

classes. Dit laatste wordt mogelijk gemaakt door de message pas tijdens run time aan een bepaalde<br />

method te verbinden en wordt “polymorfisme” genoemd. Op deze begrippen komen we later nog<br />

uitgebreid terug.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


In het propedeuse project heb je geleerd hoe je vanuit een probleemstelling voor een programma de<br />

functies en procedures kan vinden. <strong>De</strong>ze methode bestond uit het opdelen van het probleem in deelproblemen,<br />

die op hun beurt weer werden opgedeeld in deelproblemen enz. net zo lang tot de oplossing<br />

eenvoudig werd. <strong>De</strong>ze methode heet functionele decompositie. <strong>De</strong> software ontwikkelmethoden die<br />

hiervan zijn afgeleid worden “structured analyse and design” genoemd. <strong>De</strong> bekende Yourdon methode is<br />

daar een voorbeeld van. Bij het gebruik van object georiënteerde programmeertalen ontstaat al snel de<br />

vraag: “Hoe vind ik uitgaande van de probleemstelling de in dit programma benodigde classes en het<br />

verband tussen die classes?” <strong>De</strong>ze vraag is afhankelijk van de gebruikte OOA (Object Oriented Analyse)<br />

en OOD (Object Oriented <strong>De</strong>sign) methode. <strong>De</strong>ze onderwerpen komen bij E later in dit onderwijsdeel<br />

aan de orde. Bij TI zijn deze onderwerpen al aan de orde geweest en komen ze nog uitgebreid bij verschillende<br />

onderwijsdelen in H2 aan de orde.<br />

2.2 ADT’s (Abstract Data Types).<br />

<strong>De</strong> eerste stap op weg naar het object georiënteerde denken en programmeren is het leren programmeren<br />

met ADT’s. Een ADT 15 is een user-defined type dat voor een gebruiker niet te onderscheiden is van<br />

ingebouwde types (zoals int). Een ADT koppelt een bepaalde datastructuur (interne variabelen) met de<br />

bij deze datastructuur behorende bewerkingen (functies). <strong>De</strong>ze functies en variabelen kunnen voor de<br />

gebruikers van het ADT zichtbaar (=public) of onzichtbaar (=private) gemaakt worden. Functies en<br />

variabelen die private zijn kunnen alleen door functies van het ADT zelf gebruikt worden. <strong>De</strong> private<br />

functies en variabelen zitten ingekapseld in het ADT. Als de specificatie van het ADT bekend is kun je<br />

dit type op dezelfde wijze gebruiken als de ingebouwde typen van de programmeertaal (zoals char,<br />

int en double) zonder dat je de implementatie details van dit type hoeft te kennen of te begrijpen. Dit<br />

is een vorm van information hiding.<br />

In C is het niet mogelijk om ADT’s te definiëren. Als je in C een programma wilt schrijven waarbij je<br />

met breuken in plaats van met floating point getallen wilt werken 16 , dan kun je dit (niet in C) opgenomen<br />

“type” Breuk alleen maar op de volgende manier definiëren:<br />

struct Breuk { /* een breuk bestaat uit: */<br />

int boven; /* een teller en */<br />

int onder; /* een noemer */<br />

};<br />

Een C functie om twee breuken op te tellen kan dan als volgt gedefinieerd worden:<br />

struct Breuk som(struct Breuk b1, struct Breuk b2) {<br />

struct Breuk s;<br />

s.boven=b1.boven*b2.onder + b1.onder*b2.boven;<br />

s.onder=b1.onder*b2.onder;<br />

s=normaliseer(s);<br />

}<br />

Het normaliseren 17 van de breuk zorgt ervoor dat 3/8 + 1/8 als resultaat 1/2 in plaats van 4/8 heeft. Dit<br />

zorgt ervoor dat een overflow minder snel optreedt als met het resultaat weer verder wordt gerekend.<br />

15 <strong>De</strong> naam ADT wordt ook vaak gebruikt voor een formele wiskundige beschrijving van een type. Ik<br />

gebruik het begrip ADT hier alleen in de betekenis van “user-defined type”.<br />

16 Een belangrijke reden om te werken met breuken in plaats van floating point getallen is het voorkomen<br />

van afrondingsproblemen. Een breuk zoals 1/3 moet als floating point getal afgerond worden tot<br />

bijvoorbeeld: 0.333333333. <strong>De</strong>ze afrondig kan ervoor zorgen dat een berekening zoals 3*(1/3) niet<br />

zoals verwacht de waarde 1 maar de waarde 0.999999999 oplevert. Ook breuken zoals 1/10 moeten<br />

afgerond worden als ze als binair floating point getal worden weergegeven.<br />

17 Het algoritme voor het normaliseren van een breuk wordt verderop in dit dictaat besproken.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

17


18<br />

<strong>De</strong>ze manier van werken heeft de volgende nadelen:<br />

• Iedere programmeur die gebruikt maakt van het type struct Breuk kan een waarde toekennen<br />

aan de datavelden boven en onder. Het is in C niet te voorkomen dat het dataveld onder op<br />

nul gezet wordt. Als een programmeur het dataveld onder van een Breuk op nul zet, kan dit een<br />

fout veroorzaken in code van een andere programmeur die een Breuk naar een double converteert.<br />

Als deze fout geconstateerd wordt kunnen alle functies waarin breuken gebruikt worden de<br />

boosdoeners zijn.<br />

• Iedere programmeur die gebruikt maakt van het type struct Breuk kan er voor kiezen om zelf<br />

de code voor het optellen van breuken “uit te vinden” in plaats van gebruik te maken van de<br />

functie som. Er valt dus niet te garanderen dat alle optellingen van breuken correct en genormaliseerd<br />

zullen zijn. Ook niet als we wel kunnen “garanderen” dat de functies som en normaliseer<br />

correct zijn. Iedere programmeur die breuken gebruikt kan ze namelijk op zijn eigen manier<br />

optellen.<br />

• Iedere programmeur die gebruikt maakt van het type struct Breuk zal zelf nieuwe bewerkingen<br />

(zoals bijvoorbeeld het vermenigvuldigen van breuken) definiëren. Het zou beter zijn als alleen<br />

de programmeur die verantwoordelijk is voor het onderhouden van het type struct Breuk (en<br />

de bijbehorende bewerkingen) dit kan doen.<br />

<strong>De</strong>ze nadelen komen voort uit het feit dat in C de definitie van het type struct Breuk niet gekoppeld<br />

is aan de bewerkingen die op dit type uitgevoerd kunnen worden. Tevens is het niet mogelijk om bepaalde<br />

datavelden en/of bewerkingen ontoegankelijk te maken voor programmeurs die dit type gebruiken.<br />

In C++ kunnen we door gebruik te maken van een class een eigen type Breuk als volgt declareren:<br />

class Breuk { // Op een object van de class Breuk<br />

public: // kun je de volgende bewerkingen uitvoeren:<br />

void leesin(); // inlezen vanuit het toetsenbord.<br />

void drukaf() const 18 ; // afdrukken op het scherm.<br />

void plus(Breuk 19 b); // een Breuk erbij optellen.<br />

private: // Een object van de class Breuk heeft privé:<br />

int boven; // een teller,<br />

int onder; // een noemer en<br />

void normaliseer(); // een functie normaliseer.<br />

};<br />

<strong>De</strong> class Breuk koppelt een bepaalde datastructuur (2 interne integer variabelen genaamd boven en<br />

onder) met de bij deze datastructuur behorende bewerkingen 20 (functies: leesin, drukaf, plus en<br />

normaliseer). <strong>De</strong>ze functies en variabelen kunnen voor de gebruikers van de class Breuk zichtbaar<br />

(public) of onzichtbaar (private) gemaakt worden. Functies en variabelen die private zijn kunnen<br />

alleen door functies van de class Breuk zelf gebruikt worden 21 . <strong>De</strong>ze private functies en variabelen<br />

zitten ingekapseld in de class Breuk.<br />

18 const memberfunctie wordt pas behandeld op blz. 25.<br />

19 Het is beter om hier een const reference parameter te gebruiken. Dit wordt pas behandeld op blz.<br />

31.<br />

20 <strong>De</strong> verzameling van public functies wordt ook wel de interface van de class genoemd. <strong>De</strong>ze interface<br />

definieert hoe je objecten van deze class kunt gebruiken (welke memberfuncties je op de objecten<br />

van deze class kan aanroepen).<br />

21 Variabelen die private zijn kunnen door het gebruik van een pointer en bepaalde type-conversies toch<br />

door andere functies benaderd worden. Ze staan namelijk gewoon in het werkgeheugen en ze zijn dus<br />

altijd via software toegankelijk. Het gebruik van private variabelen is alleen bedoeld om “onbewust”<br />

verkeerd gebruik tegen te gaan.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


Functies de opgenomen zijn in een class worden memberfuncties genoemd. <strong>De</strong> memberfunctie plus<br />

kan als volgt gedefinieerd worden 22 :<br />

void Breuk::plus(Breuk b) {<br />

boven=boven*b.onder + onder*b.boven;<br />

onder*=b.onder;<br />

normaliseer();<br />

}<br />

Je kunt objecten (variabelen) van de class (zelf gedefinieerde type) Breuk nu als volgt gebruiken:<br />

Breuk a, b; // definieer de objecten a en b van de class Breuk<br />

a.leesin(); // lees a in<br />

b.leesin(); // lees b in<br />

a.plus(b); // tel b bij a op<br />

a.drukaf(); // druk a af<br />

Een object heeft drie kenmerken:<br />

• Geheugen (Engels: state).<br />

Een object kan “iets” onthouden. Wat een object kan onthouden blijkt uit de class declaratie. Het<br />

object a is een instantie van de class Breuk en kan, zoals uit de class declaratie van Breuk<br />

blijkt, twee integers onthouden. Elk object heeft (vanzelfsprekend) zijn eigen geheugen. Zodat de<br />

Breuk a een andere waarde kan hebben dan de Breuk b.<br />

• Gedrag (Engels: behaviour).<br />

Een object kan “iets” doen. Wat een object kan doen blijkt uit de class declaratie. Het object a is<br />

een instantie van de class Breuk en kan, zoals uit de declaratie van Breuk blijkt, 4 dingen doen:<br />

zichzelf inlezen, zichzelf afdrukken, een Breuk bij zichzelf optellen en zichzelf normaliseren.<br />

Alle objecten van de class Breuk hebben hetzelfde gedrag. Dit betekent dat de code van de<br />

memberfuncties gezamenlijk gebruikt wordt door alle objecten van de class Breuk.<br />

• Identiteit (Engels: identity).<br />

Om objecten met dezelfde waarde toch uit elkaar te kunnen houden heeft elk object een eigen<br />

identiteit. <strong>De</strong> twee objecten van de class Breuk in het voorgaande voorbeeld hebben bijvoorbeeld<br />

elk een eigen naam (a en b) waarmee ze geïdentificeerd kunnen worden.<br />

Bij de memberfunctie plus wordt bij de definitie met de zogenaamde qualifier Breuk:: aangegeven<br />

dat deze functie een member is van de class Breuk. <strong>De</strong> memberfunctie plus kan alleen op een object<br />

van de class Breuk uitgevoerd worden. Dit wordt genoteerd als: a.plus(b); Het object a wordt de<br />

receiver (ontvanger) van de memberfunctie genoemd. Als in de definitie van de memberfunctie plus de<br />

datamembers boven en onder gebruikt worden dan zijn dit de datamembers van de receiver (in dit<br />

geval object a). Als in de definitie van de memberfunctie plus de memberfunctie normaliseer<br />

gebruikt wordt dan wordt deze memberfunctie op de receiver uitgevoerd (in dit geval object a). <strong>De</strong><br />

betekenis van const achter de declaratie van de memberfunctie drukaf komt later (blz. 25) aan de orde.<br />

<strong>De</strong>ze manier van werken heeft de volgende voordelen:<br />

• Een programmeur die gebruik maakt van de class (het type) Breuk kan géén waarde toekennen<br />

aan de private datavelden boven en onder. Alleen de memberfuncties leesin, drukaf,<br />

plus en normaliseer kunnen een waarde toekennen aan deze datavelden. Als ergens een fout<br />

ontstaat omdat het dataveld onder van een Breuk op nul is gezet kunnen alleen de memberfuncties<br />

van Breuk de boosdoeners zijn.<br />

• Een programmeur die gebruik maakt van de class Breuk kan er niet voor kiezen om zelf de code<br />

voor het optellen van breuken “uit te vinden” in plaats van gebruik te maken van de memberfunctie<br />

plus. Als we kunnen “garanderen” dat de functies plus en normaliseer correct zijn, dan<br />

kunnen we dus garanderen dat alle optellingen van breuken correct en genormaliseerd zullen zijn.<br />

22 <strong>De</strong> definitie van de memberfuncties leesin, drukaf en normaliseer is hier niet gegeven.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

19


20<br />

Iedere programmeur die breuken gebruikt kan ze namelijk alleen optellen door gebruik te maken<br />

van de memberfunctie plus.<br />

• Iedere programmeur die gebruikt maakt van de class Breuk zal niet zelf nieuwe bewerkingen<br />

(zoals bijvoorbeeld het vermenigvuldigen van breuken) definiëren. Alleen de programmeur die<br />

verantwoordelijk is voor het onderhouden van de class Breuk (en de bijbehorende bewerkingen)<br />

kan dit doen.<br />

Bij het ontwikkelen van kleine programma’s zijn deze voordelen niet zo belangrijk maar bij het ontwikkelen<br />

van grote programma’s zijn deze voordelen wel erg belangrijk. Door gebruik te maken van de<br />

class Breuk met bijbehorende memberfuncties in plaats van de struct Breuk met de bijbehorende<br />

functies wordt het programma beter onderhoudbaar en eenvoudiger uitbreidbaar.<br />

<strong>De</strong> hierboven gedefinieerde class Breuk is erg beperkt. In de inleiding van dit hoofdstuk heb ik geschreven:<br />

“Een ADT is een user-defined type dat voor een gebruiker niet te onderscheiden is van ingebouwde<br />

types (zoals int).” Een ADT Breuk zal dus als volgt gebruikt moeten kunnen worden:<br />

Breuk b1, b2; // definiëren van variabelen<br />

coutb1; // inlezen met >><br />

coutb2;<br />

cout


#include <br />

#include <br />

using namespace std;<br />

// Class declaratie:<br />

class Breuk {<br />

public:<br />

// Class interface.<br />

// Vertelt wat je met een object van de class kunt doen.<br />

Breuk(); // constructors zie blz. 22<br />

Breuk(int t);<br />

Breuk(int t, int n);<br />

int teller() const; // const memberfunctie zie blz. 25<br />

int noemer() const;<br />

void plus(Breuk 23 b);<br />

void abs();<br />

private:<br />

// Class implementatie.<br />

// Vertelt wat je nodig hebt om een object van de class te maken.<br />

// Dit is voor gebruikers van de class niet van belang.<br />

int boven;<br />

int onder;<br />

void normaliseer();<br />

};<br />

// Hulpfunctie: bepaalt de grootste gemene deler.<br />

int ggd(int n, int m) {<br />

if (n==0) return m;<br />

if (m==0) return n;<br />

if (n


22<br />

}<br />

void Breuk::abs() {<br />

if (boven


met 1 geïnitialiseerd. Dit gebeurt in een zogenaamde initialisation list. Na het prototype van de constructor<br />

volgt een : waarna de datamembers één voor één geïnitialiseerd worden door middel van de<br />

(...) notatie.<br />

Breuk::Breuk(): boven(0), onder(1) {<br />

}<br />

In dit geval is er verder geen code in de constructor opgenomen. Door tussen de {} bijvoorbeeld een<br />

output opdracht op te nemen zou je een melding op het scherm kunnen geven telkens als een Breuk<br />

aangemaakt wordt. <strong>De</strong>ze constructor wordt dus aangeroepen als je een Breuk aanmaakt zonder deze te<br />

initialiseren. Na het uitvoeren van de onderstaande code heeft de breuk b de waarde 0/1.<br />

Breuk b1; // roep constructor zonder parameters aan<br />

<strong>De</strong> overige constructors worden gebruikt als je een Breuk bij het aanmaken met 1 of met 2 integers<br />

ïnitialiseert. Na afloop van de onderstaande code zal het object b2 de waarde 1/2 bevatten. (3/6 wordt<br />

namelijk in de constructor genormaliseerd tot 1/2.)<br />

Breuk b2(3,6); // roep constructor met twee int parameters aan<br />

<strong>De</strong>ze constructor Breuk(int t, int n); heb ik als volgt gedefinieerd:<br />

Breuk::Breuk(int t, int n): boven(t), onder(n) {<br />

normaliseer();<br />

}<br />

Door het definiëren van constructors kun je er dus voor zorgen dat elk object bij het aanmaken “geïnitialiseerd”<br />

wordt. Fouten die voortkomen uit het gebruik van een niet geïnitialiseerde variabele worden<br />

hiermee voorkomen. Het is dus verstandig om voor elke class één of meer constructors te definiëren. 25<br />

Als (ondanks het zo juist gegeven advies) geen enkele constructor gedefinieerd is dan wordt door de<br />

compiler een default constructor (= constructor zonder parameters) aangemaakt. <strong>De</strong>ze default constructor<br />

roept voor elk dataveld de default constructor van dit veld aan (=memberwise construction).<br />

2.5 Constructors en type conversies. (Zie eventueel TICPP Chapter12.html#Heading372.)<br />

Aan het einde van de functie main wordt de memberfunctie plus als volgt aangeroepen:<br />

b3.plus(5);<br />

Uit de uitvoer blijkt dat dit “gewoon” werkt: b3 wordt verhoogd met 5/1. Op zich is dit vreemd want de<br />

memberfunctie plus is gedefinieerd met een Breuk als argument en niet met een int als argument.<br />

Als je de memberfunctie plus aanroept op een Breuk met een int als argument gebeurt het volgende:<br />

eerst “kijkt” de compiler of in de class Breuk de memberfunctie plus(int)gedefinieerd is. Als dit<br />

het geval is dan wordt deze memberfunctie aangeroepen. Als dit niet het geval is dan “kijkt” de compiler<br />

of er in de class Breuk een memberfunctie is met een argument van een ander type waarnaar het type<br />

int omgezet kan worden 26 . In dit geval “ziet” de compiler de memberfunctie Breuk::plus(<br />

Breuk). <strong>De</strong> compiler “vraagt zich nu dus af” hoe een variabele van het type int omgezet kan worden<br />

25 Door het slim gebruik van default parameters kun je het aantal constructors vaak beperken. Alle drie<br />

de constructors die ik voor de class Breuk gedefinieerd heb zouden door één constructor met default<br />

parameters vervangen kunnen worden: Breuk::Breuk(int t=0, int n=1);<br />

26 Als dit er meerdere zijn, dan zijn er allerlei conversie regels in de C++ standaard opgenomen om te<br />

bepalen welke conversie gekozen wordt. Ik zal dat hier niet bespreken. Een conversie naar een<br />

ingebouwd type gaat altijd voor ten opzichte van een conversie naar een zelfgefinieerd type.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

23


24<br />

naar het type Breuk. Of met andere woorden hoe een Breuk gemaakt kan worden en geïnitialiseerd<br />

kan worden met een int. Of met andere woorden of de constructor Breuk(int) bestaat. <strong>De</strong>ze<br />

constructor bestaat in dit geval. <strong>De</strong> compiler maakt nu een tijdelijke naamloze variabele aan van het type<br />

Breuk (door gebruik te maken van de constructor Breuk(int)) en geeft een kopietje van deze<br />

variabele door aan de memberfunctie plus. <strong>De</strong> memberfunctie plus telt de waarde van deze variabele<br />

op bij de receiver. Na afloop van de memberfunctie plus wordt de tijdelijke naamloze variabele weer<br />

vrijgegeven.<br />

Als je dus een constructor Breuk(int) definieert, wordt het type int indien nodig automatisch<br />

omgezet naar het type Breuk. Of algemeen: als je een constructor X(Y) definieert, wordt het type Y<br />

indien nodig automatisch omgezet naar het type X 27 .<br />

2.6 Initialisation list van de constructor. (Zie eventueel TICPP<br />

Chapter08.html#Heading267.)<br />

Het initialiseren van data fields vanuit de constructor kan op twee manieren geprogrammeerd worden:<br />

• door gebruik te maken van een initialisation list.<br />

• door gebruik te maken van assignments in het code blok van de constructor.<br />

<strong>De</strong> eerste methode heeft de voorkeur, omdat dit altijd werkt. Een constante datamember kan bijvoorbeeld<br />

niet met een assignment geïnitialiseerd worden.<br />

Dus gebruik:<br />

Breuk::Breuk(): boven(0), onder(1) {<br />

}<br />

in plaats van:<br />

Breuk::Breuk() {<br />

boven=0; // Het is beter om een initialisatie lijst te<br />

onder=1; // gebruiken!<br />

}<br />

2.7 <strong>De</strong>fault copy constructor. (Zie eventueel TICPP Chapter11.html#Heading339.)<br />

Een copy constructor wordt gebruikt als een object gekopieerd moet worden. Dit is het geval als:<br />

• een object geïnitialiseerd wordt met een object van dezelfde class.<br />

• een object als value parameter wordt doorgegeven aan een functie.<br />

• een object als waarde wordt teruggegeven vanuit een functie.<br />

<strong>De</strong> compiler zal als de programmeur geen copy constructor definieert zelf een default copy constructor<br />

genereren. <strong>De</strong>ze default copy constructor kopieert elk deel waaruit de class bestaat vanuit de een naar de<br />

andere (=memberwise copy). Het is ook mogelijk om zelf een copy constructor te definiëren (wordt later<br />

behandeld) maar voor de class Breuk voldoet de door de compiler gedefinieerde copy constructor<br />

prima.<br />

2.8 <strong>De</strong>fault assignment operator.<br />

Als geen assignment operator (=) gedefinieerd is dan wordt door de compiler een default assignment<br />

operator aangemaakt. <strong>De</strong>ze default assignment operator roept voor elk dataveld de assignment operator<br />

van dit veld aan (=memberwise assignment). Het is ook mogelijk om zelf een assignment operator te<br />

27 Als je dit niet wilt kun je het keyword explicit voor de constuctor plaatsen. <strong>De</strong> als explicit gedefinieerde<br />

constructor wordt dan niet meer automatisch (=impliciet) voor type conversie gebruikt.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


definiëren (wordt later behandeld) maar voor de class Breuk voldoet de door de compiler gedefinieerde<br />

assignment operator prima.<br />

2.9 const memberfuncties. (Zie eventueel TICPP Chapter08.html#Heading271.)<br />

Een object (variabele) van de class (het zelf gedefinieerde type) Breuk kan ook als constante<br />

gedefinieerd worden. Bijvoorbeeld:<br />

Breuk b(1,3); // variabele b met waarde 1/3<br />

const Breuk halve(1,2); // constante halve met waarde 1/2<br />

Een const Breuk mag je (vanzelfsprekend) niet veranderen.<br />

halve=b;<br />

// Error: Cannot modify a const object<br />

Stel jezelf nu eens de vraag welke memberfuncties je aan mag roepen op het object halve 28 . <strong>De</strong> memberfunties<br />

teller en noemer kunnen zonder problemen worden aangeroepen op een constante breuk<br />

omdat ze de waarde van de breuk niet veranderen. <strong>De</strong> memberfunties plus en abs mogen echter niet<br />

op het object halve worden aangeroepen omdat deze memberfuncties dit object zouden wijzigen en<br />

een constant object mag je vanzelfsprekend niet veranderen. <strong>De</strong> compiler kan niet (altijd 29 ) controleren<br />

of het aanroepen van een memberfunctie een verandering in het object tot gevolg heeft. Daarom mag je<br />

een memberfunctie alleen aanroepen voor een const object als je expliciet aanduidt dat deze memberfunctie<br />

het object niet verandert. Dit doe je door het keyword const achter de memberfunctie te<br />

plaatsten. Bijvoorbeeld:<br />

int teller() const;<br />

Met deze const memberfunctie kun je de teller van een Breuk opvragen. <strong>De</strong> implementatie is erg<br />

eenvoudig:<br />

int Breuk::teller() const {<br />

return boven;<br />

}<br />

Je kunt de memberfunctie teller nu dus ook aanroepen voor constante breuken:<br />

const Breuk halve(1,2); // constante halve met waarde 1/2<br />

cout


26<br />

Als je probeert om in een const memberfunctie toch de receiver te veranderen krijg je de volgende<br />

foutmelding:<br />

int Breuk::teller() const {<br />

boven=1; // teller probeert vals te spelen<br />

// Error: Cannot modify a const object<br />

}<br />

Door het toepassen van function overloading is het mogelijk om 2 functies te definiëren met dezelfde<br />

naam en met dezelfde parameters waarbij de ene const is en de andere niet, maar dit is erg gedetailleerd<br />

en zal ik hier niet behandelen 31 .<br />

We kunnen nu de memberfuncties van een class in twee groepen opdelen:<br />

• Vraag-functies.<br />

<strong>De</strong>ze memberfuncties kunnen gebruikt worden om de toestand van een object op te vragen. <strong>De</strong>ze<br />

memberfuncties hebben over het algemeen wel een return type, geen parameters en zijn const<br />

memberfuncties.<br />

• Doe-functies.<br />

<strong>De</strong>ze memberfuncties kunnen gebruikt worden om de toestand van een object te veranderen. <strong>De</strong>ze<br />

memberfuncties hebben over het algemeen geen return type (void), wel parameters en zijn nonconst<br />

memberfuncties.<br />

2.10 Class invariant.<br />

In de memberfuncties van de class Breuk wordt ervoor gezorgd dat elk object van de class Breuk een<br />

noemer >0 heeft en de ggd (grootste gemene deler) van de teller en noemer 1 is. Door de noemer altijd<br />

>0 te maken kan het teken van de Breuk eenvoudig bepaald worden (=teken van teller). Door de<br />

Breuk altijd te normaliseren wordt onnodige overflow van de datamembers boven en onder voorkomen.<br />

Een voorwaarde waaraan elk object van een class voldoet wordt een class invariant genoemd. Als<br />

je ervoor zorgt dat de class invariant aan het einde van elke public memberfunctie geldig is en als alle<br />

datamembers private zijn, dan weet je zeker dat voor elk object van de class Breuk deze invariant altijd<br />

geldig is 32 . Dit vermindert de kans op het maken van fouten.<br />

2.11 Voorbeeld class Breuk (tweede versie).<br />

Dit voorbeeld is een uitbreiding van de op blz. 20 gegeven class Breuk (versie 1). In deze versie zul je<br />

leren hoe je een object van de class Breuk kunt optellen bij een ander object van de class Breuk met<br />

de operator += in plaats van met de memberfunctie plus (door middel van operator overloading). Het<br />

optellen van een Breuk bij een Breuk gaat nu op precies dezelfde wijze als het optellen van een int<br />

bij een int. Dit maakt het type Breuk voor programmeurs erg eenvoudig te gebruiken. Ik zal nu eerst<br />

de complete source code presenteren van een programma waarin het type Breuk gedeclareerd, geïmplementeerd<br />

en gebruikt wordt. Meteen daarna zal ik op het bovengenoemde punt ingaan.<br />

#include <br />

#include <br />

30 (...vervolg)<br />

error: passing `const Breuk' as `this' argument of `void Breuk::drukaf()' discards qualifiers. Cryptisch<br />

maar wel correct.<br />

31 Voor degene die echt alles wil weten: Als een memberfunctie zowel const als non-const gedefinieerd<br />

is wordt bij aanroep op een const object de const memberfunctie aangeroepen en wordt bij aanroep<br />

op een non-const object de non-const memberfunctie aangeroepen. (Logisch nietwaar!)<br />

32 Door het definieren van invarianten (en pre- en postcondities) wordt het zelfs mogelijk om op een<br />

formele wiskundige manier te bewijzen dat een ADT correct is. Ik zal dit hier verder niet behandelen.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


using namespace std;<br />

class Breuk {<br />

public:<br />

Breuk(int t, int n);<br />

int teller() const;<br />

int noemer() const;<br />

void operator+=(Breuk 33 right);<br />

private:<br />

int boven;<br />

int onder;<br />

void normaliseer();<br />

};<br />

// ...<br />

void Breuk::operator+=(Breuk right) {<br />

boven=boven*right.onder + onder*right.boven;<br />

onder=onder*right.onder;<br />

normaliseer();<br />

}<br />

int main() {<br />

Breuk b1(14, 4);<br />

cout


28<br />

Als de Breuk::operator+= memberfunctie wel gedefinieerd is wordt het bovenstaande statement<br />

geïnterpreteerd als:<br />

b1.operator+=(b2);<br />

<strong>De</strong> memberfunctie operator+= in deze tweede versie van Breuk heeft dezelfde implementatie als de<br />

memberfunctie plus in de eerste versie van Breuk (zie blz. 19).<br />

Je kunt voor een zelf gedefinieerd type alle operatoren zelf definiëren behalve de operator . waarmee<br />

een member geselecteerd wordt en de operator ?: 34 . Dus zelfs operatoren zoals [] (array index), -><br />

(pointer dereferentie naar member) en () (functie aanroep) kun je zelf definiëren! Je kunt de prioriteit<br />

van operatoren niet zelf definiëren. Dit betekent dat a+b*c altijd wordt geïnterpreteerd als a+(b*c).<br />

Ook de associativiteit van de operatoren met gelijke prioriteit kun je niet zelf definiëren. Dit betekent dat<br />

a+b+c altijd wordt geïnterpreteerd als (a+b)+c. Ook is het niet mogelijk om de operatoren die al<br />

gedefinieerd zijn voor de ingebouwde typen zelf te (her)definiëren. Het zelf definiëren van operatoren<br />

die in de taal C++ nog niet bestaan bijvoorbeeld @ of ** is niet mogelijk. Bij het definiëren van operatoren<br />

voor zelf gedefinieerde typen moet je er natuurlijk wel voor zorgen dat het gebruik voor de hand<br />

liggend is. <strong>De</strong> compiler heeft er geen enkele moeilijkheid mee als je bijvoorbeeld de memberfunctie<br />

Breuk::operator+= definieert die de als argument meegegeven Breuk van de receiver aftrekt. <strong>De</strong><br />

programmeurs die de class Breuk gebruiken zullen dit minder kunnen waarderen.<br />

Er blijkt nu toch nog een verschil te zijn tussen het gebruik van de operator += voor het zelf gedefinieerde<br />

type Breuk en het gebruik van de operator += voor het ingebouwde type int. Bij het type int kun<br />

je de operator += als volgt gebruiken: a+=b+=c;. Dit wordt omdat de operator += van links naar rechts<br />

geëvalueerd wordt als volgt geïnterpreteerd a+=(b+=c). Dit betekent dat eerst c bij b wordt opgeteld<br />

en dat het resultaat van deze optelling weer bij a wordt opgeteld. Zowel b als a hebben na de optelling<br />

een waarde toegekend gekregen. Als je dit probeert als a, b en c van het type Breuk zijn verschijnt de<br />

volgende (niet erg duidelijke) foutmelding:<br />

// Error: Illegal structure operation.<br />

Dit komt doordat de Breuk::operator+= memberfunctie geen return type heeft (void). Het<br />

resultaat van de bewerking b+=c moet dus niet alleen in het object b worden opgeslagen maar ook als<br />

resultaat worden teruggegeven. <strong>De</strong> operator+= memberfunctie kan dan als volgt gedeclareerd worden:<br />

Breuk operator+=(Breuk right); 35<br />

<strong>De</strong> definitie is dan als volgt:<br />

Breuk Breuk::operator+=(Breuk right) {<br />

boven=boven*right.onder + onder*right.boven;<br />

onder=onder*right.onder;<br />

normaliseer();<br />

return DIT_OBJECT; // Dit is géén C++ code!!<br />

}<br />

Ik zal nu eerst bespreken hoe je de receiver (DIT_OBJECT) kunt benaderen en daarna zal ik weer op de<br />

definitie van de operator+= terugkomen.<br />

34 Ook de niet in dit dictaat besproken operatoren .* en sizeof kun je niet zelf definiëren.<br />

35 Even verderop zal ik bespreken waarom het gebruik van het return type Breuk niet juist is en hoe<br />

het beter kan.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


2.13 this pointer.<br />

Elke memberfunctie kan beschikken over een impliciet argument genaamd this die het adres bevat van<br />

het object waarop de memberfunctie wordt uitgevoerd. Met deze pointer kun je bijvoorbeeld het object<br />

waarop een memberfunctie wordt uitgevoerd als return waarde van deze memberfunctie teruggeven.<br />

Bijvoorbeeld:<br />

Breuk Breuk::operator+=(Breuk right) 36 {<br />

boven=boven*right.onder + onder*right.boven;<br />

onder=onder*right.onder;<br />

normaliseer();<br />

return *this;<br />

}<br />

Als we nu de Breuk objecten a, b en c aanmaken en de expressie a+=b+=c; testen dan zien we dat<br />

het werkt zoals verwacht. Object a wordt gelijk aan a+b+c, object b wordt gelijk aan b+c en object c<br />

veranderd niet.<br />

Toch is het (nog steeds) niet correct. In de bovenstaande Breuk::operator+= memberfunctie zal bij<br />

het uitvoeren van het return statement een kopie van het huidige object worden teruggegeven. Dit is<br />

echter niet correct. Want als we deze operator als volgt gebruiken: (a+=b)+=c; dan wordt eerst a+=b<br />

uitgerekend en een kopie van a teruggegeven, c wordt dan bij deze kopie van a opgeteld waarna de<br />

kopie wordt verwijderd. Dit is natuurlijk niet de bedoeling, het is de bedoeling dat c bij a wordt opgeteld.<br />

<strong>De</strong> bedenker van C++, Bjarne Stroustrup, heeft om dit probleem op te lossen een heel nieuw soort<br />

variabele aan C++ toegevoegd: de reference variabele.<br />

2.14 Reference variabelen. (Zie eventueel TICPP Chapter11.html#Heading326.)<br />

Een reference is niets anders dan een alias (andere naam) voor de variabele waarnaar hij refereert. Alle<br />

operaties die op een variabele zijn toegestaan zijn ook toegestaan op een reference die naar die variabele<br />

verwijst 37 .<br />

int i(3) 38 ;<br />

int& j(i); // een reference moet geïnitialiseerd worden<br />

// er bestaat nu 1 variabele met 2 namen (i en j)<br />

36 <strong>De</strong>ze operator+= is niet juist! Zie blz. 33 voor een correcte implementatie van de operator+=.<br />

37 Een reference lijkt een beetje op een pointer maar er zijn toch belangrijke verschillen:<br />

• Als je de variabele waar een pointer p naar wijst wilt benaderen moet je de notatie *p gebruiken<br />

maar als je de variabele waar een reference r naar refereert wilt benaderen kun je gewoon<br />

r gebruiken.<br />

• Een pointer die naar de variabele v wijst kun je later naar een andere variabele laten wijzen<br />

maar een reference die naar de variabele v refereert kun je later niet naar een andere variabele<br />

laten refereren.<br />

• Een pointer kan ook naar geen enkele variabele wijzen (de waarde is dan 0) maar een reference<br />

verwijst altijd naar een variabele.<br />

38 Tot nu toe is een integer geïnitialiseerd met de uit C overgenomen syntax int i=3; In C++ mag je<br />

ook de syntax int i(3); gebruiken, alsof het ingebouwde type int een constructor heeft die je<br />

aanroept. <strong>De</strong>ze syntax heeft de voorkeur omdat deze syntax ook werkt bij zelf gedefinieerde types<br />

(classes) zoals Breuk. Een object b van de class Breuk kun je alleen initialiseren met de syntax<br />

Breuk b(3, 7);<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

29


30<br />

j=4; // i is nu gelijk aan 4<br />

// een reference is gewoon een "pseudoniem"<br />

2.15 Reference parameters. (Zie eventueel TICPP Chapter11.html#Heading327.)<br />

In C worden bij het aanroepen van een functie de parameters gekopieerd. Dit betekent dat de parameters<br />

die in een functieaanroep zijn gebruikt na afloop van de functie niet veranderd kunnen zijn. Je kunt de<br />

parameters in de functiedefinitie dus gewoon als een lokale variabele gebruiken. Een parameter die in<br />

een definitie van een functie gebruikt wordt, wordt formele parameter genoemd. Een parameter die bij<br />

een aanroep van een functie gebruikt wordt, wordt actuele parameter genoemd. Bij een functieaanroep<br />

wordt dus de waarde van de actuele parameter naar de formele parameter gekopieerd 39 . Het is dus geen<br />

probleem als de actuele parameter een constante is.<br />

void skipLines(int l) {<br />

while (l-- > 0)<br />

cout


Dit betekent dat er een probleem ontstaat als de actuele parameter een constante is omdat aan een<br />

constante geen nieuwe waarde toegekend mag worden. Bijvoorbeeld:<br />

swapInts(i, 5);<br />

<strong>De</strong> C++ Builder compiler geeft de volgende foutmeldingen:<br />

Reference initialized with 'int', needs lvalue of type 'int'<br />

Type mismatch in parameter 'q' (wanted 'int &', got 'int')<br />

2.16 const reference parameters. (Zie eventueel TICPP Chapter11.html#Heading328.)<br />

Er is nog een andere reden om een reference parameter in plaats van een “normale” value parameter te<br />

gebruiken. Bij een reference parameter wordt zoals we hebben gezien een adres (op de meeste systemen<br />

4 bytes) gekopieerd terwijl bij een value parameter de waarde (aantal bytes afhankelijk van het type)<br />

gekopieerd wordt. Als de waarde veel geheugenruimte in beslag neemt dan is het om performance<br />

redenen beter om een reference parameter te gebruiken. 40 Om er voor te zorgen dat deze functie toch<br />

aangeroepen kan worden met een constante (zonder dat er een tijdelijke kopie wordt gemaakt) kan deze<br />

parameter dan het beste als const reference parameter gedefinieerd worden. Het wordt dan ook onmogelijk<br />

om in de functie een waarde aan de formele parameter toe te kennen.<br />

void drukaf(Tijdsduur td) { // kopieer een Tijdsduur<br />

// ...<br />

}<br />

void drukaf(const Tijdsduur& td) { // kopieer een adres maar<br />

// ... // voorkom toekenningen<br />

}<br />

Als je probeert om een const reference parameter in de functie toch te veranderen krijg je de volgende<br />

foutmelding:<br />

void drukaf(const Tijdsduur& td) {<br />

td.uur=23; // drukaf probeert vals te spelen<br />

// Error: Cannot modify a const object<br />

}<br />

In alle voorafgaande code waarin een Breuk als parameter is gebruikt kan dus beter een const<br />

Breuk& als parameter worden gebruikt. (Bijvoorbeeld op blz. 18, 21 en 27.)<br />

2.17 Parameter FAQ. 41<br />

Q: Wanneer gebruik je een T& parameter in plaats van een T parameter?<br />

A: Gebruik een T& als je wilt dat een toekenning aan de formele parameter (de parameter die gebruikt<br />

wordt in de functiedefinitie) ook een verandering van de actuele parameter (de parameter<br />

die gebruikt wordt in de functieaanroep) tot gevolg heeft.<br />

Q: Wanneer gebruik je een const T& parameter in plaats van een T parameter?<br />

A: Gebruik een const T& als je in de functie de formele parameter niet verandert en als de geheugenruimte<br />

die door een variabele van type T wordt ingenomen meer is dan de geheugenruimte die<br />

nodig is om een adres op te slaan.<br />

40 Later zullen we zien dat er nog een veel belangrijke reden is om een reference parameter in plaats van<br />

een gewone parameter te gebruiken. (Zie paragraaf over polymorfisme.)<br />

41 FAQ=Frequently Asked Questions (veel gestelde vragen).<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

31


32<br />

Q: Wanneer gebruik je een T* parameter in plaats van een T& parameter?<br />

A: Gebruik een T* als je ook “niets” door wilt kunnen geven. Een reference moet namelijk altijd<br />

ergens naar verwijzen. Een pointer kan ook nergens naar wijzen (de waarde van de pointer is dan<br />

0).<br />

2.18 Reference return type.<br />

Als een functie een waarde teruggeeft door middel van een return statement dan wordt de waarde van<br />

de expressie achter het return statement gekopieerd naar de plaats waar de functie aangeroepen is.<br />

Door nu een reference als return type te gebruiken kun je een adres in plaats van een waarde teruggeven.<br />

Dit adres kun je dan gebruiken om een waarde aan toe te kennen.<br />

int& max(int& a, int& b) {<br />

if (a>b) return a;<br />

else return b;<br />

}<br />

int main() {<br />

int x(3), y(4);<br />

max(x,y)=0; // y is nu 0<br />

max(x,y)++; // x is nu 4<br />

// ...<br />

Pas op dat je geen reference naar een lokale variabele teruggeeft. Na afloop van een functie worden de<br />

lokale variabelen uit het geheugen verwijderd. <strong>De</strong> referentie verwijst dan naar een niet meer bestaande<br />

variabele.<br />

int& som(int i1, int i2) {<br />

int s=i1+i2;<br />

return s; // Een gemene fout!<br />

}<br />

// ...<br />

c=som(a, b);<br />

<strong>De</strong>ze fout (het zogenaamde “dangling reference problem”) is erg gemeen omdat de lokale variabele s<br />

natuurlijk niet echt uit het geheugen verwijderd wordt. <strong>De</strong> geheugenplaats wordt alleen vrijgegeven voor<br />

hergebruik. Dit heeft tot gevolg dat de bovenstaande aanroep van som meestal gewoon werkt 42 . <strong>De</strong> fout<br />

in de definitie van de functie som komt pas aan het licht als we de functie bijvoorbeeld als volgt aanroepen:<br />

d=som(c, som(a, b));<br />

Hetzelfde gevaar is trouwens ook aanwezig als je een pointer (bijvoorbeeld Tijdsduur*) als return<br />

type kiest (het zogenaamde “dangling pointer problem”). Je moet er dan voor oppassen dat de pointer<br />

niet naar een variabele wijst die na afloop van de functie niet meer bestaat 43 .<br />

42 <strong>De</strong> C++ Builder 6 compiler herkent het dangling reference probleem en geeft de volgende foutmelding:<br />

“Attempting to return a reference to local variable td”. <strong>De</strong> gcc compiler geeft alleen maar een<br />

warning: “reference to local variable s returned”<br />

43 Vreemd genoeg herkent de C++ Builder 6 compiler het dangling pointer probleem niet. Wel wordt de<br />

volgende warning gegeven: “Suspicious pointer conversion”. <strong>De</strong> gcc compiler geeft de warning:<br />

“address of local variable s returned”.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


2.19 Reference return type (deel 2).<br />

In de bovenstaande Breuk::operator+= memberfunctie zal bij het uitvoeren van het return<br />

statement een kopie van het huidige object worden teruggegeven. Dit is echter niet correct. Want als we<br />

deze operator als volgt gebruiken: (a+=b)+=c; dan wordt eerst a+=b uitgerekend en een kopie van a<br />

teruggegeven, c wordt dan bij deze kopie van a opgeteld waarna de kopie wordt verwijderd. Dit is<br />

natuurlijk niet de bedoeling, het is de bedoeling dat c bij a wordt opgeteld. We kunnen dit probleem<br />

oplossen door een reference naar het object zelf terug te geven. Hiermee zorgen we er meteen voor dat<br />

de expressie (a+=b)+=c gewoon goed (zoals bij integers) werkt. <strong>De</strong> operator+= memberfunctie<br />

kan dan als volgt gedeclareerd worden:<br />

Breuk& operator+=(const Breuk& right);<br />

<strong>De</strong> definitie is dan als volgt:<br />

Breuk& Breuk::operator+=(const Breuk& right) {<br />

boven=boven*right.onder + onder*right.boven;<br />

onder*=right.onder;<br />

normaliseer();<br />

return *this;<br />

}<br />

2.20 Operator overloading (deel2).<br />

Behalve de operator += kun je ook andere operatoren overloaden. Voor de class Breuk kun je bijvoorbeeld<br />

de operator + definiëren, zodat objecten b1 en b2 van de class Breuk gewoon door middel van<br />

b1+b2 opgeteld kunnen worden.<br />

class Breuk {<br />

public:<br />

Breuk();<br />

Breuk(int t);<br />

Breuk(int t, int n);<br />

Breuk& operator+=(const Breuk& right);<br />

const Breuk operator+(const Breuk& right) const;<br />

// ...<br />

};<br />

const Breuk Breuk::operator+(const Breuk& right) const {<br />

Breuk b(*this); // maak een kopie van dit object<br />

b+=right; // tel daar het object right bij op<br />

return b; // geef deze waarde terug<br />

}<br />

int main() {<br />

Breuk a(1,2);<br />

Breuk b(3,4);<br />

Breuk c;<br />

c=a+b;<br />

// ...<br />

Zoals je ziet heb ik de operator+ eenvoudig door gebruik te maken van de operator+= gedefinieerd.<br />

<strong>De</strong> code kan nog verder vereenvoudigd worden tot:<br />

const Breuk Breuk::operator+(const Breuk& right) const {<br />

return Breuk(*this)+=right;<br />

}<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

33


34<br />

2.21 operator+ FAQ.<br />

Q: Waarom gebruik je const Breuk in plaats van Breuk als return type bij operator+?<br />

A: Dit heb ik afgekeken van Scott Meyers (zie de boeken: Effective C++ en More Effective C++).<br />

Als a, b en c van het type int zijn dan levert de expressie (a+b)+=c de volgende (onduidelijke)<br />

foutmelding op "Error: Lvalue required". Dit is goed omdat het optellen van c bij een tijdelijke<br />

variabele (de som a+b wordt namelijk aan een tijdelijke variabele toegekend) zinloos is. <strong>De</strong><br />

tijdelijke variabele wordt namelijk meteen na het berekenen van de expressie weer verwijderd.<br />

Waarschijnlijk heeft de programmeur (a+=b)+=c bedoeld. Als a, b en c van het type Breuk<br />

zijn en we kiezen als return type van Breuk::operator+ het type Breuk dan zal de expressie<br />

(a+b)+=c zonder foutmelding vertaald worden. Als we echter als return type const<br />

Breuk gebruiken dan levert deze expressie wel een foutmelding op omdat de operator+= niet<br />

op een const object uitgevoerd kan worden. 44 Als je het zelfgedefinieerde type Breuk zoveel<br />

mogelijk op het ingebouwde type int wilt laten lijken (en dat wil je) dan moet je dus const<br />

Breuk in plaats van Breuk als return type van de operator+ gebruiken. 45<br />

Q: Kun je de operator+ niet beter een reference return type geven zodat het maken van de kopie<br />

bij return voorkomen wordt?<br />

A: Nee! We hebben een lokale kopie aangemaakt waarin de som is berekend. Als we een reference<br />

teruggeven naar deze lokale kopie ontstaat na return een zogenaamde dangling reference (zie<br />

blz. 32) omdat de lokale variabele na afloop van de memberfunctie opgeruimd wordt.<br />

Q: Kun je in de operator+ de benodigde lokale variabele niet met aanmaken zodat we toch een<br />

reference kunnen teruggeven? <strong>De</strong> met new 46 aangemaakte variabele blijft immers na afloop van<br />

de operator+ memberfunctie gewoon bestaan.<br />

A: Ja dit kan wel, maar je kunt het beter niet doen. <strong>De</strong> met new aangemaakte variabele zal namelijk<br />

(zo lang het programma draait) nooit meer vrijgegeven worden. Dit is een voorbeeld van een<br />

memory leak. Elke keer als er twee breuken opgeteld worden neemt het beschikbare geheugen af!<br />

Q: Kun je de implementatie van operator+ niet nog verder vereenvoudigen tot:<br />

return *this+=right;<br />

A: Nee! Na de bewerking c=a+b; is dan niet alleen c gelijk geworden aan de som van a en b maar<br />

is ook a gelijk geworden aan de som van a en b en dat is natuurlijk niet de bedoeling.<br />

Q: Kun je bij een Breuk ook een int optellen?<br />

Breuk a(1,2);<br />

Breuk b(a+1);<br />

A: Ja. <strong>De</strong> expressie a+1 wordt geïnterpreteerd als a.operator+(1). <strong>De</strong> compiler zal dan “kijken”<br />

of de memberfunctie Breuk::operator+(int) gedefinieerd is. Dit is hier niet het<br />

geval. <strong>De</strong> compiler “ziet” dat er wel een memberfunctie Breuk::operator+(const<br />

Breuk&) gedefinieerd is en zal vervolgens “kijken” of de int omgezet kan worden naar een<br />

Breuk. Dit is in dit geval mogelijk door gebruik te maken van de constructor Breuk(int). <strong>De</strong><br />

compiler maakt dan met deze constructor een tijdelijke variabele van het type Breuk aan en<br />

initialiseert deze variabele met 1. Vervolgens wordt een reference naar deze tijdelijke variabele<br />

als argument aan de operator+ memberfunctie meegegeven. <strong>De</strong> tijdelijke variabele wordt na<br />

het uitvoeren van de operator+ memberfunctie weer opgeruimd.<br />

44 Zoals al eerder vermeld is geeft de C++ Builder 6 compiler bij deze fout geen error (zoals volgens de<br />

standaard zou moeten) maar alleen een warning.<br />

45 Dat dit nogal subtiel is blijkt wel uit het feit dat Bjarne Stroustrup (de ontwerper van C++) in een<br />

vergelijkbaar voorbeeld in zijn boek The C++ programming language 3ed geen const bij het return<br />

type gebruikt.<br />

46 <strong>De</strong> operator new wordt pas besproken op pagina 74 van dit dictaat.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


Q: Kun je bij een int ook een Breuk optellen?<br />

Breuk a(1,2);<br />

Breuk b(1+a);<br />

A: Nee nu niet. <strong>De</strong> expressie 1+a wordt geïnterpreteerd als 1.operator+(a). <strong>De</strong> compiler zal<br />

dan “kijken” of de het ingebouwde type int het optellen met een Breuk heeft gedefinieerd. Dit<br />

is vanzelfsprekend niet het geval. <strong>De</strong> compiler “ziet” dat wel gedefinieerd is hoe een int bij een<br />

int opgeteld moet worden en zal vervolgens “kijken” of de Breuk omgezet kan worden naar een<br />

int. Dit is in dit geval ook niet mogelijk 47 . <strong>De</strong> Borland Builder C++ compiler genereert de<br />

volgende foutmelding:<br />

// Error: illegal structure operation.<br />

<strong>De</strong> gcc compiler genereert een veel duidelijkere foutmelding:<br />

// Error: No match for 'operator+' in '3 + b'.<br />

Als je echter de operator+ niet als memberfunctie definieert maar in plaats daarvan de globale<br />

operator+ overloadt, dan wordt het wel mogelijk om een int bij een Breuk op te tellen.<br />

2.22 Operator overloading (deel 3).<br />

Naast het definiëren van operatoren als memberfuncties van zelf gedefinieerde typen kun je ook de<br />

globale operatoren overloaden 48 . <strong>De</strong> globale operator+ is onder andere gedeclareerd voor het ingebouwde<br />

type int:<br />

const int operator+(int, int);<br />

Merk op dat deze globale operator twee parameters heeft, dit in tegenstelling tot de memberfunctie<br />

Breuk::operator+ die slechts één parameter heeft. Bij deze memberfunctie wordt de parameter<br />

opgeteld bij de receiver. Een globale operator heeft geen receiver dus zijn voor een optelling twee<br />

parameters nodig. Een expressie zoals: a+b zal dus als a en b beide van het type int zijn geïnterpreteerd<br />

worden als operator+(a, b). Je kunt nu door gebruik te maken van operator (=function)<br />

overloading zelf zoveel globale operator+ functies definiëren als je maar wilt.<br />

Als je dus een Breuk bij een int wilt kunnen optellen, kun je de volgende globale operator+<br />

definiëren:<br />

const Breuk operator+(int left, const Breuk& right) {<br />

return right+left;<br />

}<br />

<strong>De</strong>ze implementatie roept simpel de Breuk::operator+ memberfunctie aan!<br />

In plaats van zowel een memberfunctie Breuk::operator+(const Breuk&) en een globale<br />

operator+(int, const Breuk&) te declareren kun je ook één globale operator+(const<br />

Breuk&, const Breuk&) declareren.<br />

Voorbeeld van het overloaden van de globale operator+ voor objecten van de class Breuk.<br />

class Breuk {<br />

public:<br />

Breuk(int t);<br />

Breuk& operator+=(const Breuk& right);<br />

// ...<br />

};<br />

47 Op blz. 37 zal ik bespreken hoe je deze type conversie indien gewenst zelf kunt definiëren.<br />

48 <strong>De</strong> operatoren =, [], () en -> kunnen echter alleen als memberfunctie overloaded worden.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

35


36<br />

const Breuk operator+(const Breuk& left, const Breuk& right) {<br />

Breuk copyLeft(left);<br />

copyLeft+=right;<br />

return copyLeft; 49<br />

}<br />

int main() {<br />

Breuk b(1,2);<br />

Breuk b1(b+3); // wordt: Breuk b1(operator+(b, Breuk(3)));<br />

Breuk b2(3+b); // wordt: Breuk b2(operator+(Breuk(3), b));<br />

// ...<br />

<strong>De</strong> unary operatoren (dat zijn operatoren met 1 operand, zoals !) en de assignment operatoren (zoals<br />

+=) kunnen het beste als memberfunctie overloaded worden. <strong>De</strong> overige binary operatoren kunnen het<br />

beste als gewone functie overloaded worden. In dit geval wordt namelijk (indien nodig) de linker operand<br />

of de rechter operand geconverteerd naar het benodigde type.<br />

2.23 Overloaden operator++ en operator--. (Zie eventueel TICPP<br />

Chapter12.html#Heading353.)<br />

Bij het overloaden van de operator++ en operator-- ontstaat een probleem omdat beide zowel<br />

een prefix als een postfix operator variant kennen 50 . Dit probleem is opgelost door de postfix versie te<br />

voorzien van een (dummy) int argument.<br />

Voorbeeld van het overloaden van operator++ voor objecten van de class Breuk. <strong>De</strong> implementie<br />

van deze memberfuncties wordt op blz. 38 gegeven.<br />

class Breuk {<br />

public:<br />

// ...<br />

Breuk& operator++(); // prefix<br />

const Breuk operator++(int); // postfix<br />

// ...<br />

};<br />

Het gebruik van het return type Breuk& in plaats van const Breuk& bij de prefix operator++<br />

zorgt ervoor dat de expressie ++++b als b van het type Breuk is gewoon werkt (net zoals bij het type<br />

int). Het gebruik van het return type const Breuk in plaats van Breuk bij de postfix operator++<br />

zorgt ervoor dat de expressie b++++ als b van het type Breuk is een error (warning in C++<br />

Builder 6) geeft (net zoals bij het type int).<br />

49 Voor de fijnproever: de implementatie van operator+ kan ook in 1 regel:<br />

const Breuk operator+(const Breuk& left, const Breuk& right) {<br />

return Breuk(left)+=right;<br />

}<br />

Voor de connaisseur (echte kenner): <strong>De</strong> implementatie kan ook zo:<br />

const Breuk operator+(Breuk left, const Breuk& right) {<br />

return left+=right;<br />

}<br />

50 Voor wie het niet meer weet: Een postfix operator ++ wordt na afloop van de expressie uitgevoerd<br />

dus a=b++; wordt geïnterpreteerd als a=b;b=b+1;. <strong>De</strong> prefix operator ++ wordt voorafgaand aan<br />

de expressie uitgevoerd dus a=++b; wordt geïnterpreteerd als b=b+1;a=b;.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


2.24 Conversie operatoren. (Zie eventueel TICPP Chapter12.html#Heading374.)<br />

Een constructor van class Breuk met 1 argument van het type T wordt door de compiler gebruikt als<br />

een variabele van het type T moet worden geconverteerd naar het type Breuk (zie blz. 23). Door het<br />

definiëren van een conversie operator voor het type T kan de programmeur ervoor zorgen dat objecten<br />

van de class Breuk door de compiler (indien nodig) omgezet kunnen worden naar het type T.<br />

Stel dat je wilt dat een Breuk die op een plaats wordt gebruikt waar de compiler een int verwacht,<br />

door de compiler omgezet wordt naar het "gehele deel" van de Breuk dan kun je dit als volgt implementeren:<br />

class Breuk {<br />

// ...<br />

operator int () const;<br />

// ...<br />

};<br />

Breuk::operator int () const {<br />

return boven/onder;<br />

}<br />

Pas op bij conversies: definieer geen conversie waarbij “informatie” verloren gaat! Is het dan wel<br />

verstandig om een Breuk automatisch te laten converteren naar een int?<br />

2.25 Voorbeeld class Breuk (derde versie).<br />

Dit voorbeeld is een uitbreiding van de op blz. 26 gegeven class Breuk (versie 2). In deze versie zijn de<br />

operator == en de operator != toegevoegd. We zullen een nieuwe vriend (friend) leren kennen die ons<br />

helpt bij het implementeren van deze operatoren. Ook zul je leren hoe je een object van de class Breuk<br />

kunt wegschrijven en inlezen door middel van de iostream library, zodat de operator > voor inlezen gebruikt kan worden (door middel van operator overloading).<br />

<strong>De</strong> memberfuncties teller en noemer zijn nu niet meer nodig. Het wegschrijven van een Breuk<br />

(bijvoorbeeld op het scherm) en het inlezen van een Breuk (bijvoorbeeld van het toetsenbord) gaat nu<br />

op precies dezelfde wijze als het wegschrijven en het inlezen van een int. Dit maakt het type Breuk<br />

voor programmeurs erg eenvoudig te gebruiken. Ik zal nu eerst de complete source code presenteren van<br />

een programma waarin het type Breuk gedeclareerd, geïmplementeerd en gebruikt wordt. Meteen<br />

daarna zal ik op bovengenoemde punten één voor één ingaan.<br />

#include <br />

#include <br />

using namespace std;<br />

class Breuk {<br />

public:<br />

Breuk();<br />

Breuk(int t);<br />

Breuk(int t, int n);<br />

Breuk& operator+=(const Breuk& right);<br />

Breuk& operator++(); // prefix<br />

const Breuk operator++(int); // postfix<br />

// ...<br />

// Er zijn nog veel uitbreidingen mogelijk<br />

// ...<br />

private:<br />

int boven;<br />

int onder;<br />

void normaliseer();<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

37


38<br />

friend ostream& operator(istream& left, Breuk& right);<br />

bool operator!=(const Breuk& left, const Breuk& right);<br />

const Breuk operator+(const Breuk& left, const Breuk& right);<br />

// ...<br />

// Er zijn nog veel uitbreidingen mogelijk<br />

// ...<br />

int ggd(int n, int m) {<br />

// ...<br />

}<br />

Breuk::Breuk(): boven(0), onder(1) {<br />

}<br />

Breuk::Breuk(int t): boven(t), onder(1) {<br />

}<br />

Breuk::Breuk(int t, int n): boven(t), onder(n) {<br />

normaliseer();<br />

}<br />

Breuk& Breuk::operator+=(const Breuk& right) {<br />

boven=boven*right.onder + onder*right.boven;<br />

onder*=right.onder;<br />

normaliseer();<br />

return *this;<br />

}<br />

Breuk& Breuk::operator++() {<br />

boven+=onder;<br />

return *this;<br />

}<br />

const Breuk Breuk::operator++(int) {<br />

Breuk b(*this);<br />

++(*this);<br />

return b;<br />

}<br />

void Breuk::normaliseer() {<br />

// ...<br />

}<br />

const Breuk operator+(const Breuk& left, const Breuk& right) {<br />

return Breuk(left)+=right;<br />

}<br />

ostream& operator


ool operator==(const Breuk& left, const Breuk& right) {<br />

return left.boven==right.boven && left.onder==right.onder;<br />

}<br />

bool operator!=(const Breuk& left, const Breuk& right) {<br />

return !(left==right);<br />

}<br />

int main() {<br />

Breuk b1, b2;<br />

coutb1;<br />

coutb2;<br />

cout


40<br />

<strong>De</strong> zojuist besproken globale operator== kan dus eenvoudig als friend van de class Breuk gedeclareerd<br />

worden waardoor de compiler errors als sneeuw voor de zon verdwijnen.<br />

class Breuk {<br />

public:<br />

// ...<br />

private:<br />

// ...<br />

friend bool operator==(const Breuk& left, const Breuk& right);<br />

};<br />

Het maakt niets uit of een friend declaratie in het private of in het public deel van de class geplaatst<br />

wordt. Hoewel een friend function binnen de class gedeclareerd wordt is een friend function géén<br />

memberfunctie maar een globale (normale) functie. In deze friend functie kun je de private members<br />

boven en onder van de class Breuk zonder problemen gebruiken.<br />

In eerste instantie lijkt het er misschien op dat een friend function in tegenspraak is met het principe van<br />

information hiding. Dit is echter niet het geval; een class beslist namelijk zelf wie zijn vrienden zijn.<br />

Voor alle overige functies (geen member en geen friend) geldt nog steeds dat de private datamembers en<br />

private memberfuncties ontoegankelijk zijn. Net zoals in het gewone leven moet een class zijn vrienden<br />

met zorg selecteren. Als je elke functie als friend van elke class declareert worden het principe van<br />

information hiding natuurlijk wel overboord gegooid.<br />

<strong>De</strong> globale operator!= is als volgt overloaded voor het type Breuk:<br />

bool operator!=(const Breuk& left, const Breuk& right) {<br />

return !(left==right);<br />

}<br />

Bij de implementatie is gebruik gemaakt van de al eerder voor het type Breuk overloaded globale<br />

operator==. Het is dus niet nodig om deze operator!= als friend function van de class Breuk te<br />

definiëren.<br />

2.27 Operator overloading (deel 4). (Zie eventueel TICPP Chapter12.html#Heading364.)<br />

Het object cout dat in de headerfile iostream.h gedeclareerd is, is van het type ostream. Om er<br />

voor te zorgen dat je een Breuk b op het scherm kunt afdrukken door middel van: cout


Omdat alle ingebouwde overloaded


42<br />

#ifndef _memcell_ 52<br />

#define _memcell_<br />

class MemoryCell {<br />

public:<br />

int Read() const;<br />

void Write(int x);<br />

private:<br />

int StoredValue;<br />

};<br />

#endif<br />

// Dit is file memcell.cpp<br />

#include "memcell.h"<br />

int MemoryCell::Read() const {<br />

return StoredValue;<br />

}<br />

void MemoryCell::Write(int x) {<br />

StoredValue=x;<br />

}<br />

// Dit is file memappl.cpp<br />

#include <br />

using namespace std;<br />

#include "memcell.h"<br />

int main() {<br />

MemoryCell M;<br />

M.Write(5);<br />

cout


Als je twee double variabelen wilt verwisselen kun je deze functie niet rechtstreeks gebruiken. Waarschijnlijk<br />

ben je op dit moment gewend om de functie swapInts op de volgende wijze te “hergebruiken”:<br />

• maak een kopie van de functie met behulp van de editor functies “knippen” en “plakken”.<br />

• vervang het type int door het type double met behulp van de editor functie “zoek en vervang”.<br />

<strong>De</strong>ze vorm van hergebruik heeft de volgende nadelen:<br />

• Telkens als je zelf een nieuw type definieert (bijvoorbeeld Tijdsduur) waarvoor je de functie<br />

swap ook wilt kunnen gebruiken zul je opnieuw moeten knippen, plakken, zoeken en vervangen.<br />

• Bij een wat ingewikkelder algoritme, bijvoorbeeld sorteren, is het niet altijd duidelijk welke int<br />

je wel en welke int je niet moet vervangen in double als je in plaats van een int array een<br />

double array wilt sorteren. Hierdoor kunnen in een goed getest algoritme toch weer fouten<br />

opduiken.<br />

• Als er zich in het algoritme een logische fout bevindt of als je het algoritme wilt vervangen door<br />

een efficiëntere versie, dan moet je de benodigde wijzigingen in elke gekopieerde versie<br />

aanbrengen.<br />

Door middel van een template functie kun je de handelingen (knippen, plakken, zoeken en vervangen)<br />

die nodig zijn om een functie geschikt te maken voor een ander datatype automatiseren. Je definieert dan<br />

een zogenaamde generieke functie, een functie die je als het ware voor verschillende datatypes kunt<br />

gebruiken.<br />

<strong>De</strong> template functie definitie voor swap ziet er als volgt uit:<br />

template void swap(T& p, T& q) {<br />

T t(p);<br />

p=q;<br />

q=t;<br />

}<br />

Na het keyword template volgt een lijst van template parameters tussen < en >. Een template parameter<br />

zal meestal een type zijn 53 . Dit wordt aangegeven door het keyword typename 54 gevolgd door een<br />

naam voor de parameter. Ik heb hier de naam T gebruikt maar ik had net zo goed de naam VulMaarIn<br />

kunnen gebruiken. <strong>De</strong> template parameter moet 55 in de parameterlijst van de functie gebruikt worden.<br />

<strong>De</strong>ze template functie definitie genereert nog geen enkele machinecode instructie. Het is alleen een<br />

“mal” waarmee (automatisch) functies aangemaakt kunnen worden.<br />

Als je nu de functie swap aanroept zal de compiler zelf afhankelijk van het type van de gebruikte<br />

parameters de benodigde “versie” van swap genereren 56 door voor het template argument (in dit geval<br />

T) het betreffende type in te vullen.<br />

53 Een template kan ook “normale” parameters hebben. Dit komt verderop in dit dictaat aan de orde.<br />

54 In plaats van typename mag ook class gebruikt worden. Omdat het keyword typename pas laat<br />

in de ISO/ANSI C++ standaard is opgenomen gebruiken veel C++ programmeurs en C++ boeken in<br />

plaats van typename nog steeds het “verouderde” class. Het gebruik van typename is op zich<br />

duidelijker omdat bij het gebruik van de template zowel zelfgedefineerde types (classes) als ingebouwde<br />

typen (zoals int) gebruikt kunnen worden.<br />

55 Dit is niet helemaal waar. Zie de volgende voetnoot.<br />

56 Zo’n gegenereerde functie wordt een template instantiation genoemd. Je ziet nu ook waarom de template<br />

parameter in de parameterlijst van de functie definitie gebruikt moet worden. <strong>De</strong> compiler moet<br />

namelijk aan de hand van de gebruikte parameters kunnen bepalen welke functie gegenereerd en/of<br />

aangeroepen moet worden. Als de template parameter niet in de parameterlijst voorkomt dan moet<br />

deze parameter bij het gebruik van de functie (tussen < en > na de naam van de functie) expliciet<br />

opgegeven worden.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

43


44<br />

Dus de aanroep:<br />

int x(3);<br />

int y(4);<br />

swap(x, y);<br />

heeft tot gevolg dat de volgende functie gegenereerd wordt 57 :<br />

void swap(int& p, int& q) {<br />

int t(p);<br />

p=q;<br />

q=t;<br />

}<br />

Als de functie swap daarna opnieuw met twee int’s als parameters aangeroepen wordt, dan wordt<br />

gewoon de al gegenereerde functie aangeroepen. Als echter de functie swap ook als volgt aangeroepen<br />

wordt:<br />

Breuk b(1, 2);<br />

Breuk c(3, 4);<br />

swap(b, c);<br />

dan heeft dit tot gevolg dat een tweede functie swap gegenereerd wordt 58 :<br />

void swap(Breuk& p, Breuk& q) {<br />

Breuk t(p);<br />

p=q;<br />

q=t;<br />

}<br />

Het gebruik van een template functie heeft de volgende voordelen:<br />

• Telkens als je zelf een nieuw type definieert (bijvoorbeeld Tijdsduur) kun je daarvoor de<br />

functie swap ook gebruiken. Natuurlijk moet het type Tijdsduur dan wel de operaties ondersteunen<br />

die in de template op het type T uitgevoerd worden. In dit geval kopiëren (copy constructor)<br />

en assignment (operator=).<br />

• Als er zich in het algoritme een logische fout bevindt of als je het algoritme wilt vervangen door<br />

een efficiëntere versie, dan hoef je de benodigde wijzigingen alleen in de template functie aan te<br />

brengen en het programma opnieuw te compileren.<br />

Het gebruik van een template functie heeft echter ook het volgende nadeel:<br />

• Doordat de compiler de volledige template functie definitie nodig heeft om een functie aanroep te<br />

kunnen vertalen moet de template functie definitie in een headerfile (.h file) opgenomen worden<br />

en “included” worden in elke .cpp file waarin de template functie gebruikt wordt. Het is niet<br />

mogelijk om de template functie afzonderlijk te compileren tot een .obj file en deze later aan de<br />

rest van de code te “linken” zoals dit met een “gewone” functie wel kan.<br />

Bij het ontwikkelen van kleine programma’s zijn deze voordelen misschien niet zo belangrijk maar bij<br />

het ontwikkelen van grote programma’s zijn deze voordelen wel erg belangrijk. Door gebruik te maken<br />

van een template functie in plaats van “met de hand” verschillende versies van een functie aan te maken<br />

wordt een programma beter onderhoudbaar en eenvoudiger uitbreidbaar.<br />

57 Als je zelf deze functie al gedefinieerd hebt dan zal de compiler geen functie genereren maar de al<br />

gedefineerde functie gebruiken. Dit geeft ons de mogelijkheid om een template functie te definiëren<br />

met een aantal uitzonderingen voor bepaalde (van tevoren gedefinieerde) typen.<br />

58 Hier blijkt duidelijk het belang van function name overloading (zie blz. 10).<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


3.2 Template classes. (Zie eventueel TICPP Chapter16.html.)<br />

In de vorige paragraaf hebben we gezien dat je een template van een functie kunt maken waardoor de<br />

functie generiek wordt. Een genierieke functie kun je voor verschillende datatypes gebruiken. Als je de<br />

generieke functie aanroept zal de compiler zelf afhankelijk van het type van de gebruikte parameters de<br />

benodigde “versie” van de generieke functie genereren door voor het template argument het betreffende<br />

type in te vullen. Natuurlijk is het ook mogelijk om een template memberfunctie te definiëren, waarmee<br />

je dus generieke memberfuncties kunt maken. Het is in C++ zelfs mogelijk een hele class generiek te<br />

maken door deze class als template te definiëren. Om te laten zien hoe dit werkt maken we eerste een<br />

class Dozijn waarin je 12 integers kunt opslaan. Daarna maken we een template van deze class zodat<br />

we deze class niet alleen kunnen gebruiken voor het opslaan van 12 integers maar voor het opslaan van<br />

12 elementen van elk type.<br />

<strong>De</strong> class Dozijn waarin 12 integers kunnen worden opgeslagen kan als volgt gedeclareerd worden:<br />

class Dozijn {<br />

public:<br />

void zetIn(int index, int waarde);<br />

int leesUit(int index) const;<br />

private:<br />

int data[12];<br />

};<br />

In een Dozijn kunnen dus 12 integers worden opgeslagen in de private array genaamd data. <strong>De</strong><br />

programmeur die een object van de class Dozijn gebruikt kan een integer in dit object “zetten” door de<br />

boodschap zetIn naar dit object te sturen. <strong>De</strong> plaats waar de integer moet worden “weggezet” wordt<br />

als argument aan dit bericht meegegeven. <strong>De</strong> plaatsen zijn genummerd van 0 t/m 11. <strong>De</strong> waarde 13 kan,<br />

bijvoorbeeld, als volgt op plaats nummer 3 worden weggezet.<br />

Dozijn d;<br />

d.zetIn(3,13);<br />

<strong>De</strong>ze waarde kan uit het object worden “gelezen” door de boodschap leesUit naar het object te<br />

sturen. Bijvoorbeeld:<br />

cout


46<br />

ostream& operator


}<br />

data[index]=waarde;<br />

template const T& Dozijn::leesUit(int index) const {<br />

if (index11)<br />

index=11;<br />

return data[index];<br />

}<br />

<strong>De</strong> memberfunctie leesUit geeft nu de waarde van plaats 0 terug als de index =11 is. Je kunt in dit geval namelijk niet de waarde 0 teruggeven<br />

zoals we bij het Dozijn met integers (op blz. 46) gedaan hebben omdat we niet weten of het type T<br />

wel een waarde 0 kan hebben.<br />

Om de inhoud van een Dozijn van een willekeurig type eenvoudig te kunnen afdrukken is de operator


48<br />

3.4 Template details.<br />

Over templates valt nog veel meer te vertellen:<br />

• Template specialisation. Een speciale versie van een template die alleen voor een bepaald type<br />

(bijvoorbeeld int) of voor bepaalde typen (bijvoorbeeld T*) wordt gebruikt. <strong>De</strong> laatste vorm<br />

wordt partial specialisation genoemd.<br />

• Template memberfuncties. Een class (die zelf geen template hoeft te zijn) kan memberfuncties<br />

hebben die als template gedefinieerd zijn.<br />

• <strong>De</strong>fault waarden voor template parameters. Net zoals voor gewone functie parameters kun je voor<br />

template parameters ook default waarden (of typen) specificeren.<br />

Voor al deze details verwijs ik je naar Volume 2 van TICPP.<br />

3.5 Standaard Templates.<br />

In september 1998 is de ISO/ANSI C++ standaard officieel vastgesteld. In deze standaard zijn een groot<br />

aantal standaard templates voor datastructuren en algoritmen opgenomen. <strong>De</strong>ze in de standaard opgenomen<br />

verzameling tempates is grotendeels afkomstig uit de STL (Standard Template Library) een verzameling<br />

templates die in het begin van de jaren '90 ontwikkeld werd door Alex Stepanov en Meng Lee<br />

van Hewlett Packard Laboratories en sinds 1994 via internet gratis is verspreid. In deze standaard library<br />

is ook een generiek type vector opgenomen.<br />

3.6 std::vector.<br />

<strong>De</strong> template class vector uit de standaard C++ library vervangt de C array. <strong>De</strong>ze vector ondersteunt<br />

net zoals de array de operator[]. <strong>De</strong> voordelen van vector in vergelijking met array:<br />

• Een vector is in tegenstelling tot een built-in array een “echt” object.<br />

• Je kunt een vector “gewoon” vergelijken en toekennen.<br />

• Een vector kan groeien en krimpen.<br />

• Een vector heeft memberfuncties:<br />

• size() geef het aantal elementen in de vector.<br />

• at(...) geef element op de opgegeven positie. Bijna gelijk aan operator[] maar at<br />

controleert of de index geldig is en gooit een exception 60 als dit niet zo is.<br />

• capacity() geef het aantal elementen waarvoor geheugen gereserveerd is. Als de vector<br />

groter groeit worden automatisch meer elementen gereserveerd. <strong>De</strong> capaciteit wordt<br />

dan telkens verdubbeld.<br />

• push_back(...) voeg een element toe aan de vector. Na afloop is de size van de<br />

vector dus met 1 toegenomen.<br />

• resize(...) Verander de size van de vector.<br />

• ...<br />

Voorbeeld van een programma met een vector.<br />

#include <br />

#include <br />

using namespace std;<br />

int main() {<br />

// definieer vector van integers<br />

vector v;<br />

// vul met kwadraten<br />

for (int i=0; i


}<br />

Uitvoer:<br />

}<br />

// druk af<br />

for (vector::size_type i=0; i


50<br />

}<br />

}<br />

cout


4.1 <strong>De</strong> syntax van inheritance. (Zie eventueel TICPP Chapter14.html#Heading406.)<br />

Door middel van overerving (Engels: inheritance) kun je een “nieuwe variant” van<br />

een bestaande class definiëren zonder dat je de bestaande class hoeft te wijzigen en<br />

zonder dat er code gekopieerd wordt. <strong>De</strong> class die als uitgangspunt gebruikt wordt,<br />

wordt de base class genoemd (of ook wel parent class of super class). <strong>De</strong> class die<br />

hiervan afgeleid (Engels: derived) wordt, wordt de derived class genoemd (of ook<br />

wel child class of sub class). Schematisch kan dit zoals hiernaast getekend worden<br />

aangegeven. In C++ code wordt dit als volgt gedeclareerd:<br />

class Winkelier {<br />

// ...<br />

};<br />

class Bloemiste: public 62 Winkelier {<br />

// Bloemiste is afgeleid van Winkelier<br />

// ...<br />

};<br />

Je kunt van een base class meerdere derived classes afleiden. Je kunt een derived class ook weer als base<br />

class voor een nieuwe afleiding gebruiken.<br />

Een derived class heeft (minimaal) dezelfde datamembers en (minmaal) dezelfde public memberfuncties<br />

als de base class waarvan hij is afgeleid. Om deze reden mag je een object van de derived class ook<br />

gebruiken als de compiler een object van de base class verwacht. 63 <strong>De</strong> relatie tussen de derived class en<br />

de base class wordt een IS-A (is een) relatie genoemd. <strong>De</strong> derived class is een (speciaal geval van) base<br />

class. Omdat een derived class datamembers en memberfuncties kan toevoegen aan de base class is het<br />

omgekeerde niet waar. Je kunt een object van de base class niet gebruiken als de compiler een object van<br />

de derived class verwacht. Een base class IS-NOT-A derived class.<br />

<strong>De</strong> derived class erft alle datamembers van de base class over. Dit wil zeggen dat een object van een<br />

derived class (minimaal) dezelfde datamembers heeft als een object van de base class. Private datamembers<br />

uit de base class zijn in objecten van de derived class wel aanwezig maar kunnen vanuit memberfuncties<br />

van de derived class niet rechtstreeks bereikt worden. In de derived class kun je bovendien<br />

“extra” datamembers toevoegen. Als de objecten b en d op onderstaande wijze gedefinieerd zijn dan kan<br />

de structuur van deze objecten weergegeven worden zoals daarnaast getekend is. Het object b bevat<br />

alleen een datamember v en een object d bevat zowel een datamember v als een datamember w. Het<br />

datamember v is alleen toegankelijk vanuit de memberfuncties van de class Base en het datamember w<br />

is alleen toegankelijk vanuit de memberfuncties van de class <strong>De</strong>rived.<br />

class Base {<br />

// ...<br />

private:<br />

int v;<br />

};<br />

class <strong>De</strong>rived: public Base {<br />

// ...<br />

private:<br />

int w;<br />

};<br />

62 Er bestaat ook private inheritance maar dit wordt in de praktijk niet veel gebruikt en zal ik hier dan<br />

ook niet bespreken. Wij gebruiken altijd public inheritance (niet vergeten om het keyword public<br />

achter de : te typen anders krijg je per default private inheritance).<br />

63 Maar pas op voor het slicing probleem dat ik later (blz. 67) zal bespreken.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

51


52<br />

// ...<br />

Base b;<br />

<strong>De</strong>rived d;<br />

Ook erft de derived class alle memberfuncties van de base class over. Dit wil zeggen dat op een object<br />

van een derived class (minimaal) dezelfde memberfuncties uitgevoerd kunnen worden als op een object<br />

van de base class. Private memberfuncties uit de base class zijn in de derived class wel aanwezig maar<br />

kunnen vanuit de derived class niet rechtstreeks aangeroepen worden. In de derived class kun je bovendien<br />

“extra” memberfuncties toevoegen.<br />

Als de objecten b en d op onderstaande wijze gedefinieerd zijn dan kan de memberfunctie getV zowel<br />

op het object b als op het object d uitgevoerd worden. <strong>De</strong> memberfunctie getW is vanzelfsprekend<br />

alleen op het object d uit te voeren. <strong>De</strong> private memberfunctie setV is alleen aan te roepen vanuit de<br />

andere memberfuncties van de class Base en de private memberfunctie setW is alleen aan te roepen<br />

vanuit de andere memberfuncties van de class <strong>De</strong>rived.<br />

class Base {<br />

public:<br />

// ...<br />

int getV() const { return v; } 64<br />

private:<br />

void setV(int i) { v=i; }<br />

int v;<br />

};<br />

class <strong>De</strong>rived: public Base {<br />

public:<br />

// ...<br />

int getW() const { return w; }<br />

private:<br />

void setW(int i) { w=i; }<br />

int w;<br />

};<br />

// ...<br />

Base b;<br />

<strong>De</strong>rived d;<br />

4.2 Polymorfisme. (Zie eventueel TICPP Chapter15.html.)<br />

Als de classes Base en <strong>De</strong>rived zoals hierboven gedefinieerd zijn, dan kan een pointer van het type<br />

Base* niet alleen wijzen naar een object van de class Base maar ook naar een object van de class<br />

<strong>De</strong>rived. Want een <strong>De</strong>rived IS-A (is een) Base. Omdat een pointer van het type Base* naar<br />

objecten van verschillende classes kan wijzen wordt zo’n pointer een polymorfe (veelvormige) pointer<br />

genoemd. Evenzo kan een reference van het type Base& niet alleen verwijzen naar een object van de<br />

class Base maar ook naar een object van de class <strong>De</strong>rived. Want een <strong>De</strong>rived IS-A (is een) Base.<br />

Omdat een reference van het type Base& naar objecten van verschillende classes kan verwijzen wordt<br />

zo’n reference een polymorfe (veelvormige) reference genoemd. Later in dit hoofdstuk zal blijken dat<br />

polymorfisme de kern is waar het bij OOP om draait 65 . Door het toepassen van polymorfisme kan je<br />

64 Alle memberfuncties zijn in dit voorbeeld in de class zelf gedefinieerd. Dit zogenaamd inline definieren<br />

heeft als voordeel dat ik iets minder hoef te typen, maar het nadeel is dat de class niet meer afzonderlijk<br />

gecompileerd kan worden (zie blz. 41).<br />

65 Inheritance dat vaak als de kern van OOP wordt genoemd is mijn inziens alleen een middel om poly-<br />

morfisme te implementeren.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


software maken die eenvoudig aangepast, uitgebreid en hergebruikt kan worden. (Zie het uitgebreide<br />

voorbeeld op blz. 56.)<br />

Als ik nu de volgende functie definieer:<br />

void drukVaf(const Base& p) {<br />

cout


54<br />

bp1->printName();<br />

bp2->printName();<br />

Uitvoer:<br />

Ik ben een Base.<br />

Ik ben een <strong>De</strong>rived.<br />

Het is ook mogelijk om vanuit de in de derived class overridden memberfunctie de orginele functie in de<br />

base class aan te roepen. Als ik het feit dat een <strong>De</strong>rived IS-A Base in het bovenstaande programma<br />

verwerk ontstaat het volgende programma:<br />

class Base {<br />

public:<br />

virtual void printName() {<br />

cout


4.4 Abstract base class. (Zie eventueel TICPP Chapter15.html#Heading447.)<br />

Het is mogelijk om een virtual memberfunctie in een Base class alleen maar te declareren en nog niet te<br />

implementeren. Dit wordt dan een “pure virtual” memberfunctie genoemd en de betreffende base class<br />

wordt een zogenaamde “Abstract Base Class (ABC)”. Een virtual memberfunctie kan pure virtual<br />

gemaakt worden door de declaratie af te sluiten met =0;. Er kunnen geen objecten (variabelen) van een<br />

ABC gedefinieerd worden. Elke concrete derived class die van de ABC overerft is “verplicht” om alle<br />

pure virtual memberfuncties uit de base class te overridden.<br />

4.5 Constructors bij inheritance.<br />

Als een constructor van de derived class aangeroepen wordt, dan wordt automatisch eerst de constructor<br />

(zonder parameters) van de base class aangeroepen. Als je in plaats van de constructor zonder parameters<br />

een andere constructor van de base class wil “aanroepen” vanuit de constructor van de derived class<br />

dan kan dit door deze aanroep in de initialisation list van de constructor van de derived class op te<br />

nemen. <strong>De</strong> base class constructor wordt altijd als eerste uitgevoerd (onafhankelijk van zijn positie in de<br />

initialisation list).<br />

Als de objecten d1 en d2 op onderstaande wijze gedefinieerd zijn dan zijn deze objecten geïnitialiseerd<br />

zoals daarnaast getekend is.<br />

class Base {<br />

public:<br />

Base(): v(0) { }<br />

Base(int i): v(i) { }<br />

private:<br />

int v;<br />

};<br />

class <strong>De</strong>rived: public Base {<br />

public:<br />

<strong>De</strong>rived(): w(1) { } // roept automatisch Base() aan.<br />

<strong>De</strong>rived(i, j): Base(i), w(j) { }<br />

private:<br />

int w;<br />

};<br />

<strong>De</strong>rived d1;<br />

<strong>De</strong>rived d2(3, 4);<br />

4.6 protected members. (Zie eventueel TICPP Chapter14.html#Heading420.)<br />

Het is in C++ ook mogelijk om in een base class datamembers en memberfuncties te definiëren die niet<br />

toegankelijk zijn voor gebruikers van objecten van deze class, maar die wel vanuit de van deze base<br />

class afgeleide classes bereikbaar zijn. Dit wordt in de class declaratie aangegeven door het keyword<br />

protected: te gebruiken.<br />

class Toegang {<br />

public:<br />

// via een object van de class Toegang (voor iedereen) toegankelijk.<br />

protected:<br />

// alleen toegankelijk vanuit classes die direct of indirect afge-<br />

// leid zijn van de class Toegang en vanuit de class Toegang zelf.<br />

private:<br />

// alleen toegankelijk vanuit de class Toegang zelf.<br />

};<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

55


56<br />

Het definiëren van protected datamembers wordt afgeraden omdat dit slecht is voor de onderhoudbaarheid<br />

van de code. Als een protected datamember een illegale waarde krijgt moet alle source code worden<br />

doorzocht om de plaatsen te vinden waar deze datamember veranderd wordt. In elke afgeleide class is<br />

het protected datamember namelijk te veranderen. Het is soms wel zinvol om protected memberfuncties<br />

te definiëren. <strong>De</strong>ze protected functies kunnen dan niet door gebruikers van de objecten van deze class<br />

worden aangeroepen maar wel in de memberfuncties van afgeleide classes.<br />

4.7 Voorbeeld: ADC kaarten.<br />

Ik zal nu een praktisch programmeerprobleem beschrijven. Vervolgens zal ik een gestructureerde oplossing,<br />

een oplossing door middel van een ADT en een object georiënteerde oplossing bespreken. 68 Daarna<br />

zal ik deze oplossingen met elkaar vergelijken (nu mag je één keer raden welke oplossing de beste zal<br />

blijken te zijn:-).<br />

4.7.1 Probleemdefinitie.<br />

In een programma om een machine te besturen moeten bepaalde signalen via een ADC kaart (ADC =<br />

Analoog Digitaal Converter) ingelezen worden. Het programma moet met 2 verschillende typen ADC<br />

kaarten kunnen werken. <strong>De</strong>ze kaarten hebben de typenamen AD178 en NI323. <strong>De</strong>ze kaarten zijn functioneel<br />

gelijk en hebben beide een 8 kanaals 16 bits ADC met instelbare voorversterker. Het initialiseren<br />

van de kaarten, het selecteren van een kanaal, het uitlezen van de “sampled” waarde en het instellen van<br />

de versterkingsfactor moet echter bij elke kaart op een andere wijze gebeuren (andere adressen, andere<br />

bits en/of andere procedures). In de applicatie moeten meerdere ADC kaarten van verschillende typen<br />

gebruikt kunnen worden. In de applicatie is alleen de spanning in volts van de verschillende signalen van<br />

belang. Voor beide 16 bits ADC kaarten geldt dat deze spanning U als volgt berekend kan worden:<br />

U=S*F/6553.5 [V]. S is de “sampled” 16 bits waarde (two’s complement) en F is de ingestelde versterkingsfactor.<br />

Hoe kun je in het programma nu het beste met de verschillen tussen de kaarten omgaan.<br />

4.7.2 Een gestructureerde oplossing.<br />

Eerst zal ik bespreken hoe je dit probleem op een gestructureerde manier oplost. Met de methode van<br />

functionele decompositie deel ik het probleem op in een aantal deelproblemen. Voor elk deelprobleem<br />

definieer ik vervolgens een functie:<br />

• initCard voor het initialiseren van de kaart,<br />

• selectChannel voor het selecteren van een kanaal,<br />

• getChannel voor het opvragen van het op dit moment geselecteerde kanaal,<br />

• setAmplifier voor het instellen van de versterkingsfactor,<br />

• sampleCard voor het uitlezen van een sample en<br />

• readCard voor het uitlezen van de spanning in volts.<br />

Voor elke kaart die in het programma gebruikt wordt moeten een aantal gegevens bijgehouden worden<br />

zoals: de kaartsoort, de ingestelde versterkingsfactor en het geselecteerde kanaal. Om deze gegevens per<br />

kaart netjes bij elkaar te houden heb ik de struct ADCCard gedeclareerd. Voor elke kaart die in het<br />

programma gebruikt wordt, wordt dan een variabele van dit struct type aangemaakt. Aan elk van de<br />

eerder genoemde functies wordt de te gebruiken kaart dan als parameter van het type struct ADCCard<br />

doorgegeven.<br />

enum CardName {AD178, NI323};<br />

struct ADCCard {<br />

CardName t; // card type<br />

double f; // amplifying factor<br />

68 In de theorielessen zal ik een ander (minder praktisch, maar mijns inziens wel leuker) voorbeeld be-<br />

spreken.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


};<br />

int c; // selected channel<br />

void initCard(ADCCard& card, CardName name) {<br />

card.t=name;<br />

card.f=1.0;<br />

card.c=1;<br />

// eventueel voor alle kaarten benodigde code<br />

switch (card.t) {<br />

case AD178:<br />

// de specifieke voor de AD178 benodigde code<br />

cout


58<br />

ADCCard adc;<br />

initCard(adc, AD178);<br />

setAmplifier(adc, 10);<br />

selectChannel(adc, 3);<br />

cout


ADCCard::ADCCard(CardName name): t(name), f(1.0), c(1) {<br />

// eventueel voor alle kaarten benodigde code<br />

switch (t) {<br />

case AD178:<br />

// de specifieke voor de AD178 benodigde code<br />

cout


60<br />

Op zich is dit nadeel bij deze oplossing minder groot dan bij de gestructureerde oplossing omdat in dit<br />

geval alleen de ADT ADCCard aangepast hoeft te worden, terwijl in de gestructureerde oplossing de<br />

gehele applicatie (kan enkele miljoenen regels code zijn) doorzocht moet worden op het gebruik van het<br />

betreffende switch statement. Toch kan ook de oplossing door middel van een ADT tot een probleem<br />

voor wat betreft de uitbreidbaarheid leiden.<br />

Stel dat ik niet zelf het ADT ADCCard heb ontwikkeld, maar dat ik deze herbruikbare ADT heb ingekocht.<br />

Als een geschikte ADT te koop is dan heeft kopen de voorkeur boven zelf maken om een aantal<br />

redenen:<br />

• <strong>De</strong> prijs van de ADT zal waarschijnlijk zodanig zijn dat zelf maken al snel duurder wordt. Als de<br />

prijs van de bovenstaande ADT 100 Euro is (een redelijke schatting), dan betekent dit dat je zelf<br />

(als beginnende professionele programmeur) de ADT in minder dan één dag moet ontwerpen,<br />

implementeren, testen en documenteren om goedkoper uit te zijn.<br />

• <strong>De</strong> gekochte ADT is hoogst waarschijnlijk uitgebreid getest. Zeker als het product al enige tijd<br />

bestaat zullen de meeste bugs er inmiddels uit zijn. <strong>De</strong> kans dat de applicatie plotseling niet meer<br />

werkt als de kaarten in een andere PC geprikt worden is met een zelf ontwikkelde ADT groter dan<br />

met een gekochte ADT.<br />

• Als de leverancier van de AD178 kaart een hardware bug oplost waardoor ook de software aansturing<br />

gewijzigd moet worden dan zal de leverancier van de ADT (hopelijk) ook een nieuwe versie<br />

uitbrengen.<br />

Het is waarschijnlijk dat de leverancier van de ADT alleen de adccard.h file en de adccard.obj<br />

file aan ons levert maar de source code adccard.cpp file niet aan ons beschikbaar stelt 70 . Het toevoegen<br />

van de nieuwe ADC kaart (BB647) is dan niet mogelijk.<br />

<strong>De</strong> (gekochte) ADT ADCCard is een herbruikbare software component die echter niet door de gebruiker<br />

uit te breiden is. Je zult zien dat je door het toepassen van de object georiënteerde technieken inheritance<br />

en polymorfisme wel een software component kan maken die door de gebruiker uitgebreid kan worden<br />

zonder dat de gebruiker de source code van de originele component hoeft te wijzigen.<br />

4.7.4 Een object georiënteerde oplossing.<br />

Bij het toepassen van de object georiënteerde benadering constateer ik<br />

dat in het programma twee typen ADC kaarten gebruikt worden. Beide<br />

kaarten hebben dezelfde functionaliteit en kunnen dus worden afgeleid<br />

van dezelfde base class 71 . Dit is hier schematisch weergeven. Ik heb de<br />

base class als volgt gedeclareerd:<br />

class ADCCard {<br />

public:<br />

ADCCard();<br />

virtual ~ADCCard() 72 ;<br />

virtual void selectChannel(int channel) =0;<br />

int getChannel() const;<br />

70 Ook als de source code wel beschikbaar is dan willen we deze code, om redenen van<br />

onderhoudbaarheid, liever niet wijzigen. Want als de leverancier dan met een nieuwe versie van de<br />

code komt, dan moeten we ook daarin al onze wijzigingen weer doorvoeren.<br />

71 Het bepalen van de benodigde classes en hun onderlinge relaties is bij grotere programma’s niet zo<br />

eenvoudig. Het bepalen van de benodigde classes en hun relaties wordt OOA (Object Oriented Analyse)<br />

en OOD (Object Oriented <strong>De</strong>sign) genoemd. Hoe je dit moet doen wordt voor E studenten later<br />

in deze onderwijseenheid behandeld. Bij TI is dit al behandeld en komt het later in andere onderwijseenheden<br />

nog uitgebreid aan de orde.<br />

72 Dit is een zoganaamde virtual destructor, waarom het nodig is om een virtual destructor te definiëren<br />

wordt verderop in dit dictaat besproken.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


virtual void setAmplifier(double factor) =0;<br />

double read() const;<br />

protected:<br />

void rememberChannel(int channel);<br />

void rememberAmplifier(double factor);<br />

private:<br />

double f; // amplifying factor<br />

int c; // selected channel<br />

virtual int sample() const =0;<br />

};<br />

<strong>De</strong> memberfuncties selectChannel, setAmplifier en sample heb ik pure virtual (zie blz. 55)<br />

gedeclareerd omdat de implementatie per kaart type verschillend is. Dit maakt de class ADCCard<br />

abstract. Je kunt dus geen objecten van dit type definiëren, alleen references en pointers. Elke afgeleide<br />

concrete class (elk kaarttype) moet deze functies “overridden”. <strong>De</strong> memberfuncties getChannel en<br />

read heb ik non-virtual gedeclareerd omdat het niet mijn bedoeling is dat een derived class deze<br />

functies “override”. Wat er gebeurt als je dit toch probeert zal ik op blz. 64 bespreken. <strong>De</strong> datamembers<br />

f en c heb ik protected gedeclareerd om er voor te zorgen dat ze vanuit de derived classes bereikbaar<br />

zijn. <strong>De</strong> memberfunctie sample heb ik private gedefinieerd omdat deze functie alleen vanuit de memberfunctie<br />

read gebruikt hoeft te kunnen worden. <strong>De</strong>rived classes moeten deze memberfunctie dus wel<br />

definiëren maar ze mogen hem zelf niet aanroepen! Ook gebruikers van deze derived classes hebben<br />

mijn inziens de memberfunctie sample niet nodig. Ze worden dus door mij verplicht de memberfunctie<br />

read te gebruiken zodat de returnwaarde altijd in volts is (ik hoop hiermee fouten bij het gebruik te<br />

voorkomen).<br />

Van de abstracte base class ADCCard heb ik vervolgens de concrete classes AD178 en NI323 afgeleid.<br />

class AD178: public ADCCard {<br />

public:<br />

AD178();<br />

virtual void selectChannel(int channel);<br />

virtual void setAmplifier(double factor);<br />

private:<br />

virtual int sample() const;<br />

};<br />

class NI323: public ADCCard {<br />

public:<br />

NI323();<br />

virtual void selectChannel(int channel);<br />

virtual void setAmplifier(double factor);<br />

private:<br />

virtual int sample() const;<br />

};<br />

Ik heb operator overloading toegepast om een object van een van ADCCard afgeleide class eenvoudig te<br />

kunnen afdrukken:<br />

ostream& operator


62<br />

}<br />

double ADCCard::read() const {<br />

return sample()*f/6553.5;<br />

}<br />

void ADCCard::rememberChannel(int channel) {<br />

c=channel;<br />

}<br />

void ADCCard::rememberAmplifier(double factor) {<br />

f=factor;<br />

}<br />

AD178::AD178() {<br />

// de specifieke voor de AD178 benodigde code<br />

}<br />

void AD178::selectChannel(int channel) {<br />

rememberChannel(channel);<br />

// de specifieke voor de AD178 benodigde code<br />

}<br />

void AD178::setAmplifier(double factor) {<br />

rememberAmplifier(factor);<br />

// de specifieke voor de AD178 benodigde code<br />

}<br />

int AD178::sample() const {<br />

int sample;<br />

// de specifieke voor de AD178 benodigde code<br />

return sample;<br />

}<br />

NI323::NI323() {<br />

// de specifieke voor de NI323 benodigde code<br />

}<br />

void NI323::selectChannel(int channel) {<br />

rememberChannel(channel);<br />

// de specifieke voor de NI323 benodigde code<br />

}<br />

void NI323::setAmplifier(double factor) {<br />

rememberAmplifier(factor);<br />

// de specifieke voor de NI323 benodigde code<br />

}<br />

int NI323::sample() const {<br />

int sample;<br />

// de specifieke voor de NI323 benodigde code<br />

return sample;<br />

}<br />

<strong>De</strong> onderstaande functie heb ik een polymorfe parameter gegeven zodat hij met elk type ADC kaart<br />

gebruikt kan worden.<br />

void doIt(ADCCard& c) {<br />

c.setAmplifier(10);<br />

c.selectChannel(3);<br />

cout


doIt(card2);<br />

// ...<br />

Merk op dat door de overloaded operator


64<br />

4.8 Overloading en overriding van memberfuncties.<br />

Het onderscheid tussen memberfunctie overloading en memberfunctie overriding is van groot belang. Op<br />

blz. 10 heb je gezien dat een functienaam meerdere keren gebruikt (overloaded) kan worden. <strong>De</strong> compiler<br />

zal aan de hand van de gebruikte argumenten de juiste functie selecteren. Dit maakt deze functies<br />

eenvoudiger te gebruiken omdat de gebruiker (de programmeur die deze functies aanroept) slechts 1<br />

naam hoeft te onthouden. Elke functienaam, dus ook een memberfunctienaam, kan overloaded worden.<br />

Een memberfunctie die een andere memberfunctie overload heeft dus dezelfde naam. Bij een aanroep<br />

van de memberfunctienaam wordt de juiste memberfunctie door de compiler aangeroepen door naar de<br />

argumenten bij aanroep te kijken.<br />

Voorbeeld 73 van het gebruik van overloading van memberfuncties:<br />

class Class {<br />

public:<br />

void f() const {<br />

cout


};<br />

void f(int i) const 74 { // Verberg f() !! Geen goed idee !!!<br />

cout


66<br />

Bas besluit nu om zijn class Base uit te breiden en voegt een functie f toe:<br />

// Aangepaste code van Bas<br />

class Base {<br />

public:<br />

// ...<br />

void f(int i) const {<br />

cout


};<br />

class <strong>De</strong>rived: public Base {<br />

public:<br />

void f(int i) const {<br />

cout


68<br />

class Docent: public Mens {<br />

public:<br />

Docent(): salaris(30000) {<br />

}<br />

virtual void printSoort() {<br />

cout


#include <br />

#include <br />

using namespace std;<br />

class Fruit {<br />

public:<br />

virtual void printSoort() const =0;<br />

// ...<br />

};<br />

class Appel: public Fruit {<br />

public:<br />

virtual void printSoort() const {<br />

cout


70<br />

Omdat de vector nu pointers naar de objecten bevat, moeten we zelf goed opletten dat deze objecten<br />

niet verwijderd worden voordat de vector wordt verwijderd.<br />

4.11 Voorbeeld: Impedantie calculator.<br />

In dit uitgebreide praktijkvoorbeeld kun je zien hoe de OOP technieken die je tot nu toe hebt geleerd<br />

kunnen worden toegepast bij het maken van een elektrotechnische applicatie.<br />

4.11.1 Weerstand, spoel en condensator.<br />

Passieve elektrische componenten (hierna componenten genoemd) hebben een complexe impedantie Z.<br />

<strong>De</strong>ze impedantie is een functie van de frequentie f. Componenten hebben een impedantie en een waarde.<br />

Er bestaan 3 soorten (basis)componenten:<br />

• R (weerstand): Z=waarde.<br />

• L (spoel): Z=j*2*π*frequentie*waarde.<br />

• C (condensator): Z=-j/(2*π*frequentie*waarde).<br />

<strong>De</strong> classes Component, R, L en C hebben de volgende relaties (zie ook<br />

nevenstaande figuur):<br />

• een R is een Component.<br />

• een L is een Component.<br />

• een C is een Component.<br />

We willen een programma maken waarin gebruik gemaakt kan worden van<br />

passieve elektrische componenten. <strong>De</strong> ABC (Abstract Base Class) Component<br />

kan dan als volgt gedefinieerd worden:<br />

class Component {<br />

public:<br />

virtual ~Component() 81 {<br />

}<br />

virtual complex Z(double f) const=0;<br />

virtual void print(ostream& o) const=0;<br />

};<br />

Het type complex is opgenomen in de ISO/ANSI standaard C++ library. Zie eventueel Volume 2 van<br />

TICPP.<br />

<strong>De</strong> functie Z moet in een van Component afgeleide class de impedantie berekenen (een complex getal)<br />

bij de als parameter meegegeven frequentie f. <strong>De</strong> functie print moet in een van Component afgeleide<br />

class het type en de waarde afdrukken op de als parameter meegegeven output stream o. Bijvoorbeeld:<br />

L(1E-3) voor een spoel van 1mH.<br />

Als we componenten ook met behulp van de operator


}<br />

cout


72<br />

Antwoord:<br />

class R: public Component { // R=Weerstand<br />

public:<br />

R(double r): value(r) {<br />

}<br />

virtual complex Z(double) const {<br />

return value;<br />

}<br />

virtual void print(ostream& o) const {<br />

o


serieschakeling wilt doorrekenen van een R, L en C maak je bijvoorbeeld eerst van de R en L een serieschakeling,<br />

die je vervolgens met de C combineert tot een tweede serieschakeling. Op soortgelijke wijze<br />

kun je het programma uitbreiden met parallelschakelingen. Met dit programma kun je dan van elk<br />

passief elektrisch netwerk de impedantie berekenen.<br />

<strong>De</strong> classes S en P kunnen dan als volgt gebruikt worden:<br />

int main() {<br />

R r1(1E2);<br />

C c1(1E-6);<br />

L l1(3E-2);<br />

S s1(r1, c1);<br />

S s2(r1, l1);<br />

P p(s1, s2);<br />

printImpedanceTable(p);<br />

// ...<br />

Je ziet dat je de al bestaande polymorfe functie printImpedanceTable ook voor objecten van de<br />

nieuwe classes S en P kunt gebruiken!<br />

<strong>De</strong> uitvoer van het bovenstaande programma is:<br />

Impedantie tabel voor: ((R(100)+C(1e-06))//(R(100)+L(0.03)))<br />

freq Z<br />

10 (100.016,1.25659)<br />

100 (101.591,12.5146)<br />

1000 (197.893,-14.3612)<br />

10000 (101.132,-10.5795)<br />

100000 (100.011,-1.061)<br />

1e+06 (100,-0.106103)<br />

Vraag:<br />

Implementeer nu zelf de classes S en P.<br />

Antwoord:<br />

class S: public Component { // S = Serieschakeling van 2 componenten<br />

public:<br />

S(const Component& c1, const Component& c2): comp1(c1), comp2(c2) {<br />

}<br />

virtual complex Z(double f) const {<br />

return comp1.Z(f)+comp2.Z(f);<br />

}<br />

virtual void print(ostream& o) const {<br />

o


74<br />

}<br />

virtual void print(ostream& o) const {<br />

o


Dit kan in C++ met de operatoren new en delete. Voor het dynamisch aanmaken en verwijderen van<br />

array’s (waarvan de grootte dus tijdens het uitvoeren van het programma bepaald kan worden) beschikt<br />

C++ over de operatoren new[] en delete[] (zie eventueel TICPP Chapter13.html#Heading393). <strong>De</strong><br />

operatoren new en new[] geven een pointer naar het nieuw gereserveerde geheugen terug. <strong>De</strong>ze pointer<br />

kan dan gebruikt worden om dit geheugen te gebruiken. Als dit geheugen niet meer nodig is dan kan dit<br />

worden vrijgegeven door de operator delete of delete[] uit te voeren op de pointer die bij new of<br />

respectievelijk new[] is teruggegeven. <strong>De</strong> met new aangemaakte variabelen bevinden zich in een<br />

speciaal geheugengebied “heap” genaamd. Tegenover het voordeel van dynamische geheugenallocatie,<br />

een grotere flexibiliteit, staat het gevaar van een geheugenlek (memory leak). Een geheugenlek ontstaat<br />

als een programmeur vergeet een met new aangemaakte variabele weer met delete te verwijderen.<br />

In C werden de functies malloc en free gebruikt om geheugenruimte op de "heap" te reserveren en<br />

weer vrij te geven. <strong>De</strong>ze functies zijn echter niet "type veilig" omdat het return type van malloc een<br />

void* is die vervolgens door de gebruiker naar het gewenste type moet worden omgezet. <strong>De</strong> compiler<br />

merkt het dus niet als de gebruikte "type aanduidingen" niet overeenkomen. Om deze reden zijn in C++<br />

nieuwe memory allocatie operatoren (new en delete) toegevoegd. Bij het ontwerpen van nieuwe<br />

software kan je het best van deze nieuwe operatoren gebruik maken.<br />

Voorbeeld met new en delete:<br />

double* dp(new double); // reserveer een double<br />

int i; cin>>i;<br />

double* drij(new double[i]); // reserveer een array met i doubles<br />

// ...<br />

delete dp; // geef de door dp aangewezen geheugenruimte vrij<br />

delete[] drij; // idem voor de door drij aangewezen array<br />

In paragraaf 5.5 zul je zien hoe, door het gebruik van dynamische geheugenallocatie in plaats van het<br />

gebruik van een statische array, geen grens gesteld hoeft te worden aan het aantal elementen in een<br />

array. <strong>De</strong> enige grens is dan de grootte van het beschikbare (virtuele) werkgeheugen.<br />

5.2 <strong>De</strong>structor ~Breuk. (Zie eventueel TICPP Chapter06.html#Heading226.)<br />

Een class kan naast een aantal constructors (zie blz. 22) ook één destructor hebben. <strong>De</strong> destructor heeft<br />

als naam, de naam van de class voorafgegaan door het teken ~. Als programmeur hoef je niet zelf de<br />

destructor aan te roepen. <strong>De</strong> compiler zorgt ervoor dat de destructor aangeroepen wordt net voordat het<br />

object opgeruimd wordt. Dit is:<br />

• aan het einde van het blok waarin de variabele gedefinieerd is voor een lokale variabele.<br />

• aan het einde van het programma voor globale variabele.<br />

• bij het aanroepen van delete voor dynamische variabelen.<br />

Als voorbeeld zullen we aan de eerste versie van de class Breuk (zie blz. 20) een destructor toevoegen:<br />

class Breuk {<br />

public:<br />

Breuk();<br />

Breuk(int t);<br />

Breuk(int t, int n);<br />

~Breuk();<br />

// rest van de class Breuk is niet gewijzigd.<br />

};<br />

Breuk::~Breuk() {<br />

cout


76<br />

}<br />

cin.get();<br />

Het hoofdprogramma om de eerste versie van Breuk te testen is (zie blz. 22):<br />

int main() {<br />

Breuk b1(4);<br />

cout


volgorde uit het geheugen verwijderd 82 . In de destructor ~Auto (die wordt aangeroepen net voordat de<br />

geheugenruimte van het Auto object wordt vrijgegeven) kunnen we het Auto object nog gewoon naar<br />

de autosloperij rijden. We weten zeker dat de Wiel objecten nog niet zijn verwijderd omdat het Auto<br />

object later is aangemaakt, en dus eerder wordt verwijderd. Iets uit elkaar halen gaat ook altijd in de<br />

omgekeerde volgorde dan iets in elkaar zetten dus het is logisch dat het opruimen van objecten in de<br />

omgekeerde volgorde van aanmaken gaat.<br />

We zien dat halverwege de functie main ook nog een Breuk object wordt verwijderd. Dit lijkt op het<br />

eerste gezicht wat vreemd. Als we goed kijken zien we dat deze destructor wordt aangeroepen bij echt<br />

uitvoeren van de volgende regel uit main:<br />

b3.plus(5);<br />

Snap je welk Breuk object aan het einde van deze regel wordt verwijderd? Lees indien nodig paragraaf<br />

2.5 nog eens door. <strong>De</strong> memberfunctie plus verwacht een Breuk object als argument. <strong>De</strong> integer<br />

5 wordt met de constructor Breuk(5) “omgezet naar” een Breuk object. Dit impliciet door de compiler<br />

aangemaakte object wordt dus aan het einde van de regel (nadat de memberfunctie plus is aangeroepen<br />

en teruggekeerd) weer uit het geheugen verwijderd.<br />

Als geen destructor gedefinieerd is dan wordt door de compiler een default destructor aangemaakt. <strong>De</strong>ze<br />

default destructor roept voor elk dataveld de destructor van dit veld aan (=memberwise destruction). In<br />

dit geval heb ik de destructor ~Breuk een melding op het scherm laten afdrukken. Dit is in feite nutteloos<br />

en ik had net zo goed de door de compiler gegenereerde default destructor kunnen gebruiken.<br />

5.3 <strong>De</strong>structors bij inheritance.<br />

In paragraaf 4.5 hebben we gezien dat als een constructor van de derived class aangeroepen wordt,<br />

automatisch eerst de constructor (zonder parameters) van de base class wordt aangeroepen. Als de<br />

destructor van de derived class automatisch (door de compiler) wordt aangeroepen dan wordt daarna<br />

automatisch de destructor van de base class aangeroepen. <strong>De</strong> base class destructor wordt altijd als<br />

laatste uitgevoerd. Snap je waarom? 83<br />

5.4 Virtual destructor. (Zie eventueel TICPP Chapter15.html#Heading456.)<br />

Als een class nu of in de toekomst als base class gebruikt wordt dan moet de destructor virtual zijn zodat<br />

van deze class afgeleide classes via een polymorfe pointer “deleted” kan worden.<br />

Hier volgt een voorbeeld van het gebruik van een ABC en polymorfisme:<br />

class Fruit {<br />

public:<br />

virtual ~Fruit() {<br />

cout


78<br />

};<br />

virtual ~Appel() {<br />

cout


Als in de base class Fruit geen virtual destructor gedefinieerd wordt maar een gewone (non-virtual)<br />

destructor dan wordt de uitvoer als volgt:<br />

<strong>De</strong> fruitmand bevat:<br />

Appel.<br />

Peer.<br />

Appel.<br />

Er is een stuk Fruit verwijderd.<br />

Er is een stuk Fruit verwijderd.<br />

Er is een stuk Fruit verwijderd.<br />

Dit komt doordat de destructor via een polymorfe pointer (zie blz. 52) aangeroepen wordt. Als de<br />

destructor virtual gedefinieerd is dan wordt tijdens het uitvoeren van het programma bepaald naar welk<br />

type object (een appel of een peer) deze pointer wijst. Vervolgens wordt de destructor van deze class<br />

(Appel of Peer) aangeroepen 84 . Omdat de destructor van een derived class ook altijd de destructor van<br />

zijn base class aanroept (zie blz. 55) wordt de destructor van Fruit ook aangeroepen. Als de destructor<br />

niet virtual gedefinieerd is dan wordt tijdens het compileren van het programma bepaald van welk type<br />

de pointer is. Vervolgens wordt de destructor van deze class (Fruit) aangeroepen 85 . In dit geval wordt<br />

dus alleen de destructor van de base class aangeroepen.<br />

Let op! Als we een class aanmaken zonder destructor dan zal de compiler zelf een zogenaamde default<br />

destructor aanmaken. <strong>De</strong>ze automatisch aangemaakte destructor is echter niet virtual. In elke class die<br />

nu of in de toekomst als base class gebruikt wordt moeten we dus zelf een virtual destructor definiëren!<br />

5.5 Voorbeeld class Array.<br />

Het in C (en dus ook in C++) ingebouwde array type heeft een aantal nadelen en beperkingen. <strong>De</strong><br />

belangrijkste daarvan zijn:<br />

• <strong>De</strong> grootte van de array moet bij het compileren van het programma bekend zijn. In de praktijk<br />

komt het vaak voor dat pas bij het uitvoeren van het programma bepaald kan worden hoe groot de<br />

array moet zijn.<br />

• Er vindt bij het gebruiken van de array geen controle plaats op de gebruikte index. Als je element<br />

N benadert uit een array die N-1 elementen heeft, dan krijg je geen foutmelding maar wordt de<br />

geheugenplaats “achter” het einde van de array benaderd. Dit is vaak de oorzaak van fouten die<br />

lang onopgemerkt kunnen blijven en dan (volgens de wet van Murphy op het moment dat het het<br />

slechtst uitkomt) plotseling voor de dag kunnen komen. <strong>De</strong>ze fouten zijn vaak heel moeilijk te<br />

vinden omdat de oorzaak van de fout niets met het gevolg van de fout te maken heeft.<br />

In dit voorbeeld zul je zien hoe ik zelf een eigen array type genaamd Array heb gedefinieerd 86 waarbij:<br />

• de grootte pas tijdens het uitvoeren van het programma bepaald kan worden.<br />

• bij het indexeren de index wordt gecontroleerd en een foutmelding wordt gegeven als de index<br />

zich buiten de grenzen van de Array bevindt.<br />

<strong>De</strong> door de compiler gegenereerde copy constructor, destructor en operator= blijken voor de class<br />

Array niet correct te werken. Ik heb daarom voor deze class zelf een copy constructor, destructor en<br />

operator= gedefinieerd. Na het voorbeeld worden de belangrijkste aspecten toegelicht en worden<br />

verwijzingen naar het TICPP boek gegeven.<br />

84 <strong>De</strong> destructor is dan dus overridden.<br />

85 <strong>De</strong> destructor is dan dus overloaded.<br />

86 In de ISO/ANSI standaard C++ library is ook een type std::vector opgenomen. Dit type is in<br />

paragraaf 3.6 besproken. Het verschil tussen onze zelfgemaakte Array en std::vector is dat een<br />

std::vector object kan groeien en krimpen nadat het is aangemaakt.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

79


80<br />

#include <br />

#include <br />

#include <br />

using namespace std;<br />

class Array {<br />

public:<br />

explicit Array(int s);<br />

Array(const Array& r);<br />

Array& operator=(const Array& r);<br />

~Array();<br />

int& operator[](int index);<br />

const int& operator[](int index) const;<br />

int length() const;<br />

bool operator==(const Array& r) const;<br />

bool operator!=(const Array& r) const;<br />

// ...<br />

// Er zijn vele uitbreidingen mogelijk.<br />

private:<br />

int size;<br />

int* data;<br />

};<br />

Array::Array(int s): size(s), data(new int[s]) {<br />

}<br />

Array::Array(const Array& r): size(r.size), data(new int[r.size]) {<br />

for (int i(0); i=0 && index=0 && index


}<br />

int Array::length() const {<br />

return size;<br />

}<br />

bool Array::operator==(const Array& r) const {<br />

if (size!=r.size)<br />

return false;<br />

for (int i(0);i0) {<br />

Array a(i);<br />

for (int j(0); j


82<br />

• een object als waarde wordt teruggegeven vanuit een functie.<br />

<strong>De</strong> compiler zal als de programmeur geen copy constructor definieert zelf een default copy constructor<br />

genereren. <strong>De</strong>ze default copy constructor kopieert elk deel waaruit de class bestaat vanuit de een naar de<br />

andere (=memberwise copy). Naast de default copy constructor genereert de compiler ook (indien niet<br />

door de programmeur gedefinieerd) een default assignment operator en een default destructor. <strong>De</strong><br />

default assignment operator doet een memberwise assignment en de default destructor doet een memberwise<br />

destruction.<br />

Dat je voor de class Array zelf een destructor moet<br />

definiëren waarin je het in de constructor met new<br />

gereserveerde geheugen met delete weer vrij moet<br />

geven zal niemand verbazen. Dat je voor de class<br />

Array zelf een copy constructor en operator=<br />

moet definiëren ligt misschien minder voor de hand.<br />

Ik zal eerst bespreken wat het probleem is bij de<br />

door de compiler gedefinieerde default copy constructor en operator=. Daarna zal ik bespreken hoe<br />

we zelf een copy constructor en een operator kunnen declareren en implementeren. Een Array a met 4<br />

elementen gevuld met kwadraten is hierboven schematisch weergegeven.<br />

<strong>De</strong> gewenste situatie na het kopiëren van de Array<br />

a naar de Array b is hiernaast weergegeven. Om<br />

deze situatie te bereiken moeten je zelf de copy constructor<br />

van de class Array declareren:<br />

Array::Array(const Array&);<br />

Ik zal nu eerst de problematiek van de assignment<br />

operator bespreken omdat die vergelijkbaar is met de<br />

problematiek van de copy constructor.<br />

<strong>De</strong> door de compiler gegenereerde copy constructor<br />

zal een memberwise copy uitvoeren. <strong>De</strong> datamembers<br />

size en data worden dus gekopieerd. Als je<br />

de Array a naar de Array b kopieert door middel<br />

van het statement Array b(a); 88 dan ontstaat de<br />

hiernaast weergegeven situatie.<br />

Dit is niet goed omdat als je nu de kopie wijzigt (bijvoorbeeld<br />

b[2]=8;) dan zal ook het origineel<br />

(a[2]) gewijzigd zijn en dat is natuurlijk niet de<br />

bedoeling.<br />

88 Dit statement kan ook als volgt geschreven worden Array b = a; Ook in dit geval wordt de<br />

copy-constructor van de class Array aangeroepen en dus niet de operator= memberfunctie. Het<br />

gebruik van het = teken bij een initialisatie is verwarrend omdat het lijkt alsof er een assigment wordt<br />

gedaan terwijl in werkelijkheid de copy-constructor wordt aangeroepen. Om deze reden raad ik je aan<br />

om bij initialisatie altijd de notatie Array w(v); te gebruiken. Ook bij de ingebouwde types, bijvoorbeeld<br />

int i(0);<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


5.8 Overloading operator=. (Zie eventueel TICPP Chapter12.html#Heading366.)<br />

Voor de door de compiler gegenereerde assignment<br />

operator geldt ongeveer hetzelfde verhaal als voor de<br />

door de compiler gegenereerde copy constructor. Na<br />

het statement b=a; zal de situatie zoals hiernaast<br />

weergegeven ontstaan. Je ziet dat de door de compiler<br />

gegenereerde assignment operator niet alleen onjuist<br />

werkt maar bovendien een memory leak veroorzaakt.<br />

<strong>De</strong> copy constructor kan eenvoudig als volgt gedefinieerd<br />

worden:<br />

Array::Array(const Array& r): size(r.size), data(new int[r.size]) {<br />

for (int i(0);i


84<br />

Voor de liefhebbers:<br />

Vraag:<br />

Is de onderstaande implementatie van operator= correct? Verklaar je antwoord!<br />

Array& Array::operator=(Array r) {<br />

std::swap(data, r.data);<br />

std::swap(size, r.size);<br />

return *this;<br />

}<br />

Antwoord:<br />

Ja! <strong>De</strong> copy constructor wordt nu impliciet aangeroepen bij het doorgeven van de parameter r. (call by<br />

value).<br />

5.9 Wanneer moet je zelf een destructor, copy constructor en operator= definiëren.<br />

Een class moet een zelf gedefinieerde copy constructor, operator= en destructor bevatten als:<br />

• die class een pointer bevat en<br />

• als bij het kopiëren van een object van de class niet de pointer, maar de data waar de pointer naar<br />

wijst moet worden gekopieerd en<br />

• als bij een toekenning aan een object van de class niet de pointer, maar de data waar de pointer<br />

naar wijst moet worden toegekend en<br />

• als bij het "destructen" van een object van de class niet de pointer, maar de data waar de pointer<br />

naar wijst moet worden "destructed".<br />

Dit betekent dat de class Breuk geen zelf gedefinieerde assignment operator, geen zelf gedefinieerde<br />

copy constructor en ook geen zelf gedefinieerde destructor nodig heeft. <strong>De</strong> class Array heeft wel een<br />

zelf gedefinieerde assignment operator, een zelf gedefinieerde copy constructor en ook een zelf gedefinieerde<br />

destructor nodig.<br />

In de vorige paragraaf heb ik het zelf gedefinieerde type Array besproken. Dit type heeft een aantal<br />

voordelen ten opzichte van het ingebouwde array type. <strong>De</strong> belangrijkste zijn dat het aantal elementen pas<br />

tijdens het uitvoeren van het programma bepaald wordt en dat bij het gebruik van de operator[] de<br />

index wordt gecontroleerd. Het zelf gedefinieerde type Array heeft ten opzichte van het ingebouwde<br />

array type als nadeel dat de elementen alleen maar van het type int kunnen zijn. Als je een Array met<br />

elementen van het type double nodig hebt, dan kun je natuurlijk gaan kopiëren, plakken, zoeken en<br />

vervangen maar daar zitten weer de bekende nadelen aan (elke keer als je code kopieert wordt de onderhoudbaarheid<br />

van die code slechter). Als je verschillende versies van Array “met de hand” genereert<br />

moet je bovendien elke versie een andere naam geven omdat een class naam uniek moet zijn. In plaats<br />

daarvan kun je ook het template mechanisme gebruiken om een Array met elementen van het type T te<br />

definiëren, waarbij het type T pas bij het gebruik van de template class Array wordt bepaald. Bij het<br />

gebruik van de template class Array kan de compiler niet (snel) zelf bepalen wat het type T moet zijn.<br />

Vandaar dat je dit bij het gebruik van de template class Array zelf moet specificeren. Bijvoorbeeld:<br />

Array vb(300); // een Array met 300 breuken.<br />

5.10 Voorbeeld template class Array.<br />

#include <br />

#include <br />

#include <br />

using namespace std;<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


template class Array {<br />

public:<br />

explicit Array(int s);<br />

Array(const Array& r);<br />

Array& operator=(const Array& r);<br />

~Array();<br />

T& operator[](int index);<br />

const T& operator[](int index) const;<br />

int length() const;<br />

bool operator==(const Array& r) const;<br />

bool operator!=(const Array& r) const;<br />

private:<br />

int size;<br />

T* data;<br />

};<br />

template Array::Array(int s):<br />

size(s), data(new T[s]) {<br />

}<br />

// ... enz. ...<br />

int main() {<br />

couti;<br />

Array a(i);<br />

for (int j(0); j


86<br />

er weer een nieuw type met bijbehorende machinecode voor alle memberfuncties gegenereerd 89 .<br />

Bij de template class Array wordt maar één “versie” aangemaakt als de variabelen<br />

Array a(3) en Array b(4) gedefinieerd worden. <strong>De</strong> expressies a!=b en<br />

a=b enz. zijn dan wel toegestaan.<br />

We kunnen dus concluderen dat de template class FixedArray niet zo’n goed idee was.<br />

6 Losse flodders.<br />

In dit hoofdstuk worden nog enkele onderwerpen besproken die niets met elkaar te maken hebben en die<br />

ik niet in een van de voorafgaande hoofdstukken wilde opnemen.<br />

6.1 static class members.<br />

Je hebt geleerd dat elk object zijn eigen datamembers heeft terwijl de memberfuncties door alle objecten<br />

van een bepaalde class "gedeeld" worden. Stel nu dat je wilt tellen hoeveel objecten van een bepaalde<br />

class “levend” zijn. Dit zou kunnen door een globale “teller” te definiëren die in de constructor van de<br />

class met 1 wordt verhoogd en in de destructor weer met 1 wordt verlaagd. Het gebruik van een globale<br />

variabele maakt het programma echter slecht onderhoudbaar.<br />

Een static datamember is een onderdeel van de class en wordt door alle objecten van de class gedeeld.<br />

Z’n static datamember kan bijvoorbeeld gebruikt worden om het aantal “levende” objecten<br />

van een class te tellen. In UML worden static members onderstreept weergegeven.<br />

#include <br />

using namespace std;<br />

class Hond {<br />

public:<br />

Hond(const string& n);<br />

~Hond();<br />

void blaf() const;<br />

static int aantal();<br />

private:<br />

string naam;<br />

static int aantalHonden;<br />

};<br />

int Hond::aantalHonden=0;<br />

Hond::Hond(const string& n): naam(n) {<br />

++aantalHonden;<br />

}<br />

Hond::~Hond() {<br />

--aantalHonden;<br />

}<br />

int Hond::aantal() {<br />

return aantalHonden;<br />

}<br />

void Hond::blaf() const {<br />

cout


int main() {<br />

cout


88<br />

<strong>De</strong> qualifier const kan op verschillende manieren bij pointers gebruikt worden.<br />

6.2.1 const *<br />

int i(3);<br />

int j(4);<br />

const int* p(&i); 90<br />

Dit betekent: p wijst naar i en je kan i niet via p wijzigen 91 . Let op: je kan i zelf wel wijzigen!<br />

i=4; // Goed<br />

*p=5; // Error: Cannot modify a const object<br />

p=&j; // Goed<br />

6.2.2 * const<br />

int* const q(&i);<br />

Dit betekent: q wijst naar i en je kan q nergens anders meer naar laten wijzen 92 . Let op: je kan i wel via<br />

q (of rechtstreeks) wijzigen.<br />

i=4; // Goed<br />

*q=5; // Goed<br />

q=&j; // Error: Cannot modify a const object<br />

6.2.3 const * const<br />

const int* const r(&i);<br />

Dit betekent: r wijst naar i en je kan i niet via r wijzigen en je kan r nergens anders meer naar laten<br />

wijzen. Let op: je kan i zelf wel wijzigen!<br />

i=4; // Goed<br />

*r=5; // Error: Cannot modify a const object<br />

r=&j; // Error: Cannot modify a const object<br />

6.3 Constanten in een class.<br />

Met behulp van het keyword const kun je globale constanten definiëren. Globale constanten komen de<br />

onderhoudbaarheid niet ten goede. Als een datamember const wordt gedefinieerd dan krijgt elk object<br />

zijn eigen constante. Als je alle objecten van een class dezelfde constante wilt laten delen dan kun je<br />

deze constante static definiëren. Als alternatief kun je ook een enum gebruiken.<br />

90 <strong>De</strong> notatie int const* p(&i); heeft dezelfde betekenis maar wordt in de praktijk zelden ge-<br />

bruikt.<br />

91 Dit is zinvol als je een pointer als parameter aan een functie wilt meegeven en als je niet wilt dat de<br />

variabele waar deze pointer naar wijst in de functie gewijzigd wordt. Je gebruikt dan als parameter<br />

een const T* in plaats van een T*. Dat de functie de variabele waar de pointer naar wijst niet mag<br />

wijzigen zouden we natuurlijk ook gewoon kunnen afspreken (en bijvoorbeeld in het commentaar van<br />

de functie vermelden) maar door deze afspraak expliciet als een const declaratie vast te leggen kan<br />

deze afspraak door de compiler gecontroleerd worden. Dit vermindert de kans op het maken van<br />

fouten bij het implementeren of wijzigen van de functie.<br />

92 Dit is zinvol als je een pointer altijd naar dezelfde variabele wilt laten wijzen.<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


Voorbeeld met static constanten:<br />

class Color {<br />

public:<br />

Color();<br />

Color(int c);<br />

int getValue() const;<br />

void setValue(int c);<br />

// constanten:<br />

static const int BLACK = 0x00000000;<br />

static const int RED = 0x00FF0000;<br />

static const int YELLOW = 0x00FFFF00;<br />

static const int GREEN = 0x0000FF00;<br />

static const int LIGHTBLUE = 0x0000FFFF;<br />

static const int BLUE = 0x000000FF;<br />

static const int PURPER = 0x00FF00FF;<br />

static const int WHITE = 0x00FFFFFF;<br />

// ...<br />

private:<br />

int value;<br />

};<br />

<strong>De</strong>ze constanten kunnen als volgt gebruikt worden:<br />

Color c(Color::YELLOW);<br />

//...<br />

c.setValue(Color::BLUE);<br />

Voorbeeld met anonymous (naamloze) enum:<br />

class Color {<br />

public:<br />

Color();<br />

Color(int c);<br />

int getValue() const;<br />

void setValue(int c);<br />

// constanten:<br />

enum {<br />

BLACK = 0x00000000, RED = 0x00FF0000,<br />

YELLOW = 0x00FFFF00, GREEN = 0x0000FF00,<br />

LIGHTBLUE = 0x0000FFFF, BLUE = 0x000000FF,<br />

PURPER = 0x00FF00FF, WHITE = 0x00FFFFFF<br />

// ...<br />

};<br />

private:<br />

int value;<br />

};<br />

<strong>De</strong>ze constanten kunnen als volgt gebruikt worden:<br />

Color c(Color::YELLOW);<br />

//...<br />

c.setValue(Color::BLUE);<br />

6.4 inline memberfuncties. (Zie eventueel TICPP Chapter09.html#Heading280.)<br />

Veel programmeurs denken dat het gebruik van eenvoudige memberfuncties zoals teller en noemer<br />

te veel vertraging opleveren in hun applicatie. Dit is meestal ten onrechte! Maar voor die gevallen waar<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

89


90<br />

het echt nodig is biedt C++ de mogelijkheid om functies (ook memberfuncties) als "inline" te definiëren.<br />

Dit betekent dat de compiler een aanroep naar deze functie niet vertaalt naar een "jump to subroutine"<br />

machinecode maar probeert om de machinecode waaruit de functie bestaat rechtstreeks op de plaats van<br />

aanroep in te vullen. (Net zoals macro's in assembler.) Dit heeft wel tot gevolg dat de code omvangrijker<br />

wordt. Memberfuncties mogen ook in de class declaratie gedefinieerd worden. Ze zijn dan "vanzelf"<br />

inline. Als de memberfunctie in de class declaratie alleen maar gedeclareerd is dan kan de definitie van<br />

die functie voorafgegaan worden door het keyword inline om de functie inline te maken.<br />

Eerste manier om de memberfuncties teller en noemer inline te maken:<br />

class Breuk {<br />

// ...<br />

int teller() const {<br />

return boven;<br />

}<br />

int noemer() const {<br />

return onder;<br />

}<br />

// ...<br />

};<br />

Tweede manier om de memberfuncties teller en noemer inline te maken:<br />

class Breuk {<br />

// ...<br />

int teller() const;<br />

int noemer() const;<br />

// ...<br />

};<br />

inline int Breuk::teller() const {<br />

return boven;<br />

}<br />

inline int Breuk::noemer() const {<br />

return onder;<br />

}<br />

6.5 Namespaces.<br />

Bij grote programma’s kunnen verschillende classes “per ongeluk” dezelfde naam krijgen. In C++ kun je<br />

classes (en functies etc.) groeperen in zogenaamde namespaces.<br />

namespace Bd {<br />

void f(int);<br />

double sin(double x);<br />

}<br />

// andere file zelfde namespace:<br />

namespace Bd {<br />

class string {<br />

//...<br />

};<br />

}<br />

// andere namespace:<br />

namespace Vi {<br />

class string {<br />

//...<br />

Object Georiënteerd Programmeren in C++ <strong>Harry</strong> <strong>Broeders</strong>


}<br />

};<br />

Je ziet dat in de namespace Bd een functie sin is opgenomen die ook in de standaard library is opgenomen.<br />

<strong>De</strong> in de namespace Bd gedefinieerde class string is ook in de namespace Vi en ook in de<br />

standaard library gedefinieerd. Je hebt op blz. 8 gezien dat alle functies en classes uit de standaard<br />

library in de namespace std zijn opgenomen. Je kunt met de scope-resolution operator :: aangeven uit<br />

welke namespace je een class of functie wilt gebruiken:<br />

Bd::string s1("<strong>Harry</strong>");<br />

Vi::string s2("John");<br />

std::string s3("Standard");<br />

In het bovenstaande codefragment worden 3 objecten gedefinieerd van 3 verschillende classes. Het<br />

object s1 is van de class string die gedefinieerd is in de namespace Bd, het object s2 is van de class<br />

string die gedefinieerd is in de namespace Vi en het object s3 is van de class string die gedefinieerd<br />

is in de namespace std (de standaard library). Je ziet dat je met behulp van namespaces classes<br />

die toevallig dezelfde naam hebben toch in 1 programma kunt combineren. Namespaces maken dus het<br />

hergebruik van code eenvoudiger.<br />

Als je in een stuk code steeds de string uit de namespace Bd wilt gebruiken dan kun je dat opgeven<br />

met behulp van een using declaration.<br />

using Bd::string;<br />

string s4("Hallo");<br />

string s5("Dag");<br />

<strong>De</strong> objecten s4 en s5 zijn nu beide van de class string die gedefinieerd is in de namespace Bd. <strong>De</strong><br />

using declaratie blijft net zolang geldig als een gewone variabele declaratie. Tot de bijbehorende accolade<br />

sluiten dus.<br />

Als je in een stuk code steeds classes en functies uit de namespace Bd wilt gebruiken dan kun je dat<br />

opgeven met behulp van een using directive.<br />

using namespace Bd;<br />

string s6("Hallo");<br />

double d(sin(0,785398163397448));<br />

Het object s6 is nu van de class string die gedefinieerd is in de namespace Bd. <strong>De</strong> functie sin die<br />

wordt aangeroepen is nu de in de namespace Bd gedefinieerde functie. <strong>De</strong> using directive blijft net<br />

zolang geldig als een gewone variabele declaratie. Tot de bijbehorende accolade sluiten dus.<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

91


92<br />

6.6 Exceptions.<br />

Vaak zal in een functie of memberfunctie gecontroleerd worden op “uitzonderlijke” situaties (fouten).<br />

<strong>De</strong> volgende functie berekent de impedantie van een condensator van c Farad bij een frequentie van f<br />

Hz.<br />

complex impedanceC(double c, double f) {<br />

return complex(0, -1/(2*M_PI*f*c));<br />

}<br />

<strong>De</strong>ze functie kan als volgt aangeroepen worden om de impedantie van een condensator van 1 μF bij<br />

1 kHz op het scherm af te drukken:<br />

cout


Het programma stopt nog steeds abrupt. <strong>De</strong> foutmelding is nu wel veel duidelijker. Als het programma<br />

echter gecompileerd wordt zonder zogenaamde debug informatie dan worden alle assert functies<br />

verwijderd en verschijnt weer de onduidelijke foutmelding (uitzondering 10H).<br />

<strong>De</strong> standaard functie assert is bedoeld om tijdens het ontwikkelen van programma’s te controleren of<br />

iedereen zich aan bepaalde afspraken houdt. Als bijvoorbeeld is afgesproken dat de functie<br />

impedanceC alleen mag worden aangeroepen met parameters die niet gelijk aan 0 zijn dan is dat prima<br />

met assert te controleren. Elk programmadeel waarin impedanceC wordt aangeroepen moet nu<br />

voor de aanroep zelf controleren of de parameters geldige waarden hebben. Bij het testen van het programma<br />

(gecompileerd met debug informatie) zorgt de assert ervoor dat het snel duidelijk wordt als<br />

de afspraak geschonden wordt. Als na het testen duidelijk is dat iedereen zich aan de afspraak houdt dan<br />

is het niet meer nodig om de assert uit te voeren. Het programma wordt gecompileerd zonder debug<br />

informatie en alle assert aanroepen worden verwijderd.<br />

Het gebruik van assert heeft de volgende nadelen:<br />

• Het programma wordt nog steeds abrupt afgebroken en krijgt niet de kans om tijdelijk opgeslagen<br />

data op te slaan.<br />

• Op elke plek waar deze functie aangeroepen wordt moeten, voor aanroep, eerst de parameters<br />

gecontroleerd worden. Als de functie op veel plaatsen aangeroepen wordt dan is het logischer om<br />

de controle in de functie zelf uit te voeren. Dit maakt het programma ook beter onderhoudbaar (als<br />

de controle aangepast moet worden dan hoeft de code maar op 1 plaats gewijzigd te worden) en<br />

betrouwbaarder (je kunt de functie niet meer zonder controle aanroepen, de controle zit nu immers<br />

in de functie zelf).<br />

We kunnen concluderen dat assert in dit geval niet geschikt is.<br />

6.6.2 Het gebruik van een bool returnwaarde.<br />

In C (en ook in C++) werd dit traditioneel opgelost door de functie een returnwaarde te geven die<br />

aangeeft of het uitvoeren van de functie gelukt is:<br />

bool impedanceC(complex& res, double c, double f) {<br />

if (c!=0.0 && f!=0.0) {<br />

res=complex(0, -1/(2*M_PI*f*c));<br />

return true;<br />

}<br />

else<br />

return false;<br />

}<br />

<strong>De</strong>ze functie kan nu als volgt aangeroepen worden:<br />

complex z;<br />

if (impedanceC(z, 1e-6, 1e3)) {<br />

cout


94<br />

Het programma wordt nu niet meer abrupt afgebroken. Het gebruik van een return waarde om aan te<br />

geven of een functie gelukt is heeft echter de volgende nadelen:<br />

• Bij elke aanroep moet de returnwaarde getest worden.<br />

• Op de plaats waar de fout ontdekt wordt kan hij meestal niet opgelost worden.<br />

• <strong>De</strong> “echte” returnwaarde van de functie moet nu via een call by reference parameter worden<br />

teruggegeven. Dit betekent dat je om de functie aan te roepen altijd een variabele aan moet maken<br />

(om het resultaat in op te slaan) ook als je het resultaat alleen maar wilt doorgeven aan een andere<br />

functie of operator.<br />

<strong>De</strong> C library stdio werkt bijvoorbeeld met returnwaarden van functies die aangeven of de functie gelukt<br />

is. Zo geeft de functie printf bijvoorbeeld een int returnwaarde. Als de functie gelukt is geeft<br />

printf het aantal geschreven bytes terug maar als er een error is opgetreden geeft printf de waarde<br />

EOF terug. Een goed geschreven programma moet dus bij elke aanroep naar printf de returnwaarde<br />

testen! 93<br />

6.6.3 Het gebruik van standaard exceptions.<br />

C++ heeft exceptions ingevoerd voor het afhandelen van “uitzonderlijke” fouten. Een exception is een<br />

object dat in de functie waar de fout ontstaat “gegooid” kan worden en dat door de aanroepende functie<br />

(of door zijn aanroepende functie enz...) “opgevangen” kan worden. In de standaard library zijn ook een<br />

aantal standaard exceptions opgenomen. <strong>De</strong> class van de exception die bedoeld is om te gooien als een<br />

parameter van een functie een ongeldige waarde heeft is de naam domain_error 94 .<br />

#include <br />

using namespace std;<br />

complex impedanceC(double c, double f) {<br />

if (c==0.0)<br />

throw domain_error("Capaciteit == 0");<br />

if (f==0.0)<br />

throw domain_error("Frequentie == 0");<br />

return complex(0, -1/(2*M_PI*f*c));<br />

}<br />

Je kunt een exception object gooien door het C++ keyword throw te gebruiken. Bij het aanroepen van<br />

de constructor van de class domain_error die gedefinieerd is in de include file kun<br />

je een string meegeven die de oorzaak van de fout aangeeft. Als de throw wordt uitgevoerd dan wordt<br />

de functie meteen afgebroken. Lokale variabelen worden wel netjes opgeruimd (de destructors van deze<br />

lokale objecten wordt netjes aangeroepen). Ook de functie waarin de functie impedanceC is aangeroepen<br />

wordt meteen afgebroken. Dit proces van afbreken wordt gestopt zodra de exception wordt opgevangen.<br />

Als de exception nergens wordt opgevangen dan wordt het programma gestopt.<br />

try {<br />

cout


Exceptions kunnen worden opgevangen als ze optreden in een zogenaamd try blok. Dit blok begint met<br />

het keyword try en wordt afgesloten met het keyword catch. Na het catch keyword moet je tussen<br />

haakjes aangeven welke exceptions je wilt opvangen gevolgd door een codeblok dat uitgevoerd wordt<br />

als de gespecificeerde exception wordt opgevangen. Na het eerste catch blok kunnen er nog een willekeurig<br />

aantal catch blokken volgen om andere exceptions ook te kunnen vangen. Als je alle mogelijke<br />

exceptions wilt opvangen dan kun je dat doen met: catch(...) { /*code*/ }.<br />

Een exception kun je het beste als reference opvangen. Dit heeft 2 voordelen:<br />

• Het voorkomt een extra kopie.<br />

• We kunnen op deze manier ook van domain_error afgeleide classes opvangen zonder dat het<br />

slicing probleem (zie blz. 67) optreedt.<br />

<strong>De</strong> class domain_error heeft een memberfunctie what() die de bij constructie van het object<br />

meegegeven string weer teruggeeft. <strong>De</strong> uitvoer van het bovenstaande programma is:<br />

(0,-159.155)<br />

Frequentie == 0<br />

The END.<br />

Merk op dat het laatste statement in het try blok niet meer uitgevoerd wordt omdat bij het uitvoeren van<br />

het tweede statement een exception optrad. Het gebruik van exceptions in plaats van een bool returnwaarde<br />

heeft de volgende voordelen.<br />

• Het programma wordt niet meer abrupt afgebroken. Door de exception op te vangen op een plaats<br />

waar je er wat mee kunt heb je de mogelijkheid om het programma na een exception gewoon door<br />

te laten gaan of op z’n minst netjes af te sluiten.<br />

• Je hoeft niet bij elke aanroep te testen. Je kunt meerder aanroepen opnemen in hetzelfde try blok.<br />

Als in het bovenstaande programma een exception zou optreden in het eerste statement dan wordt<br />

het tweede (en derde) statement niet meer uitgevoerd.<br />

• <strong>De</strong> returnwaarde van de functie kan nu weer gewoon gebruikt worden om de berekende impedantie<br />

terug te geven.<br />

Vraag:<br />

Pas de impedance calculator (zie blz. 70) aan zodat dit programma niet meer abrupt door een divide by<br />

zero error afgebroken kan worden.<br />

Antwoord:<br />

<strong>De</strong> classes C en P moeten worden aangepast:<br />

class C: public Component { // C=Condensator<br />

public:<br />

C(double c): value(c) {<br />

}<br />

virtual complex Z(double f) const {<br />

if (value==0.0)<br />

throw domain_error("Capacity == 0");<br />

if (f==0.0)<br />

throw domain_error("Frequency == 0");<br />

return complex(0, -1/(2*M_PI*f*value));<br />

}<br />

virtual void print(ostream& o) const {<br />

o


96<br />

public:<br />

P(const Component& c1, const Component& c2): comp1(c1), comp2(c2) {<br />

}<br />

virtual complex Z(double f) const {<br />

if (comp1.Z(f)+comp2.Z(f)==0)<br />

throw domain_error("Illegal parallel circuit");<br />

return (comp1.Z(f)*comp2.Z(f)) / (comp1.Z(f)+comp2.Z(f));<br />

}<br />

virtual void print(ostream& o) const {<br />

o


cout


98<br />

cout


• Exceptions in de std. Een overzicht van alle exceptions die in de standaard library gedefinieerd<br />

zijn en een overzicht van alle exceptions die bij het gebruik van standaard C++ kunnen optreden.<br />

Voor al deze details verwijs ik je naar hoofdstuk 1 van “Thinking in C++, Volume 2”.<br />

6.7 Casting en runtime type information.<br />

6.7.1 Casting.<br />

In C kun je met een eenvoudige vorm van “casting” typeconversies doen. Stel dat we het adres van een C<br />

string in een integer willen opslaan 95 dan kun je dit als volgt proberen:<br />

int i;<br />

i="Hallo";<br />

Als je dit probeert te compileren dan krijg je de volgende foutmelding:<br />

[C++ Error] Cannot convert 'char *' to 'int'<br />

Het is namelijk helemaal niet zeker dat een char* in een int variabele past. Stel dat je zeker weet dat<br />

het past (omdat je het programma alleen maar onder Win32 wilt gebruiken) dan kun je de compiler<br />

“dwingen” met behulp van een zogenaamde cast:<br />

i=(int)"Hallo";<br />

In C++ mag je deze cast ook als volgt coderen:<br />

i=int("Hallo");<br />

Het vervelende van beide vormen van casting is dat een cast erg moeilijk te vinden is. Omdat een cast in<br />

bijna alle gevallen code oplevert die niet portable is, is het echter wel belangrijk om alle casts in een<br />

programma op te kunnen sporen. Een cast wordt door C programmeurs ook vaak gebruikt terwijl dat<br />

helemaal niet nodig is (zoals in het bovenstaande geval).<br />

In de C++ standaard is om deze reden een nieuwe syntax voor casting gedefinieerd die eenvoudiger te<br />

vinden is:<br />

i=reinterpret_cast("Hallo");<br />

In dit geval moeten we een reinterpret_cast gebruiken omdat de cast niet portable is.<br />

Stel dat je een C++ programma schrijft dat moet draaien op een 68HC11 microcontroller. Als je de<br />

output poort B van deze controller wilt aansturen dan kan dat via adres $1004. Dit kun je in C++ als<br />

volgt doen:<br />

char* ptrPortB(reinterpret_cast(0x1004));<br />

*ptrPortB=189; // schrijf 189 (decimaal) naar adres 0x1004 (hex)<br />

Als we een cast willen doen die wel portable is dan kan dat met static_cast.<br />

95 Er is geen enkele reden te bedenken waarom je dit zou willen. Het adres van een C string moet je<br />

natuurlijk in een char* opslaan!<br />

<strong>De</strong> <strong>Haagse</strong> <strong>Hogeschool</strong> Elektrotechniek en Technische Informatica<br />

99


100<br />

Vraag:<br />

Wat is de uitvoer van het volgende programma:<br />

#include <br />

int main() {<br />

int i1(1);<br />

int i2(2);<br />

double d(i1/i2);<br />

std::cout


6.7.2 Casting en overerving.<br />

Als we een pointer naar een Base class hebben dan mogen we een pointer naar een <strong>De</strong>rived (van<br />

Base afgeleide) class toekennen aan deze Base class pointer. Voorbeeld:<br />

class Hond { /* ... */ };<br />

class SintBernard: public Hond { /* ... */ };<br />

Hond* hp(new SintBernard); // OK: een SintBernard is een Hond<br />

SintBernard* sp(new Hond); // ERROR: een Hond is geen SintBernard<br />

Het omzetten van een Hond* naar een SintBernard* kan soms toch nodig zijn. We noemen dit een<br />

down-cast omdat we afdalen in de class hiërarchie. Voorbeeld:<br />

class Hond {<br />

public:<br />

virtual void blaf() const {<br />

std::cout


102<br />

Hond* borisPtr(new SintBernard);<br />

geefHulp(borisPtr);<br />

delete borisPtr;<br />

Uitvoer:<br />

Woef!<br />

Geeft drank.<br />

10 liter.<br />

Als we de functie geefHulp echter aanroepen met een Hond* als argument geeft het programma<br />

onvoorspelbare resultaten (of loopt het vast):<br />

Hond* borisPtr(new Hond);<br />

geefHulp(borisPtr);<br />

delete borisPtr;<br />

Uitvoer:<br />

Blaf.<br />

Geeft drank.<br />

6684840 liter.<br />

Een static_cast is dus alleen maar geschikt als down-cast als je zeker weet dat de cast geldig is. In<br />

het bovenstaande programma zou je de mogelijkheid willen hebben om te kijken of een down-cast<br />

mogelijk is. Dit kan met een zogenaamde dynamic_cast.<br />

void geefHulp(Hond* hp) {<br />

hp->blaf();<br />

SintBernard* psb(dynamic_cast(hp));<br />

if (psb != 0) {<br />

std::cout


Een versie van geefHulp die werkt met een Hond& in plaats van met een Hond* kun je dus als volgt<br />

implementeren:<br />

#include <br />

void geefHulp(Hond& hr) {<br />

hr.blaf();<br />

try {<br />

SintBernard& sbr(dynamic_cast(hr));<br />

std::cout


104<br />

6.7.4 Maak geen misbruik van RTTI en dynamic_cast.<br />

Het verkeerd gebruik van dynamic_cast en RTTI kan leiden tot code die niet uitbreidbaar en aanpasbaar<br />

is! Je zou bijvoorbeeld op het idee kunnen komen om een functie blaf() als volgt te implementeren:<br />

class Hond { public: virtual ~Hond(); /* ... */ };<br />

class SintBernard: public Hond { /* ... */ };<br />

class Tekkel: public Hond { /* ... */ };<br />

// <strong>De</strong>ze code is NIET uitbreidbaar!<br />

// ******* DON'T DO THIS AT HOME *******<br />

// blaf moet als virtual memberfunctie geïmplementeerd worden!<br />

void blaf(const Hond* hp) {<br />

if (dynamic_cast(hp)!=0)<br />

std::cout

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!