© 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");
Borland C++ Builder => Warning: Non-const function Hond::setNaam(const string &) called for const object
Microsoft Visual C++ => Error: 'Hond::setNaam' : cannot convert 'this' pointer from 'const Hond' to 'Hond &'
GNU gcc => Error: no matching function for call to `Hond::setNaam(const char[4]) const'
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.
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) {
Borland C++ Builder geeft de volgende foutmelding:
'Hond::naam' is not an unambiguous base class of 'Tekkel'
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 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
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 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 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); Borland C++ Builder geeft de volgende foutmelding: Cannot create instance of abstract class 'Hond' Class 'Hond' is abstract because of 'Hond::blaf() const = 0' 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 (vector<Hond*>::size_type i(0); i<honden.size(); ++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();
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;
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);
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) {
std::swap(vatPtr, r.vatPtr);
return *this;
}
SintBernard&
return type en return
*this
bij de
SintBernard::operator=
?
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: