© Harry Broeders.
Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.
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");
Microsoft Visual C++ geeft de volgende foutmelding:
Error: 'Hond::setNaam' : cannot convert 'this' pointer from 'const Hond' to 'Hond &'
GNU gcc geeft de volgende foutmelding:
Error: no matching function for call to `Hond::setNaam(const char[4]) const'
Als de honden worden verwijderd verschijnt de volgende uitvoer:
Helaas, Leika is gestorven.
Helaas, Kees is gestorven.
Vragen:
naam
private gedefinieerd. string
gebruikt en geen character array
(zoals je in C gewend was). setNaam
ook als volgt definiëren?
void
Hond::setNaam(string n) {
naam = n;
}
Welk effect heeft dit bij het uitvoeren van het programma?
setNaam
ook als volgt definiëren?
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) {
Microsoft Visual C++ geeft de volgende foutmelding: 'Hond' : no appropriate default constructor available 'Tekkel' : illegal member initialization: 'naam' is not a base or member GNU gcc geeft de volgende foutmelding: `std::string Hond::naam' is private error: within this context `Tekkel' does not have any field named `naam' no matching function for call to `Hond::Hond()'cout << "Er is een Tekkel geboren!" << endl;
}
De foutmelding is erg 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 polymorf 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 polymorfisme alleen maar werkt bij
virtual
memberfuncties.Als je een virtuele memberfunctie override dan moeten de naam, de types van de parameters en het al dan niet const zijn, exact hetzelfde zijn. Als er een verschil is dan krijg je er "gewoon" een nieuwe memberfunctie bij. Als je de class Tekkel bijvoorbeeld als volgt definieert:
class Tekkel: public Hond {
public:
Tekkel(const string& n);
virtual ~Tekkel();
virtual void blaft() const;
};
Dan heeft een Tekkel
een memberfunctie blaf()
overgeërft van de basisklasse Hond
en een nieuwe in
Tekkel
gedefinieerde memberfunctie
blaft()
.
Als je een object h1
van deze klasse Tekkel
aanmaakt en dit object laat blaffen door de memberfunctie blaf()
aan te roepen:
Tekkel h1("Biefie");
h1.blaf();
Dan is de uitvoer als volgt:
Hoera, Biefie is geboren! Er is een Tekkel geboren! Blaf blaf
Het zou natuurlijk kunnen dat dit exact de bedoeling was van de programmeur
van Tekkel
. Maar het ligt meer voor de hand dat de programmeur de
memberfunctie blaf()
uit de basisklasse Hond
wilde
overridden en per ongeluk blaft
in plaats van
blaf
heeft ingetypt. Als je een functie wil overridden dan kun je
dit expliciet aangeven door het woord override
achter de functie te plaatsen.
class Tekkel: public Hond {
public:
Tekkel(const string& n);
virtual ~Tekkel();
virtual void blaft() const override;
};
De compiler geeft nu een foutmelding:
virtual void blaft() const override;
Microsoft Visual C++ geeft de volgende foutmelding:
'Tekkel::blaft' : method with override specifier 'override' did not override any base class methods
GNU gcc geeft de volgende foutmelding:
'virtual void Tekkel::blaft() const' marked override, but does not override
Conclusie:
override
achter de
functie te plaatsen zodat de compiler controleert of je wel echt aan het
overridden bent. 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 override;
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;
}
bool WhiskeyVat::geefBorrel() {
if (aantalBorrels > 0) {
--aantalBorrels;
cout << "Ik kom je helpen, drink deze borrel maar op!" << endl;
return true;}
cout << "Ik kan je niet helpen, mijn whiskey is op." << endl; return false;}
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 polymorf zijn! (Dit probleem
wordt het slicing problem genoemd.)
In het vorige voorbeeld hebben we gezien dat een pointer wel polymorf kan zijn. Een reference kan ook polymorf 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);
Microsoft Visual C++ geeft de volgende foutmelding:
'Hond' : cannot instantiate abstract class
due to following members:
'void Hond::blaf(void) const' : is abstract
GNU gcc geeft de volgende foutmelding:
cannot declare variable `h2' to be of type `Hond'
because the following virtual functions are abstract:
virtual void Hond::blaf() const
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 polymorfisme kan gebruiken. Ik definieer dat een roedel honden bestaat uit een groep honden die van verschillende soorten kunnen zijn.
class Roedel {
public:
void voegToe(Hond& h);
void blafAllemaal() const;
private:
vector<Hond*> honden;
};
Vragen:
Hond*
) op en niet gewoon
honden (Hond
)? Hond*
) op en niet
references naar honden (Hond&
)? void Roedel::voegToe(Hond& h) {
honden.push_back(&h);
}
void Roedel::blafAllemaal() const {
for (auto hp : honden)
{hp->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();
In voorbeeld 4 heb je een SintBernard
gezien die altijd een
WhiskeyVat
om zijn nek heeft. In dit voorbeeld maken we een
SintBernard
die 0 of 1 WiskeyVat
om zijn nek
heeft.
class SintBernard: public Hond {
public:
SintBernard(const string& n); /* aanmaken van een SintBernard zonder WhiskeyVat */
SintBernard(const string& n, int b); /* aanmaken van een SintBernard met WhiskeyVat gevuld met b borrels*/
SintBernard(const SintBernard& s);
virtual ~SintBernard();
SintBernard& operator=(const SintBernard& r);
virtual void blaf() const override;
void help();
private:
WhiskeyVat* vatPtr;
};
SintBernard::SintBernard(const string& n): Hond(n), vatPtr(0) {
cout << "Er is een SintBernard geboren!" << endl;
}
SintBernard::SintBernard(const string& n, int b): Hond(n), vatPtr(new WhiskeyVat(b)) {
cout << "Er is een SintBernard geboren!" << endl;
}
SintBernard::SintBernard(const SintBernard& s): Hond(s), vatPtr(0) {
if (s.vatPtr != 0) {
vatPtr = new WhiskeyVat(*(s.vatPtr)); }
cout << "Er is een SintBernard gekopieerd!" << endl;
}
SintBernard::~SintBernard() {
cout << "Er is een SintBernard bijna dood." << endl;
if (vatPtr != 0) {
while (vatPtr->geefBorrel()) /* drink alle borrels op */; }
delete vatPtr;
cout << "Er is een SintBernard gestorven." << endl;
}
SintBernard& SintBernard::operator=(const SintBernard& r) {
SintBernard t(r);
Hond::operator=(t);std::swap(vatPtr, t.vatPtr);
return *this;
}
void SintBernard::blaf() const {
cout << "WOEF, WOEF" << endl;
}
void SintBernard::help() {
if (vatPtr != 0) {
vatPtr->geefBorrel(); }
blaf();
}
Vragen:
SintBernard(SintBernard s);
operator
=
te definiëren?
operator
=
ook als
volgt gedeclareerd worden?
SintBernard& SintBernard::operator=(SintBernard r) {
SintBernard t(r); Hond::operator=(t);std::swap(vatPtr, r.vatPtr);
return
*this;
}
SintBernard&
return type en return
*
this
bij de
SintBernard::operator=
? Hond::operator=(t);
SintBernard
geen zelfgedefinieerde copy constructor en operator
=
heeft? int main() {
SintBernard h1("Boris", 10);
h1.help();
SintBernard h2("BlauweKnoop");
h2.help();
// Maak een kopietje van h1
SintBernard h3(h1);
for (int i = 0; i < 5; ++i) {
h3.help() /* help 5 keer */; }
h1.help();
// Doe een toekenning
h3 = h2;
h3.help();
cin.get();
return 0;
}
Alle source codes kun je hier downloaden: