© Harry Broeders.
Deze pagina is bestemd voor studenten van de Haagse Hogeschool - TH Rijswijk/Academie voor Engineering.
Laten we eerst
een eenvoudig type
Hond
declareren.
class Hond {
public:
Hond(const string& n);
~Hond();
void setNaam(const string& n);
void blaf() const;
private:
string naam;
};
De memberfuncties worden als volgt gedefinieerd:
Hond::Hond(const string& n): naam(n) {
cout<<"Hoera, "<<naam<<" is geboren!"<<endl;
}
Hond::~Hond() {
cout<<"Helaas, "<<naam<<" is gestorven."<<endl;
cin.get();
}
void Hond::setNaam(const string& n) {
naam=n;
}
void Hond::blaf() const {
cout<<"Blaf blaf"<<endl;
}
Je kunt nu als volgt een object van de class Hond
aanmaken:
Hond h1("Fikkie");
Op het moment dat deze hond wordt aangemaakt verschijnt de volgende uitvoer:
Hoera, Fikkie is geboren!
Deze hond kun je als volgt laten blaffen:
h1.blaf();
Uitvoer:
Blaf blaf
Je kunt de naam van deze hond als volgt veranderen:
h1.setNaam("Kees");
Het is ook mogelijk om een "constante" hond aan te maken.
const Hond h2("Leika");
Uitvoer:
Hoera, Leika is geboren!
Deze hond kun je ook laten blaffen:
h2.blaf();
Uitvoer:
Blaf blaf
Als je echter de naam van deze hond wil veranderen dan geeft de compiler de volgende foutmelding:
h2.setNaam("Lex");
Error: Non-const function Hond::setNaam(const string &) called for const object
Borland Builder geeft een warning maar volgens de standaard is dit echt een error!
Als de honden worden verwijderd verschijnt de volgende uitvoer:
Helaas, Leika is gestorven.
Helaas, Kees is gestorven.
Vragen:
naam
private gedefinieerd.
void Hond::setNaam(string n) {
naam=n;
}
Welk effect heeft dit bij het uitvoeren van het programma?
void Hond::setNaam(const string& n): naam(n) {
}
h1
wel de message
setNaam
kunt sturen maar h2
niet?
const
achter de declaratie van de
memberfunctie blaf
?
We gaan het programma uitbreiden. Er komt een nieuwe soort hond bij: de tekkel.
Een Tekkel
is een Hond
dus je kunt overerving
toepassen:
class Tekkel: public Hond {
public:
Tekkel(const string& n);
~Tekkel();
void blaf() const;
};
De constructor kan niet als volgt gedefinieerd worden:
Tekkel::Tekkel(const string& n): naam(n) {
Error: 'Hond::naam' is not an unambiguous base class of 'Tekkel'cout<<"Er is een Tekkel geboren!"<<endl;
}
De foutmelding is er wazig maar de oorzaak van de fout is dat de membervariabele
naam
private is. Dus alleen de memberfuncties van
Hond
kunnen deze variabele veranderen. Je kunt echter vanuit
de initialisatielijst van een afgeleide class de constructor van de base
class aanroepen.
Tekkel::Tekkel(const string& n): Hond(n) {
cout<<"Er is een Tekkel geboren!"<<endl;
}
De destructor en de functie blaf
kunnen als volgt gedefinieerd
worden:
Tekkel::~Tekkel() {
cout<<"Er is een Tekkel gestorven."<<endl;
}
void Tekkel::blaf() const {
cout<<"Kef kef"<<endl;
}
Je kunt nu een gewone hond aanmaken en laten blaffen:
Hond h1("Fikkie");
h1.blaf();
Uitvoer:
Hoera, Fikkie is geboren!
Blaf blaf
Je kunt ook een tekkel aanmaken en laten blaffen:
Tekkel h2("Biefie");
h2.blaf();
Uitvoer:
Hoera, Biefie is geboren!
Er is een Tekkel geboren!
Kef kef
Je ziet duidelijk dat de constructor van Hond
vanuit de constructor
van Tekkel
wordt aangeroepen.
Omdat een Tekkel
een Hond
is kun je een
Hond
pointer naar een Tekkel
laten wijzen. We noemen
z'n pointer polymorph omdat hij naar objecten van verschillende typen kan
wijzen.
Hond* hp(new Tekkel("Harry"));
Als je deze hond laat blaffen krijg je echter niet het gewenste resultaat:
hp->blaf(); // dit is een verkorte notatie voor (*hp).blaf();
Uitvoer:
Hoera, Harry is geboren!
Er is een Tekkel geboren!
Blaf blaf
Deze tekkel blaft dus niet zoals een tekkel behoort te blaffen!
Ook als je de tekkel verwijdert krijg je niet het gewenste resultaat:
delete hp;
Uitvoer:
Helaas, Harry is gestorven.
Je ziet dat nu alleen de destructor van Hond
wordt aangeroepen
terwijl je zou verwachten dat de destructor van Tekkel
wordt
aangeroepen.
Het tot slot verwijderen van h2 en h1 geeft de volgende uitvoer:
Er is een Tekkel gestorven.
Helaas, Biefie is gestorven.
Helaas, Fikkie is gestorven.
Je ziet dat vanuit de destructor van de derived class Tekkel
automatisch de destructor van de base class Hond
wordt aangeroepen.
In het voorgaande voorbeeld heb je gezien dat als je de
gewone memberfunctie blaf
aanroept via een
Hond*
dat dan altijd de functie Hond::blaf
wordt
aangeroepen. Ook als de pointer naar een Tekkel
wijst! De compiler
vertaald de aanroep gewoon naar een "jump to subroutine" in machine code.
En de aanroep komt dus altijd bij dezelfde code uit. Dit wordt compile-time
binding of ook wel static binding genoemd. Dat is natuurlijk niet de bedoeling:
een tekkel moet altijd blaffen als een tekkel.
De oplossing ligt in het gebruik van een virtuele functie. Als een functie virtueel is dan wordt bij het aanroepen van deze functie tijdens het uitvoeren van het programma een "message" naar de receiver gestuurd. Dit wordt run-time binding of ook wel dynamic binding genoemd.
De class Hond
moet nu dus als volgt gedeclareerd worden:
class Hond {
public:
Hond(const string& n);
virtual ~Hond();
void setNaam(const string& n);
virtual void blaf() const;
private:
string naam;
};
De definitie van de memberfuncties veranderd niet. (Het keyword virtual kan alleen in class declaraties worden gebruikt.)
De class Tekkel
kan dan als volgt gedeclareerd worden:
class Tekkel: public Hond {
public:
Tekkel(const string& n);
virtual ~Tekkel();
virtual void blaf() const;
};
Het gebruik van het keyword virtual
is hier niet echt nodig.
Als een memberfunctie eenmaal virtual
is dan blijft hij
virtual
. Het opnieuw definiëren van een virtual memberfunctie
wordt overridding genoemd.
Omdat een Tekkel
(nog steeds) een Hond
is kun je
een Hond
pointer weer naar een Tekkel
laten wijzen.
Hond* hp(new Tekkel("Harry"));
Als je deze hond laat blaffen krijg je nu wel het gewenste resultaat:
hp->blaf(); // dit is een verkorte notatie voor (*hp).blaf();
Uitvoer:
Hoera, Harry is geboren!
Er is een Tekkel geboren!
Kef kef
Deze tekkel blaft dus zoals een tekkel behoort te blaffen!
Ook als je de tekkel verwijdert krijg je nu het gewenste resultaat:
delete hp;
Uitvoer:
Er is een Tekkel gestorven.
Helaas, Harry is gestorven.
Je ziet dat nu keurig de destructor van Tekkel
wordt aangeroepen
(die automatisch de destructor van Hond
aanroept).
Conclusies:
virtual
zijn.
virtual
zijn gedeclareerd
kun je in een afgeleide class opnieuw definiëren (overridden). Een gewone
memberfunctie kun je ook wel opnieuw definiëren (dat heet dan overloaden)
maar dat moet je niet doen omdat polymorphism alleen maar
werkt bij virtual
memberfuncties.
Als een uitbreiding op het bovenstaande voorbeeld kun je het type
SintBernard
definiëren. Een SintBernard
is een Hond
en een SintBernard
heeft een WhiskeyVat
.
class WhiskeyVat {
public:
WhiskeyVat(int b);
~WhiskeyVat();
void geefBorrel();
private:
int aantalBorrels;
};
![]()
class SintBernard: public Hond {
public:
SintBernard(const string& n, int b);
virtual ~SintBernard();
virtual void blaf() const;
void help();
private:
WhiskeyVat vat;
};
WhiskeyVat::WhiskeyVat(int b): aantalBorrels(b) {
cout<<"Vat met "<<aantalBorrels<<" borrels aangemaakt."<<endl;
}
WhiskeyVat::~WhiskeyVat() {
cout<<"Vat met "<<aantalBorrels<<" borrels opgeruimd."<<endl;
}
void WhiskeyVat::geefBorrel() {
if (aantalBorrels > 0) {
--aantalBorrels;
cout<<"Ik kom je helpen, drink deze borrel maar op!"<<endl;
}
}
SintBernard::SintBernard(const string& n, int b): Hond(n), vat(b) {
cout<<"Er is een SintBernard geboren!"<<endl;
}
SintBernard::~SintBernard() {
cout<<"Er is een SintBernard gestorven."<<endl;
}
void SintBernard::blaf() const {
cout<<"WOEF, WOEF"<<endl;
}
void SintBernard::help() {
vat.geefBorrel();
blaf();
}
Deze class kun je als volgt gebruiken:
SintBernard h1("Boris", 10);
h1.blaf();
h1.help();
Uitvoer:
Hoera, Boris is geboren!
Vat met 10 borrels aangemaakt.
Er is een SintBernard geboren!
WOEF, WOEF
Ik kom je helpen, drink deze borrel maar op!
WOEF, WOEF
Uitvoer als h1
verwijderd wordt:
Er is een SintBernard gestorven.
Vat met 9 borrels opgeruimd.
Helaas, Boris is gestorven.
Je kunt Boris als volgt proberen te klonen (kopiëren):
Hond h2(h1);
Als je deze kopie laat blaffen geeft dit een onverwacht resultaat:
h2.blaf();
Uitvoer:
Blaf blaf
Als je er even over nadenkt is dat ook niet zo vreemd. Een
SintBernard
past niet in de geheugenruimte van een
Hond
(hij heeft namelijk een WhiskeyVat
als extra
datamember). Een gewone variabele kan dus nooit polymorph zijn! (Dit probleem
wordt het slicing problem genoemd.)
In het vorige voorbeeld hebben we gezien dat een pointer wel polymorph kan zijn. Een reference kan ook polymorph zijn:
Hond& h2(h1);
h2
is nu geen kopie van h1
maar alleen maar een
andere naam voor h1
. Als je h2
laat blaffen geeft
dit het verwachte resultaat:
h2.blaf();
Uitvoer:
WOEF WOEF
In het vorige voorbeeld heb je gezien dat het gebruik van objecten van een base class tot problemen leidt (slicing problem). Deze problemen kun je voorkomen door de Base class abstract te maken. Dat doe je door 1 of meerdere functies wel te declareren maar niet te definiëren. Je moet dan wel aangeven dat de definitie niet gewoon vergeten is maar dat je die bewust hebt weggelaten door =0 achter de memberfunctie te plaatsen. Z'n memberfunctie wordt een abstracte of ook wel pure virtual memberfunctie genoemd. Als je een concrete afgeleide class wilt declareren dan moet je in die class alle abstracte functies uit de base class overridden.
Je kunt de basis class Hond
als volgt abstract maken:
class Hond {
public:
Hond(const string& n);
virtual ~Hond();
void setNaam(const string& n);
virtual void blaf() const =0;
private:
string naam;
};
De definitie van Hond::blaf
is nu ook niet
meer nodig.
Als je nu probeert om een object van de class Hond aan te maken krijg je tijdens het compileren fouten:
SintBernard h1("Boris", 10);
Hond h2(h1);
Error: Cannot create instance of abstract class 'Hond' Error: Class 'Hond' is abstract because of 'Hond::blaf() const = 0'
Je kunt nog steeds pointers en references van het type Hond definiëren:
Hond& h3(h1);
h3.blaf();
Uitvoer:
WOEF WOEF
Conclusies:
In dit voorbeeld laat ik je zien hoe je polymorphism kan gebruiken. Ik definieer dat een roedel honden bestaat uit een groep van maximaal 12, mogelijk verschillende honden.
class Roedel {
public:
Roedel();
void voegToe(Hond& h);
void blafAllemaal() const;
private:
int aantal;
Hond* honden[12];
};
Vragen:
Hond*
) op en niet gewoon
honden (Hond
)?
Hond*
) op en niet references
naar honden (Hond&
)?
Roedel::Roedel(): aantal(0) {
}
void Roedel::voegToe(Hond& h) {
if (aantal < 12)
honden[aantal++] = &h;
else
cout<<"Roedel is vol!"<<endl;
}
void Roedel::blafAllemaal() const {
for (int i(0); i<aantal; ++i)
honden[i]->blaf();
}
Vraag:
SintBernard h1("Boris", 10);
Tekkel h2("Fikkie");
Tekkel h3("Harry");
Roedel r;
r.voegToe(h1);
r.voegToe(h2);
r.voegToe(h3);
r.blafAllemaal();
Alle source codes kun je hier downloaden: