Extra voorbeelden bij Object Georiënteerd Programmeren in C++.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.

Voorbeeld 1.

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:

Voorbeeld 2

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.

Voorbeeld 3.

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:

Voorbeeld 4.

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:

Voorbeeld 5.

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

Voorbeeld 6.

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:

Voorbeeld 7.

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:

void Roedel::voegToe(Hond& h) {
    honden.push_back(&h);
}

void Roedel::blafAllemaal() const {
    for (auto hp : honden) {
        hp->blaf();
    }
}

Vraag:

Voorbeeld 8.

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:

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;
}

Source code van alle voorbeelden.

Alle source codes kun je hier downloaden: