Institute of Computer Science
  1. Courses
  2. 2023/24 spring
  3. Programming in C++ (LTAT.03.025)
ET
Log in

Programming in C++ 2023/24 spring

  • Pealeht
  • 1. Muutujad ja andmetüübid
  • 2. Keele põhikonstruktsioonid I
  • 3. Keele põhikonstruktsioonid II
  • 4. Klass, struktuur, mallid
  • 5. Dünaamiline mäluhaldus I
  • 6. Dünaamiline mäluhaldus II
  • 7. Kontrolltöö 1

Seitsmendal nädalal toimub 1. kontrolltöö

1. kontrolltöö näidis on Moodles

  • 8. Dünaamiline mäluhaldus III
  • 9. STL andmestruktuurid I
  • 10. STL andmestruktuurid II
  • 11. OOP I Klassid
  • 12. OOP II Pärilus ja polümorfism
12 OOP II Pärilus ja polümorfism
12.1 Kodutöö
12.2 Harjutused
12.3 Videolingid
  • 13. Erindite töötlemine
  • 14. Täiendavad teemad
  • 15. Kontrolltöö 2

Viieteistkümnendal nädalal toimub 2. kontrolltöö

  • 16. Projekti esitlus
  • Mõned viited - vajalikud kaaslased
  • Vanad materjalid
  • Juhendid
  • Viited

OOP II Pärilus ja polümorfism

Pärast selle praktikumi läbimist üliõpilane

  • oskab koostada klasse kasutades pärilust (inheritance)
  • teab, kuidas kasutada piiritlejaid (access specifier)
  • oskab kasutada funktsioonide ülekatmist (overriding) ja varajast seostamist (early binding)
  • teab, mis on virtuaalsed (virtual) funktsioonid
  • saab aru polümorfismi (polymorphism) mõistest
  • teab, mis on abstraktsed funktsioonid ja abstraktsed klassid

Sisukord

1. Pärilus5. Funktsioonide ülekatmine (overriding) ja varajane seostamine (early binding)9. Abstraktsed funktsioonid ja abstraktsed klassid
2. Konstruktorid päriluse korral6. Virtuaalsed (virtual) funktsioonid ja polümorfism (polymorphism)10. Staatilised (static) muutujad ja funktsioonid
3. Destruktorid päriluse korral7. Hübriidpärimine ja virtuaalsed ülemklassidEnesetestid
4. Pärimine mitmest ülemklassist8. Teisendamine dynamic_cast abil 

Pärilus

Pärilus on üks objektorienteeritud programmeerimise põhimõistetest. Pärilus on tarkvara korduvkasutus, kus üks klass (alamklass) pärib endale teise klassi (ülemklassi) andmed ja käitumise ja täiendab ennast uute võimalustega. Öeldakse, et alamklass on päritud ülemklassist. Alamklassi objekti ja ülemklassi objekti vahel on järgmine seos: alamklassi objekt on ülemklassi objekt, kuid mitte vastupidi. Näiteid pärimisest:

 
 

Näiteks, iga kolmnurk on kujund; iga nelinurk on kujund, iga võrdkülgne kolmnurk on kolmnurk, aga samal ajal ka kujund jne. Kolmnurgal on kõik kujundi omadused ja lisaks veel kolmnurgale iseloomulikud andmed ja omadused. Samas, iga kolmnurk ei ole võrdkülgne, iga nelinurk ei ole ristkülik jne.

 

Alamklassis näidatakse pärimist kooloniga

class Kolmnurk: public Kujund{ 
... 
};

Seni on klassi liikmed olnud kas piiritlejaga public või private. Kui klassi liige on public, on ta kättesaadav ka väljaspool klassi, samas klassi private liikmetele on juurdepääs ainult klassi seest (välja arvatud klassi friend funktsioonid). On ka kolmas piiritleja protected. Kui klassi liige on protected, siis väljaspool klassi ei ole ta kättesaadav, välja arvatud klassi pärimisel. Klassi pärimisel on see liige kättesaadav ka alamklassis.

Pärimine võib olla public, protected või private. Piiritleja puudumise korral on klassi pärimine private ja struktuuri pärimine public. Vaatame neid lähemalt.

Pärimine:

  • public

Ülemklassi kõik public liikmed on ka alamklassi public liikmed ja ülemklassi kõik protected liikmed on ka alamklassi protected liikmed.

  • protected

Ülemklassi kõik public liikmed on alamklassi protected liikmed ja ülemklassi kõik protected liikmed on ka alamklassi protected liikmed.

  • private

Ülemklassi kõik public ja protected liikmed on alamklassi private liikmed.

Ühelgi juhul ei ole ülemklassi private liikmed alamklassis kättesaadavad.

 
 

Konstruktorid päriluse korral

Konstruktoreid ei pärita, kuid ülemklassi konstruktori poole saab (ja tuleb) alamklassist vajadusel pöörduda. Kui alamklassi konstruktoris on parameetreid, mis kuuluvad ülemklassi, siis on soovitatav nende initsialiseerimiseks pöörduda ülemklassi konstruktori poole. Kui alamklassi konstruktoris on parameetreid ja ülemkassi konstruktori poole pöördumine puudub, siis kompilaator lisab pöördumise ülemklassi vaikekonstruktori () poole.

Vaatame näidet, kus Isik on ülemklass ja Töötaja on alamklass:

#include <iostream>
#include <string>
using namespace std;
class Isik {
public:
    Isik() {
        cout << "Isik() konstruktoris\n";
    };
    Isik(string nimi, string ik) : m_nimi{nimi}, m_ik{ik} {
        cout << "Isik(string, string) konstruktoris: " << m_nimi << " " << ik << '\n';
    }
private:
    string m_nimi{};
    string m_ik{};
};

class Töötaja : public Isik {
public:
    Töötaja() {
        cout << "Töötaja() konstruktoris\n";
    }

    Töötaja(string nimi, string ik, string amet) : Isik(nimi, ik), m_amet{amet} {
        cout << "Töötaja(string, string, string) konstruktoris: " << m_amet << '\n';

    }
    Töötaja(string amet) : m_amet{amet}{
        cout << "Töötaja(string) konstruktoris: " << m_amet << '\n';
    }
private:
    string m_amet{};
};

int main() {
    Isik isik1{}; // Isik() konstruktor (vaikekonstruktor)
    cout << "---\n";
    Isik isik2{"Maie Maasikas", "60303032525"}; //Isik(string, string) konstruktor
    cout << "---\n";
    Töötaja töötaja1{}; // Isik() konstruktor, Töötaja() konstruktor
    cout << "---\n";
    Töötaja töötaja2{"Kaie", "60202023535", "insener"}; // Isik(string, string) konstruktor, Töötaja (string, string, string) konstruktor
    cout << "---\n";
    Töötaja töötaja3{"ehitaja"}; // Isik() konstruktor, Töötaja(string) konstruktor
    return 0;
}
Isik() konstruktoris
---
Isik(string, string) konstruktoris: Maie Maasikas 60303032525
---
Isik() konstruktoris
Töötaja() konstruktoris
---
Isik(string, string) konstruktoris: Kaie 60202023535
Töötaja(string, string, string) konstruktoris: insener
---
Isik() konstruktoris
Töötaja(string) konstruktoris: ehitaja

Näites on nii ülemklassis kui ka alamklassis vaikekonstruktor. Vastavalt soovitustele

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#main

peab konstruktor looma täielikult initsialiseeritud objekti. Antud juhul garanteerib vaikekonstruktoriga objekti loomisel selle isendimuutujate initsialiseerimine klassis Isik

string m_nimi{}; // tühi sõne
string m_ik{}; // tühi sõne

ja klassis Töötaja

string m_amet{}; //tühi sõne

Alamklassis ei ole juurdepääsu ülemklassi privaatsetele liikmetele. Alamklassi Töötaja kolme parameetriga konstruktoris on pöördumine ülemklassi kahe parameetriga konstruktori poole ja seejärel isendimuutuja m_amet initsialiseerimine.

Töötaja(string nimi, string ik, string amet) : Isik(nimi, ik), m_amet{amet} {
    cout << "Töötaja konstruktoris: " << m_amet << '\n';
    }

Objektide loomisest lähemalt:

  • Isik isik1{}; Objekti loob klassi Isik parameetriteta konstruktor (vaikekonstruktor).
  • Isik isik2{"Maie Maasikas", "60303032525"}; Objekti loob klassi Isik kahe parameetriga konstruktor.
  • Töötaja töötaja1{}; Objekti loomine toimub kahes etapis: alamklassi Töötaja vaikekonstruktorist pöördutakse ülemklassi Isik vaikekonstruktori poole ja seejärel täidetakse Töötaja vaikekonstruktor.
  • Töötaja töötaja2{"Kaie", "60202023535", "insener"}; Objekti loomine toimub kahes etapis: alamklassi Töötaja kolme parameetriga konstruktorist pöördutakse ülemklassi kahe parameetriga konstruktori poole ja seejärel täidetakse klassi Töötaja kolme parameetriga konstruktor.
  • Töötaja töötaja3{"ehitaja"}; Kuna klassi Töötaja ühe parameetriga konstruktoris ei ole pöördumist ülemklassi konstruktori poole, siis kompilaator lisab pöördumise ülemklassi Isik vaikekonstruktori poole ja seejärel täidetakse Töötaja ühe parameetriga konstruktor.

Kui alamklass luuakse vaikekonstruktoriga, siis toimub automaatselt pöördumine ülemklassi vaikekonstruktori poole.

Kui alamklass luuakse parameetritega konstruktoriga ja konstruktoris puudub pöördumine ülemklassi konstruktori poole, siis toimub automaatselt pöördumine ülemklassi vaikekonstruktori poole.

Destruktorid päriluse korral

Kui konstruktorite korral algab objekti loomine ülemklassi konstruktori täitmisega, siis destruktorite korral algab objekti hävitamine alamklassi destruktorist. Seda illustreerib järgmine näide, kus on ainult konstruktorid ja destruktorid ja klasside hierarhia on Kujund<-Nelinurk<-Ruut

class Kujund {
public:
    Kujund(){cout << "Kujund konstruktoris\n";}
    ~Kujund(){cout << "Kujund destruktoris\n";}
};
class Nelinurk : public Kujund {
public:
    Nelinurk(){cout << "Nelinurk konstruktoris\n";}
    ~Nelinurk(){cout << "Nelinurk destruktoris\n";}
};
class Ruut : public Nelinurk {
public:
    Ruut(){cout << "Ruut konstruktoris\n";}
    ~Ruut(){cout << "Ruut destruktoris\n";}
};

int main() {
    Ruut r1{};
    cout << "----\n";
return 0;
}
Kujund konstruktoris
Nelinurk konstruktoris
Ruut konstruktoris
----
Ruut destruktoris
Nelinurk destruktoris
Kujund destruktoris

Pärimine mitmest ülemklassist

Klassil võib olla mitu ülemklassi ja klassil võib olla mitu alamklassi. Pärimisel võib tekkida klasside hierarhia. Joonisel on toodud pärimise põhijuhud. Tegelik hierarhia võib tekkida kõikide nende kombineerimisest.

 

Pärimisel tuleb iga ülemklassi ette lisada piiritleja (tavaliselt public). Alamklassis saab kasutada ülemklasside andmeid ja funktsioone. Näites on ülemklasside isendimuutujad protected, et nad oleksid alamklassis otse kättesaadavad. Privaatsete isendimuutujate kättesaamiseks tuleb kasutada avalikke piilumeetodeid (get, set). Ülemklassides Ülem1 ja Ülem2 on isendimuutujad m_x ja m_y, millele on juurdepääs alamklassis Alam. Funktsioonis main on alamklassi Alam tüüpi objektil ob juurdepääs mõlema ülemklassi avalikele liikmetele.

#include <iostream>
using namespace std;
class Ülem1{
protected:
    int m_x{};
public:
    void näitaMX(){
        cout << "m_x: " << m_x << '\n';
    }
};
class Ülem2{
protected:
    int m_y{};
public:
    void näitaMY(){
        cout << "m_y: " << m_y << '\n';
    }
};
class Alam: public Ülem1, public Ülem2{
public:
    void setXY(int x, int y){
        m_x = x;
        m_y = y;
    }
};
int main() {
    Alam ob{};
    ob.setXY(1, 2); // klassis Alam
    ob.näitaMX(); // klassis Ülem1
    ob.näitaMY(); // klassis Ülem2
    //ob.m_x{1}; // viga, m_x on kättesaadav vaid klassides Ülem1 ja Alam
    //ob.m_y{2}; // viga, m_y on kättesaadav vaid klassides Ülem2 ja Alam
    return 0;
}
m_x: 1
m_y: 2

Täpsemalt saab päriluse kohta uurida aadressil https://en.cppreference.com/w/cpp/language/derived_class

Funktsioonide ülekatmine (overriding) ja varajane seostamine (early binding)

Alamklass pärib kõik ülemklassi andmed ja funktsioonid. Alamklassis on võimalik nii andmeid kui ka funktsioone lisada. Mõnikord on vaja, et ülemklassis defineeritud funktsioon käituks alamklassis teisiti. Siis on võimalik defineerida alamklassis sama nime, parameetrite tüübi ja arvu ning tagastustüübiga funktsioon, mis peidab (hides) ülemklassi funktsiooni alamklassi objekti eest. Seda nimetatakase ka funktsioonide ülekatmiseks (overriding). Vaatame klasside hierarhiat Kujund<-Nelinurk<-Ristkülik, kus igas klassis on oma versioon funktsioonist joonista(). Funktsioonis main loome igast klassist objekti ja pöördume funktsiooni joonista() poole. Tulemus on ootuspärane: pöördumine on objekti tüüpi klassi funktsiooni poole, nt Nelinurk tüüpi objekti korral pöördutakse Nelinurk funktsiooni joonista poole jne.

#include <iostream>
using namespace std;
class Kujund {
public:
    void joonista() {
        cout << "Kujund: joonista()\n";
    }
};
class Kolmnurk : public Kujund {
public:
    void joonista() {
        cout << "Kolmnurk: joonista()\n";
    }
};
class Nelinurk : public Kujund {
public:
    void joonista() {
        cout << "Nelinurk: joonista()\n";
    }
};
class Ristkülik : public Nelinurk {
public:
    void joonista() {
        cout << "Ristkülik: joonista()\n";
    }
};
int main() {
    Kujund k1{};
    Kolmnurk kn1{};
    Nelinurk n1{};
    Ristkülik r1{};

    k1.joonista();
    kn1.joonista();
    n1.joonista();
    r1.joonista();
    return 0;
}
Kujund: joonista()
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()

Klasside pärimisel alamklassi tüüpi objekt on ka ülemklassi tüüpi objekt. See võimaldab ülemklassi tüüpi kasutada ka alamklassi korral. Järgmises näites on muutujad n2 ja r2 ülemklassi Kujund tüüpi. Kuigi nad tegelikult viitavad alamklassi objektidele, on nende tüüp Kujund ja pöördumisel funktsiooni joonista() poole käivitub klassi Kujund funktsioon joonista(). Seda nimetatakse varajaseks seostamiseks (early binding), kus kompilaator valib muutuja tüübi järgi funktsiooni, mille poole pöörduda.

int main() {
    Kujund k1{};
    Kolmnurk kn1{};
    Nelinurk n1{};
    Ristkülik r1{};

    shared_ptr<Kujund> kn2{make_shared<Kolmnurk>()}; // objekt on Kolmnurk
    shared_ptr<Kujund> n2{make_shared<Nelinurk>()}; // objekt on Nelinurk
    shared_ptr<Kujund> r2{make_shared<Ristkülik>()}; // objekt on Ristkülik
    kn2->joonista();
    n2->joonista();
    r2->joonista();
    return 0;
}
Kujund: joonista()
Kujund: joonista()
Kujund: joonista()

Ülemklassi tüübi alla viimine on kasulik andmekogumites (nt massiiv, vector, jne), kus ei saa hoida eri tüüpi andmeid. Näites on loodud massiiv Kujund tüüpi objektidest (massiivi elemendid on shared_ptr<Kujund>). Ka siin rakendatakse ülemklassi Kujund funktsiooni joonista().

int main() {
    Kujund k1{};
    Kolmnurk kn1{};
    Nelinurk n1{};
    Ristkülik r1{};
    shared_ptr<Kujund> kn0{make_shared<Kujund>()}; // objekt on Kujund
    shared_ptr<Kujund> kn2{make_shared<Kolmnurk>()}; // objekt on Kolmnurk
    shared_ptr<Kujund> n2{make_shared<Nelinurk>()}; // objekt on Nelinurk
    shared_ptr<Kujund> r2{make_shared<Ristkülik>()}; // objekt on Ristkülik

    shared_ptr<Kujund> kujundid[]{kn0, kn2, n2, r2}; // massiiv 

    for (shared_ptr<Kujund>& k: kujundid) {
        k->joonista();
    }
    return 0;
}
Kujund: joonista()
Kujund: joonista()
Kujund: joonista()
Kujund: joonista()

Oleme tutvunud funktsioonide üledefineerimise (overloading) ja ülekatmisega (overriding). Järgmises tabelis on toodud põhilised erinevused nende vahel:

 

Kuidas saaks korraldada nii, et ülemklassi tüüpi muutuja, mis viitab alamklassi objektile, pöörduks nö "õige" funktsiooni poole? Seda võimaldavad virtuaalsed (virtual) funktsioonid.

Virtuaalsed (virtual) funktsioonid ja polümorfism (polymorphism)

C++ toetab nii kompileerimisaegset kui ka käitusaegset polümorfismi. Kompileerimisaegne polümorfism saavutatakse funktsioonide ja operaatorite üledefineerimisega. Käitusaegne polümorfism saavutatakse kasutades pärimist ja virtuaalseid funktsioone.

 

Virtuaalne funktsioon on liikmefunktsioon, mis deklareeritakse ülemklassis ja kaetakse üle alamklassis. Funktsioonile lisatakse võtmesõna virtual . Kui ülemklassi tüüpi muutuja, mis viitab alamklassi objektile, pöördub virtuaalse funktsiooni poole, siis C++ otsustab täitmise käigus, millise funktsiooni poole pöörduda. Näiteks, kui funktsioon joonista() on virtuaalne, siis n2->joonista() pöördub Nelinurk funktsiooni poole ja r2->joonista() pöördub Ristkülik funktsiooni poole. Seda nimetatakse hiliseks seostamiseks (late binding) või ka dünaamiliseks (dynamic) seostamiseks. Alamklassis ülekaetud virtuaalse funktsiooni võib varustada määratlejaga override. Alamklassis virtuaalse funktsiooni ülekatmisel ei pea kasutama võtmesõna virtual, kuid seda on selguse mõttes siiski soovitav teha.

NB! Hiline seostamine töötab ainult viitade (pointer) või viidete (reference) korral.

#include <iostream>
using namespace std;
class Kujund {
public:
    virtual void joonista() {
        cout << "Kujund: joonista()\n";
    }
};
class Kolmnurk : public Kujund {
public:
    virtual void joonista() override {
        cout << "Kolmnurk: joonista()\n";
    }
};
class Nelinurk : public Kujund {
public:
    virtual void joonista() override {
        cout << "Nelinurk: joonista()\n";
    }
};
class Ristkülik : public Nelinurk {
public:
    virtual void joonista() override {
        cout << "Ristkülik: joonista()\n";
    }
};
int main() {
    Kujund k1{};
    Kolmnurk kn1{};
    Nelinurk n1{};
    Ristkülik r1{};

    shared_ptr<Kujund> kn0{make_shared<Kujund>()}; // Kujund
    shared_ptr<Kujund> kn2{make_shared<Kolmnurk>()}; // Kolmnurk
    shared_ptr<Kujund> n2{make_shared<Nelinurk>()}; // Nelinurk
    shared_ptr<Kujund> r2{make_shared<Ristkülik>()}; // Ristkülik
    cout << "---\n";

    shared_ptr<Kujund> kujundid[]{kn0, kn2, n2, r2}; // massiiv

    for (shared_ptr<Kujund>& k: kujundid) {
        k->joonista();
    }
    return 0;
}
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()
---
Kujund: joonista()
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()

Näites on ka massiiv, mille elemendid on shared_ptr<Kujund>). Tänu virtuaalsetele funktsioonidele valitakse täitmise käigus iga objekti (kuigi on Kujund tüüpi) korral sobiv funktsioon sõltuvalt sellest, mis tüüpi objektile viidamuutuja viitab.

Sõna polümorfism tähendab paljude vormide olemasolu. Polümorfism keeles C++ tähendab, et sama nimega liikmefunktsioonil on klasside hierarhias palju eri vorme. Defineerides funktsioonid virtuaalseteks, sõltub konkreetse funktsiooni valimine funktsiooni käivitava objekti tüübist. Ehk siis, virtuaalsed funktsioonid võimaldavad rakendada polümorfismi.

Virtuaalsete funktsioonide poole saab pöörduda ka otse (alam)klassi objekti korral, nagu on näha järgmises näites

Kujund k1{};
Kolmnurk kn1{};
Nelinurk n1{};
Ristkülik r1{};

k1.joonista();
kn1.joonista();
n1.joonista();
r1.joonista();
Kujund: joonista()
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()

Põhilised erinevused varajase ha hilise seostamise vahel on toodud järgmises tabelis:

 

Praktikud soovitavad alamklassides mittevirtuaalseid funktsioone selguse mõttes mitte üle katta, sest juurdepääs sellistele funktsioonidele on ainult täpselt sama tüüpi objektist ja klasside hierarhiat ei ole vaja, vt

Effective C++: 50 Specific Ways to Improve Your Programs and Designs (Addison-Wesley Professional Computing Series) Subsequent Edition, by Scott Meyers.

Täpsemalt saab virtuaalsete funktsioonide kohta uurida aadressil https://en.cppreference.com/w/cpp/language/virtual

Hübriidpärimine ja virtuaalsed ülemklassid

Vaatame erijuhtu, kus toimub hübriidpärimine. Siin klass A on ülemklassiks klassidele B ja C. Klassi D ülemklassideks on klassid B ja C. Klass D pärib klassi A omadused mööda kahte liini (läbi B ja läbi C). Klass D võib olla ka klassi A otseseks alamklassiks (joonisel murdjoon). Sellist ülemklassi A nimetatakse ka kaudseks baasklassiks (indirect base class).

 

Selline pärimine võib tekitada probleeme, sest klassi A kõik public ja protected liikmed päritakse klassi D kahekordselt, alguses klassi B kaudu ja seejärel klassi C kaudu. See toob sisse ebamäärasuse, mida tuleks vältida.

Sellist päritud liikmete dubleerimist saab vältida, deklareerides ühise ülemklassi virtuaalseks:

class A{
...
}; 
class B : virtual public A{
...
};
class C : virtual public A{
...
};
class D : public B, public C{
...  // siia kopeeritakse ainult üks koopia klassist A 
};

Kui klass on deklareeritud virtuaalseks ülemklassiks, siis C++ hoolitseb selle eest, et sellest päritakse ainult üks koopia.

Teisendamine dynamic_cast abil

Operaatori dynamic_cast abil on võimalik alamklassi teisendada ülemklassiks. Olgu meil klassid Isik ja Tudeng, kusjuures Tudeng on klassi Isik alamklass. Isikul on nimi ja tudengil lisaks õppekava. Mõlema klassi jaoks on defineeritud operator<<, kusjuures alamklassi operator<< definitsioonis ei pea ülemklassi oma üle kordama. Lühiduse eesmärgil on kogu kood ühes failis.

#include <iostream>
#include <string>
using namespace std;
class Isik{
public:
    Isik() = default;
    Isik(string nimi): m_nimi{nimi}{}
    friend ostream& operator<< (ostream& os, Isik& i);
private:
    string m_nimi{};
};
class Tudeng : public Isik{
public:
    Tudeng(string nimi, string oppekava) : Isik(nimi), m_oppekava{oppekava}{}
    friend ostream& operator<<(ostream& os, Tudeng& t);
private:
    string m_oppekava{};
};
int main() {
    Isik isik{"Kalle Kass"};
    Tudeng tudeng{"Mia Moos", "informaatika"};
    cout << isik << '\n';
    cout << tudeng << '\n';
    return 0;
}
ostream& operator<< (ostream& os, Isik& i){
    os << "Isik: " << i.m_nimi;

    return os;
}
ostream& operator<<(ostream& os, Tudeng& t){
    os << dynamic_cast<Isik&>(t) << " on tudeng õppekaval " << t.m_oppekava;

    return os;
}
Isik: Kalle Kass
Isik: Mia Moos on tudeng õppekaval informaatika

NB! Seda, kas dynamic_cast on võimalik, saab kontrollida ainult siis, kui kasutada viitasid (pointer).

Täpsemalt saab uurida aadressil https://en.cppreference.com/w/cpp/language/dynamic_cast

Abstraktsed funktsioonid ja abstraktsed klassid

Mõnikord ei ole võimalik ülemklassis virtuaalsele funktsioonile sisu anda. Sellisel juhul võib selle funktsiooni kuulutada abstraktseks ehk puhtaks virtuaalseks funktsiooniks (pure virtual function). Kuulutame klassis Kujund virtuaalse funktsiooni joonista() abstraktseks:

virtual void joonista() = 0;   

Kuna nüüd klass Kujund sisaldab funktsiooni, millel puudub realisatsioon, siis sellest klassist isendit (objekti) teha ei saa. Sellist klassi nimetatakse abstraktseks (abstract). Abstraktsest klassist pärimisel tuleb abstraktsed funktsioonid üle katta. Kui seda ei tehta, on päritud klass samuti abstraktne. Abstraktset klassi võib kasutada viida või viite tüübina. Ülekaetud funktsioonid tuleks (ja on soovitav) varustada kas võtmesõnaga virtual või määratlejaga override. Näites on kasutatud mõlemaid. Eelmises näites muutub ainult see, et klassist Kujund ei saa objekti teha:

#include <iostream>
using namespace std;
class Kujund {
public:
    virtual void joonista() = 0; // abstraktne virtuaalne funktsioon
};
class Kolmnurk : public Kujund {
public:
    virtual void joonista() override {
        cout << "Kolmnurk: joonista()\n";
    }
};
class Nelinurk : public Kujund {
public:
    virtual void joonista() override {
        cout << "Nelinurk: joonista()\n";
    }
};
class Ristkülik : public Nelinurk {
public:
    virtual void joonista() override {
        cout << "Ristkülik: joonista()\n";
    }

};

int main() {
    //Kujund k1{}; // Klassist Kujund ei saa objekti teha
    Kolmnurk kn1{};
    Nelinurk n1{};
    Ristkülik r1{};

   // k1.joonista();
    kn1.joonista();
    n1.joonista();
    r1.joonista();
    cout << "---\n";

    Kujund* kn2{&kn1}; // Klassi Kujund võib viida tüübina kasutada
    Kujund* n2{&n1};
    Kujund* r2{&r1};
    kn2->joonista();
    n2->joonista();
    r2->joonista();
    cout << "---\n";

    Kujund* kujundid[]{kn2, n2, r2}; // Siit on eemaldatud k1
    for (Kujund* k: kujundid) {
        k->joonista();
    }
    return 0;
}
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()
---
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()
---
Kolmnurk: joonista()
Nelinurk: joonista()
Ristkülik: joonista()

Kuna abstraktne klass sisaldab abstraktseid funktsioone, siis saab seda kasutada alamklasside jaoks liidesena (interface).

Abstraktsete klasside kohta saab täpsemalt uurida aadressil https://en.cppreference.com/w/cpp/language/abstract_class

Staatilised (static) muutujad ja funktsioonid

Mõnikord on vaja, et klassi liige oleks seotud klassiga, mitte klassi objektidega. Näiteks laenude intressimäär (eeldame, et kõigil laenudel on sama intressimäär). Või on vaja loendada, mitu laenu on võetud. Üks lahendus objektide loendamiseks on globaalne muutuja, mida suurendatakse konstruktoris. C++ võimaldab seda lahendada klassimuutujaga, mille ees on piiritleja static. Vaatame näidet klassist Laen, kus on ainult privaatsed väljad intressi ja laenude arvu jaoks.

private:
    static int LaenudeArv;
    static double intress;

Kuna need väljad ei ole seotud ühegi klassi objektiga ja ei ole konstantsed (const, consexpr), siis saab neid algväärtustada väljaspool klassi, kasutades klassi skoobi operaatorit Laen::.

double Laen::intress = 0.001;
int Laen::LaenudeArv = 0;

Klassi staatilised liikmed eksisteerivad väljaspool klassi objekte, st objektid ei sisalda staatiliste andmeliikmetega seotud andmeid. On ainult üks intress ja laenudeArv, mida jagavad kõik Laen objektid.

Staatilised võivad olla ka funktsioonid. Staatilise funktsiooni sisu defineerimine võib olla ka väljaspool klassi. Väljaspool klassi ei pea enam võtmesõna static kasutama. Staatilised liikmed on kättesaadavad klassi kõikides funktsioonides (ka sellistes, mis ei ole static). Klassi liikmefunktsioonides on staatilised liikmed kättesaadavad otse, ilma skoobioperaatorita.

Väljaspool klassi saab mitteprivaatsete staatiliste liikmete poole pöörduda, kasutades kas klassi skoobioperaatorit või klassi objekti (ka viita), nt

Laen::getLaenudeArv();   
l1.getLaenudeArv(); 
#include <iostream>
#include <string>
using namespace std;
class Laen {
public:
    Laen() {
        LaenudeArv += 1; // Suurendatakse iga objekti loomisel
    }
    static double getIntress() {
        return intress;
    }
    static void setIntress(double);
    static int getLaenudeArv() {
        return LaenudeArv;
    }
private:
    static int LaenudeArv;
    static double intress;
};
double Laen::intress = 0.001; // Algväärtustamine
int Laen::LaenudeArv = 0; // Algväärtustamine
void Laen::setIntress(double uusIntress) { 
    intress = uusIntress;
}
int main() {
    Laen l1{};
    cout << "Laenude arv: " << l1.getLaenudeArv() << " " << Laen::getLaenudeArv() << '\n';
    cout << "Intress: " << l1.getIntress() << " " << Laen::getIntress() << '\n';
    Laen l2{};
    l1.setIntress(0.05); // või Laen::setIntress(0.05); 
    cout << "Laenude arv: " << l1.getLaenudeArv() << " " << l2.getLaenudeArv() << '\n';
    cout << "Intress: " << l1.getIntress() << " " << l2.getIntress() << '\n';
    return 0;
}
Laenude arv: 1 1
Intress: 0.001 0.001
Laenude arv: 2 2
Intress: 0.05 0.05

Klassi staatiliste liikmete kohta saab täpsemalt uurida aadressil https://en.cppreference.com/w/cpp/language/static

Enesetestid

NB! Enesetestides eeldame, et on kasutatud standardnimeruumi (using namespace std;)

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>


  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment