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
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.
public
Ülemklassi kõikpublic
liikmed on ka alamklassipublic
liikmed ja ülemklassi kõikprotected
liikmed on ka alamklassiprotected
liikmed.protected
Ülemklassi kõikpublic
liikmed on alamklassiprotected
liikmed ja ülemklassi kõikprotected
liikmed on ka alamklassiprotected
liikmed.private
Ülemklassi kõikpublic
japrotected
liikmed on alamklassiprivate
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:
|
|
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 klassiIsik
parameetriteta konstruktor (vaikekonstruktor).Isik isik2{"Maie Maasikas", "60303032525"};
Objekti loob klassiIsik
kahe parameetriga konstruktor.Töötaja töötaja1{};
Objekti loomine toimub kahes etapis: alamklassiTöötaja
vaikekonstruktorist pöördutakse ülemklassiIsik
vaikekonstruktori poole ja seejärel täidetakseTöötaja
vaikekonstruktor.Töötaja töötaja2{"Kaie", "60202023535", "insener"};
Objekti loomine toimub kahes etapis: alamklassiTöötaja
kolme parameetriga konstruktorist pöördutakse ülemklassi kahe parameetriga konstruktori poole ja seejärel täidetakse klassiTöötaja
kolme parameetriga konstruktor.Töötaja töötaja3{"ehitaja"};
Kuna klassiTöötaja
ühe parameetriga konstruktoris ei ole pöördumist ülemklassi konstruktori poole, siis kompilaator lisab pöördumise ülemklassiIsik
vaikekonstruktori poole ja seejärel täidetakseTöö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.
Pärimine mitmest ülemklassist
Klassisl võib olla mitu ülemklassi. 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
. Peameetodis alamklassi Alam
tüüpi objektil ob
on juurdepääs mõlema ülemklassi avalikele liikmetele.
|
|
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()
. Peafunktsioonis 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.
|
|
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.
|
|
Ü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 objektide aadressid). Ka siin rakendatakse ülemklassi Kujund
funktsiooni joonista()
.
|
|
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)
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 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.
|
|
Näites on ka massiiv, mille elemendid viitavad Kujund
tüüpi objektidele. 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
|
|
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
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.
|
|
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. Eelmises näites muutub ainult see, et klassist Kujund
ei saa objekti teha:
|
|
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(); |
|
|
Klassi staatiliste liikmete kohta saab täpsemalt uurida aadressil https://en.cppreference.com/w/cpp/language/static