OOP I Klassid
Pärast selle praktikumi läbimist üliõpilane oskab
- luua klassitüüpi objekte ehk isendeid (instance)
- luua ning kasutada erinevaid konstruktoreid ja isendivälju
- kasutada võtmesõna
this
- luua ja kasutada erinevat tüüpi objektide massiive
- paigutada klassiga seotud koodi mitmesse faili
- üle defineerida operaatoreid
Klassid (classes)
- Klass on kasutaja poolt defineeritud andmetüüp.
- Klass koosneb liikmetest - andmetest ja funktsioonidest.
- Liikmefunktsioonid võivad määrata objekti tähenduse, loomise, kopeerimise, teisaldamise ja hävitamise.
- Liikmetele pääseb juurde kas
.
või−>
abil.
Klass on üks tähtsamaid programmeerimiskeele C++ elemente. C++ toetab objektorienteeritud programmeerimist (OOP). C++ klass defineerib uue andmetüübi, mida saab kasutada seda tüüpi objektide loomiseks. Objekt on klassi isend (instance). Lihtsa klassi üldkuju on järgmine:
class klassi_nimi { privaatsed liikmed (nt andmed ja funktsioonid) public: avalikud liikmed (nt andmed ja funktsioonid) }objekti nimede loetelu;
kus objekti nimede loetelu võib puududa. Klassi liikmed, mis on võtmesõna public
järel, on kättesaadavad kõikjal programmis. Võtmesõnu public
ja private
nimetatakse piiritlejateks (access specifiers). On ka kolmas piiritleja protected
, aga seda käsitletakse hiljem. Vaikimisi on piiritlejaks private
, st kui piiritlejat ei ole, siis klassi liikmed ei ole kättesaadavad väljaspool klassi (on privaatsed). Näiteks klassis Kast
class Kast{ double m_pikkus{}; double m_laius{}; double m_kõrgus{}; public: double ruumala(){ return m_pikkus * m_laius * m_kõrgus; } };
on privaatsed double
tüüpi isendimuutujad m_pikkus
, m_laius
, m_kõrgus
ja avalik funktsioon ruumala
kasti ruumala arvutamiseks. Võtmesõnu public
ja private
võib kasutada klassi definitsioonis mitu korda eri kohtades. Isendimuutujate nime ees kasutatakse sageli prefiksit m_
(sõnast member). Klassi definitsioon ise on objekti mall. Klassi nime abil saab luua klassi isendeid (instance) ehk objekte, st klassi nime saab kasutada andmetüübina.
Kast kast;
Klassi mitteprivaatseid liikmeid saab kätte operaatori .
abil:
#include <iostream> using namespace std; class Kast { public: double ruumala() { return m_pikkus * m_laius * m_kõrgus; } private: double m_pikkus{}; double m_laius{}; double m_kõrgus{}; }; int main() { Kast kast; double ruumala = kast.ruumala(); cout << "Kasti ruumala: " << ruumala << "\n"; return 0; } | Kasti ruumala: 0 |
Tavaline praktika on jätta klassi definitsiooni ainult isendimuutujate ja funktsioonide deklaratsioonid ja realisatsioonid (definitsioonid) kirjutada klassist eraldi. Viies klassi liikmete definitsioonid väljapoole klassi, saab klassist kiiremini ülevaate (eriti siis, kui funktsioonid on pikemad kui klassi definitsioon ise). Klassi definitsiooni koos liikmete deklaratsioonidega võib paigutada päisefaili .h
ja realisatsioonid .cpp
faili, kus saab päisefaili käsuga include
kaasata. Antud näites on lühiduse mõttes kõik siiski ühes failis. Funktsioonide ja teiste klassi liikmete realiseerimisel väljaspool klassi tuleb kasutada skoobioperaatorit ::
. Avaldis Kast::ruumala()
annab kompilaatorile teada, et funktsioon ruumala
kuulub klassi Kast
, st on klassi Kast
skoobis.
#include <iostream> using namespace std; class Kast { double m_pikkus{}; double m_laius{}; double m_kõrgus{}; public: double ruumala(); }; double Kast::ruumala() { return m_pikkus * m_laius * m_kõrgus; } int main() { Kast kast; double ruumala = kast.ruumala(); cout << "Kasti ruumala: " << ruumala << "\n"; return 0; } | Kasti ruumala: 0 |
Klassi isenditel on reeglina eri komplekt isendimuutujate väärtusi, sest vastasel korral oleks ju tegemist identsete objektidega. Isendimuutujate väärtused määravad klassi isendi oleku. Objekti kast
isendimuutujad on initsialiseeritud väärtusega 0
, mistõttu on ka ruumala 0
. Tavaliselt algväärtustatakse isendimuutujad klassi konstruktoris.
Konstruktorid (constructors)
Klassi konstruktor võimaldab luua klassist isendeid e objekte. Konstruktor on spetsiaalset tüüpi funktsioon, mis erineb tavalisest funktsioonist mitmel viisil. Konstruktoril on klassiga sama nimi ja ta ei tagasta midagi. Konstruktori nime ees ei ole tagastustüüpi, isegi mitte void
. Objekti loomisel kutsutakse konstruktor välja automaatselt. Klassis võib olla mitu konstruktorit, mis erinevad üksteisest parameetrite tüübi/arvu poolest (st on lubatud konstruktorite üledefineerimine). Kui klassis ei ole ühtegi konstruktorit, siis kompilaator lisab automaatselt parameetriteta vaikekonstruktori. Tänu vaikekonstruktorile toimus eelmises näites klassi Kast
isendi loomine käsuga Kast kast;
. Kui klassis on kasvõi üks parameetritega konstruktor, siis kompilaator vaikekonstruktorit ei lisa. Defineerime klassile Kast
kolme parameetriga konstruktori.
#include <iostream> using namespace std; class Kast { double m_pikkus{}; double m_laius{}; double m_kõrgus{}; public: // konstruktori deklaratsioon Kast (double pikkus, double laius, double kõrgus); double ruumala(); }; double Kast::ruumala() { return m_pikkus * m_laius * m_kõrgus; } // konstruktori definitsioon Kast::Kast(double pikkus, double laius, double kõrgus) { cout << "Kast konstruktoris.\n"; m_pikkus = pikkus; m_laius = laius; m_kõrgus = kõrgus; } int main() { Kast kast(1.1, 2.2, 3.3); cout << "Kasti ruumala: " << kast.ruumala() << "\n"; // Kast kast2; //Kompileerimisviga! return 0; } | Kast konstruktoris. Kasti ruumala: 7.986 |
Näites on klassi enda sees konstruktori deklaratsioon Kast (double pikkus, double laius, double kõrgus);
, konstruktor ise on defineeritud väljaspool klassi (kasutades skoobioperaatorit ::
). Kast
konstruktoril on kolm double
tüüpi parameetrit, mis konstruktori sees omistatakse isendimuutujatele. Klassi isend luuakse käsuga Kast kast{1.1, 2.2, 3.3};
. Klassi isendi loomisel edastatakse loogelistes sulgudes olevad parameetrid konstruktorile. Konstruktori esimese käsuga kuvatakse ekraanile teade Kast konstruktoris.
. Kuna nüüd on klassis Kast
üks parameetritega konstruktor, siis vaikekonstruktorit ei lisata ja käsk Kast kast2;
, mis vajab parameetriteta konstruktorit, annab kompileerimisvea.
Klassi saab tühja kehaga vaikekonstruktori lisada käsuga
Kast(){};
Selle asemel soovitatakse kasutada võtmesõna default
. See annab kompilaatorile teada, et on vaja lisada vaikekonstruktor (isegi, kui see sisaldab käske).
Kast() = default;
Konstruktori definitsioonis kasutasime isendimuutujatele omistamisi, mis võtavad enda alla mitu rida. Selle asemel võib kasutada efektiivsemat tehnikat, mida nimetatakse liikmete initsialiseerija nimekirjaks (member initializer list):
Kast::Kast(double pikkus, double laius, double kõrgus) : m_pikkus{pikkus}, m_laius{laius}, m_kõrgus{kõrgus} { cout << "Kast konstruktoris.\n"; }
Initsialiseerija nimekiri on eraldatud konstruktori parameetrite nimekirjast kooloniga :
, millele
järgneb initsialiseerimiste loetelu, nt m_pikkus{pikkus}
korral isendimuutuja m_pikkus
initsialiseeritakse parameetri pikkus
väärtusega. Võrreldes omistamisega konstruktori kehas toimub siin initsialiseerimine vahetult isendimuutuja loomise järel. Konstruktori keha võib ka tühjaks jääda, antud näites on seal ekraanile kuvamise käsk.
NB! Ka konstruktoris võib kasutada parameetrite vaikeväärtusi, nt
Kast (double pikkus, double laius, double kõrgus = 1);
Enesetest
Võtmesõna explicit
Kui klassis on ühe parameetriga konstruktor, siis objekti loomiseks võib kasutada kas initsialiseerimist või omistamist. Järgmises näites on klassi A
isendi loomiseks kasutatud käske A a1{5};
või A a2 = 4;
. Mõlemad loovad objekti, mille isendimuutuja m_a
väärtuseks on kas 5
või 4
.
#include <iostream> using namespace std; class A{ public: A(int a): m_a{a}{}; void kuva(){ cout << "m_a: " << m_a << "\n"; } private: int m_a{}; }; int main() { A a1{5}; a1.kuva(); A a2 = 4; a2.kuva(); return 0; } | m_a: 5 m_a: 4 |
On olukordi, kus selline omistamine võib tekitada mittesoovitud tulemuse. Järgmises näites annab klassi Kuup isendite ruumala võrdlemine soovitud tulemuse, kuid ruumala võrdlemise arvuga if (kuup1.kas_ruumala_suurem(100))
tekitab uue kuubi, mille ruumala on 1000000
.
#include <iostream> using namespace std; class Kuup{ public: Kuup() = default; Kuup(double külg): m_külg{külg}{}; double ruumala(){ return m_külg * m_külg * m_külg; } bool kas_ruumala_suurem(Kuup k){ return ruumala() > k.ruumala(); } private: double m_külg{}; }; int main() { Kuup kuup1{5}; Kuup kuup2{2}; if (kuup1.kas_ruumala_suurem(kuup2)){ cout << "kuup1 ruumala on suurem kui kuup2 ruumala\n"; } else{ cout << "kuup2 ruumala on suurem kui kuup1 ruumala\n"; } if (kuup1.kas_ruumala_suurem(100)){ cout << "kuup1 ruumala on suurem kui 100\n"; } else{ cout << "kuup1 ruumala on väiksem kui 100\n"; } return 0; } | kuup1 ruumala on suurem kui kuup2 ruumala kuup1 ruumala on väiksem kui 100 |
Seda kõrvalefekti saab vältida võtmesõnaga explicit
konstruktori ees, mis sunnib alati objekti loomiseks ilmutatult konstruktorit kasutama.
explicit Kuup(double külg): m_külg{külg}{};
Kuna funktsioonis kas_ruumala_suurem
on parameetriks Kuup k
, siis võrdlemine kuup1.kas_ruumala_suurem(100)
annab vea, sest parameetriks peab olema Kast
tüüpi objekt, mitte arv.
Konstruktorite delegeerimine
Klassis võib olla mitu konstruktorit, st konstruktoreid saab üle defineerida. Koodi kordamise vältimiseks võib üks konstruktor pöörduda teise poole. Järgmises näites on kuubi konstruktoris kasti konstruktori poole pöördumine explicit Kast(double külg): Kast{külg, külg, külg}
. Kuubi loomisel täidetakse kasti konstruktor 1. konstruktoris: ...
ja seejärel kuubi konstruktor 2. konstruktoris: ...
.
#include <iostream> using namespace std; class Kast { public: Kast() = default; Kast(double pikkus, double laius, double kõrgus): m_pikkus{pikkus}, m_laius{laius}, m_kõrgus{kõrgus}{ cout << "1. konstruktoris: " << pikkus << "\n"; }; explicit Kast(double külg): Kast{külg, külg, külg}{ cout << "2. konstruktoris: " << külg << "\n"; }; private: double m_pikkus{}; double m_laius{}; double m_kõrgus{}; }; int main() { Kast kast1{1.1, 2.2, 3.3}; Kast kuup{4.4}; return 0; } | 1. konstruktoris: 1.1 1. konstruktoris: 4.4 2. konstruktoris: 4.4 |
Enesetest
Võtmesõna this
Võtmesõna this
kasutatakse viidana jooksvale objektile. Kui on pöördumine objekti funktsiooni poole, siis kompilaator lisab pöördumisele peidetud viida vastavale objektile. Näiteks pöördumises
kast1.ruumala();on peidetud viit
Kast*
tüüpi ja viitab isendile kast1
(st on objekti kast1
aadress). Seda viita saab funktsiooni sees kasutada:
double ruumala(){ return this->m_pikkus * this->m_laius * this->m_kõrgus; }
Siin this->m_külg
viitab klassi Kast
konkreetse objekti kast
isendimuutujale m_külg
.
Tegelikult just selliselt kompilaator funktsiooni realiseeribki. Täiendame funktsiooni ruumala
double ruumala() { cout << "this: " << this << "\n"; return this->m_pikkus * this->m_laius * this->m_kõrgus; }
ja loome main
funktsioonis Kast
isendi
Kast kast{1.1, 2.2, 3.3}; cout << "&kast: " << &kast << "\n"; cout << "kast.ruumala(): " << kast.ruumala() << "\n";
Ekraanil kuvatud objekti kast
aadress ja this
viit on samad.
&kast: 0xcf64bff880 kast.ruumala(): this: 0xcf64bff880 7.986
Destruktorid (destructors)
Objektid, mis luuakse konstruktori abil, tuleb peale kasutamise lõppu mälust kustutada. See ülesanne sisaldab reeglina mälu vabastamist, failide sulgemist jms. Seda saab teha spetsiaalse funktsiooni, destruktori, abil. Kui näiteks konstruktor avab faili, siis destruktor peab selle sulgema. Destruktor kannab klassi nime, mille ees on märk ~
ja tal ei ole tagastustüüpi ega ka parameetreid. Destruktorit ei ole võimalik üle defineerida, st igal klassil on täpselt üks destruktor. Kui klassis ei ole defineeritud destruktorit, siis kompilaator lisab nn vaikedestruktori. Destruktor deklareeritakse klassi public
sektsioonis ja tal on järgmine süntaks:
~klassi-nimi();
Destruktor kutsutakse välja automaatselt objekti eluea lõpul. Täiendame eelmist näidet destruktoriga, mille sisuks on teksti "Kast destruktoris." ekraanile kuvamine. Luuakse kaks klassi Kast
isendit, üks kolme parameetriga konstruktori abil ja teine vaikekonstruktoriga, kus midagi ekraanile ei kuvata. Peale töö lõppu mõlemad objektid hävitatakse ja mõlema korral kutsutakse välja destruktor.
#include <iostream> using namespace std; class Kast { double m_pikkus{}; double m_laius{}; double m_kõrgus{}; public: Kast() = default; Kast (double pikkus, double laius, double kõrgus); // destruktori deklaratsioon ~Kast(); double ruumala(); }; Kast::Kast(double pikkus, double laius, double kõrgus) { cout << "Kast konstruktoris.\n"; m_pikkus = pikkus; m_laius = laius; m_kõrgus = kõrgus; } // destruktori definitsioon Kast::~Kast() { cout << "Kast destruktoris.\n"; } double Kast::ruumala() { return m_pikkus * m_laius * m_kõrgus; } int main() { Kast kast(1.1, 2.2, 3.3); cout << "Kasti ruumala: " << kast.ruumala() << "\n"; Kast kast2; cout << "Kasti ruumala: " << kast2.ruumala() << "\n"; cout << "Viimane rida main-is.\n"; return 0; } | Kast konstruktoris. Kasti ruumala: 7.986 Kasti ruumala: 0 Viimane rida main-is. Kast destruktoris. Kast destruktoris. |
NB! Destruktorit on mõtet defineerida ainult siis, kui sellel on mõistlik sisu. Näiteks, kui klassi liikmete hulgas on viidamuutujaid (pointers), siis rippuvate viitade vältimiseks on vaja need destruktoris kustutada.
Kui klassis on hõivatud mälu (nt käsuga new
), siis tuleb see destruktoris kindlasti vabastada.
Piilufunktsioonid (accessor functions ja mutator functions)
Klassi mitteprivaatseid liikmeid saab kätte operaatori .
abil. Sageli on vaja juurdepääsu klassi privaatsetele isendimuutujatele. Selleks saab klassi definitsiooni lisada avalikud piilufunktsioonid isendimuutujate kättesaamiseks ja muutmiseks:
#include <iostream> using namespace std; class Kast { double m_pikkus{}; double m_laius{}; double m_kõrgus{}; public: Kast() = default; Kast(double pikkus, double laius, double kõrgus); double getPikkus(); double getLaius(); double getKõrgus(); void setPikkus(double pikkus); void setLaius(double laius); void setKõrgus(double kõrgus); double ruumala(); }; double Kast::getPikkus(){ return m_pikkus; } double Kast::getLaius(){ return m_laius; } double Kast::getKõrgus(){ return m_kõrgus; } void Kast::setPikkus(double pikkus) { if (pikkus > 0) { m_pikkus = pikkus; } } void Kast::setLaius(double laius){ if (laius > 0){ m_laius = laius; } } void Kast::setKõrgus(double kõrgus){ if (kõrgus > 0){ m_kõrgus = kõrgus; } } double Kast::ruumala() { return m_pikkus * m_laius * m_kõrgus; } Kast::Kast(double pikkus, double laius, double kõrgus) : m_pikkus{pikkus}, m_laius{laius}, m_kõrgus{kõrgus} { cout << "Kast konstruktoris.\n"; } int main() { Kast kast; kast.setPikkus(1.1); kast.setLaius(2.2); kast.setLaius(-5); //ei tee midagi cout << "Kasti laius: " << kast.getLaius() << "\n"; kast.setKõrgus(3.3); cout << "Kasti ruumala: " << kast.ruumala()<< "\n"; return 0; } | Kasti laius: 2.2 Kasti ruumala: 7.986 |
Funktsiooni, mis tagastab isendimuutuja väärtuse, nimetatakse inglise keeles accessor funktsiooniks. Üldlevinud kokkuleppe kohaselt on sellise funktsiooni nimi prefiksiga get
, millele järgneb isendimuutuja nimi suure algustähega, nt getPikkus, getLaius, getKõrgus
. Funktsiooni, mille abil saame isendimuutujate väärtust muuta, nimetatakse inglise keeles mutator funktsiooniks ja selle nimes kasutatakse prefiksit set
, nt setPikkus, setLaius, setKõrgus
. Sageli nimetatakse neid funktsioone lihtsalt getters ja setters. Eesti keeles kasutatakse ka nimetust get- ja set funktsioonid. Meie näites on set
-funktsioonides parameetri kontroll, mis välistab kasti negatiivse mõõtme (konstruktor kasutab neid funktsioone).
Isendite loomine operaatoriga new
Keeles C++ on mäluhalduse operaatorid new
ja delete
. Neid operaatoreid kasutatakse mälu hõivamiseks ja hõivatud mälu vabastamiseks. Operaator new
hõivab mälu ja tagastab hõivatud mälu aadressi. Operaator new
sobib ka uue objekti loomiseks. Dünaamiliselt loodud objekt ei erine tavalisest objektist millegi poolest.
Kast* p_kast2 = new Kast(2.1, 4.2, 6.3); cout << "p_kast2 pikkus: " << p_kast2->getPikkus() << "\n"; cout << "p_kast2 ruumala: " << p_kast2->ruumala() << "\n"; delete p_kast2; | Kast konstruktoris. p_kast2 pikkus: 2.1 p_kast2 ruumala: 55.566 |
Käsuga Kast* p_kast2 = new Kast(2.1, 4.2, 6.3);
hõivatakse mälu ja luuakse klassi Kast
isend, millele hakkab viitama viidamuutuja p_kast2
. NB! Siin kasutatakse konstruktori poole pöördumiseks ümarsulge ()
.
Hõivatud mälu tuleb ka vabastada. Seda saab teha operaatori delete
abil. Objekti vabastamisel kutsutakse välja tema destruktor. Peale delete
käsku isend, millele p_kast2
viitab, lakkab olemast.
Klass mitmes failis
Paigutame nüüd klassi definitsiooni päisefaili kast.h
ja konstruktorite ja funktsioonide realisatsioonid faili kast.cpp
. Funktsioon main
jääb faili main.cpp
. Päisefaili paigutame alamkausta include
ja kast.cpp
faili alamkausta src
:
Failide sisu näeb siit:
Allikas https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#Rc-org annab järgmised soovitused päisefailidele:
- nimetada päisefailid laiendiga
.h
ja koodifailid laiendiga.cpp
- päisefailid ei tohiks sisaldada funktsioonide, konstruktorite, jne realisatsioone, välja arvatud
inline
funktsioonid - päisefail ei tohiks sisaldada globaalsel tasandil lauset
using namespace ...
Viit (pointer) klassi isendile (objektile)
Klassi isendeid saab omistada ka viidamuutujale. Vaatame näidet.
#include <iostream> #include "include/kast.h" using namespace std; int main() { Kast kast1{1.1, 2.2, 3.3}; // konstruktori poole pöördumine cout << "kast1 ruumala: " << kast1.ruumala() << "\n"; Kast* p_kast1 = &kast1; // cout << "p_kast1 ruumala: " << (*p_kast1).ruumala() << "\n"; cout << "p_kast1 ruumala: " << p_kast1->ruumala() << "\n"; return 0; } | Kast konstruktoris. kast1 ruumala: 7.986 p_kast1 ruumala: 7.986 p_kast1 ruumala: 7.986 |
Käsuga Kast kast1{1.1, 2.2, 3.3};
luuakse Kast
objekt, millele viitab muutuja kast1
. Väljatrükis on vastav konstruktori teade. Käsuga Kast* p_kast1 = &kast1;
luuakse viidamuutuja p_kast1
, mis hakkab viitama samale objektile.
Kuna viidamuutuja abil saab viidatavatele andmetele ligi operaatori *
abil, siis näiteks ruumala arvutamiseks tuleks kirjutada (*p_kast1).ruumala()
. See on pikk ja kohmakas, selle asemel on lihtsam võimalus kasutada nooleoperaatorit ->
:
Avaldis
viidamuutuja -> klassi-liige
on samaväärne avaldisega
(*viidamuutuja).klassi-liige
Näiteks (*p_kast1).ruumala()
ja p_kast1->ruumala()
on samaväärsed.
Objekti edastamine funktsioonile ja koopiakonstruktor (copy constructor)
Funktsioonile võib edastada klassitüüpi objekte samamoodi nagu kõiki teisi tüüpe. Objekt edastatakse funktsioonile kopeerimise teel. Järgmise näite
#include <iostream> using namespace std; class Minuklass { public: Minuklass(int i) : m_i{i} { cout << "Minuklass konstruktoris\n"; } ~Minuklass() { cout << "Minuklass destruktoris\n"; } void set_i(int i) { m_i = i; } int get_i() { return m_i; } private: int m_i{}; }; void muuda_objekti(Minuklass o) { o.set_i(5); cout << "See on 'i' muuda_objekti funktsioonis: "; cout << o.get_i() << "\n"; } int main() { Minuklass o{1}; muuda_objekti(o); cout << "See on 'i' main funktsioonis: "; cout << o.get_i() << "\n"; return 0; } | Minuklass konstruktoris See on 'i' muuda_objekti funktsioonis: 5 Minuklass destruktoris See on 'i' main funktsioonis: 1 Minuklass destruktoris |
väljatrükist on näha, et klassi Minuklass
konstruktori poole pöördutakse üks kord, aga destruktori poole kaks korda. Kui funktsioonile edastatakse objekt, siis tehakse sellest objektist koopia ja see koopia muutub funktsiooni parameetriks. Kui funktsiooni töö lõpeb, siis objekti koopia hävitatakse, st käivitatakse ka destruktor. Objektist koopia tegemisel ei pöörduta tavalise konstruktori poole, vaid spetsiaalse koopiakonstruktori (copy constructor) poole. Kompilaator lisab koopiakonstruktori automaatselt. Kompilaatori lisatud koopiakonstruktor teeb objektist bitikaupa koopia. Bitikaupa koopia objektist võib põhjustada ka probleeme. Näiteks, juhul kui objekti loomisel hõivatakse käsuga new
mälu ja destruktoris see mälu vabastatakse (nt delete
käsuga). Siis funktsiooni töö lõppedes hõivatud mälu vabastatakse ja esialgne objekt muutub kasutuskõlbmatuks.
Kuna funktsioonile muuda_objekti
edastatakse objektist o
koopia, siis peale funktsiooni täitmist objekti o
isendimuutuja i
ei ole muutunud, st tema väärtus on endiselt 1.
Koopiakonstruktori poole pöördutakse ka juhul kui kasutada initsialiseerimist:
Minuklass ob1{ob};
Siin objektist ob
tehakse koopiakonstruktori abil koopia ja sellele hakkab viitama muutuja ob1
.
NB! Kui kasutame ühe objektimuutuja teisele omistamist, nt
Minuklass ob1 = ob;
siis tehakse objektist bitikaupa koopia ja koopiakonstruktori poole ei pöörduta. Et vältida kõrvalefekte, on võimalik objekti jaoks omistamisoperaator =
üle defineerida.
Koopiakonstruktorit on võimalik üle defineerida, kus saab määrata, kuidas objektist koopia teha. Koopiakonstruktori üledefineerimisel on parameetriks viide konstantsele kopeeritavale objektile, sest vastasel korral tekib rekurrentne pöördumine koopiakonstruktori poole
Minuklass(const Minuklass& ob) { ... }
Koopiakonstruktori loomist saab ära keelata kasutades võtmesõna delete
Minuklass(const Minuklass& ob) = delete;
Klassid ja struktuurid
Struktuurid on osa C
-keelest. Klassid ja struktuurid erinevad ainult selle poolest, et klassis on kõik liikmed vaikimisi privaatsed (private), aga struktuuris on kõik liikmed vaikimisi avalikud (public). Millal on mõistlik kasutada klassi, millal struktuuri?
Vastavalt C++ soovitustele
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#Rc-org
tuleks kasutada klassi, kui selle liikmed peavad olema omavahel kooskõlas ja struktuuri, kui liikmeid võib muuta üksteisest sõltmatult. Kasutatakse ka invariandi mõistet.
Näiteks, struktuuriks võib olla Paar
struct Paar { // võime muuta x ja y teineteisest sõltumatult int x; int y; };
ja klassiks võib olla Kuupäev
class Kuupäev { public: // kontrollida, kas {päev, kuu, aasta} on kehtiv kuupäev ja initsialiseerida Kuupäev(int päev, int kuu, int aasta); // ... private: int päev; int kuu; int aasta; };
Oluline on, et alates objekti loomisest kuni objekti eluea lõpuni peab kuupäeva objekt sisaldama kehtivat kuupäeva.
Objektide massiiv
Objektidest saab luua massiivi samal viisil nagu lihttüüpi elementidest. Massiivi iga objekt tuleb luua konstruktoriga; kui objekti loomisel algväärtused puuduvad, kutsutakse välja vaikekonstruktor. Vaatame näidet, kus klassil Minuklass
on kaks konstruktorit. Funktsioonis main
luuakse massiiv klassi Minuklass
viie objektiga. Massiivi kolm esimest elementi on juba olemasolevad objektid, seega pöördutakse koopiakonstruktori poole duplikaatide tegemiseks. Viimase kahe elemendi jaoks ei ole valmis objekte, objektide loomiseks kasutatakse vaikekonstruktorit.
#include <iostream> using namespace std; class Minuklass { public: Minuklass(int i) : m_i{i} { cout << "Minuklass konstruktoris: " << i << "\n"; } Minuklass(): m_i{0}{ cout << "Minuklass vaikekonstruktoris\n"; } void set_i(int i) { m_i = i; } int get_i() { return m_i; } private: int m_i{}; }; int main() { Minuklass mk1{1}; Minuklass mk2{2}; Minuklass mk3{4}; Minuklass obj[5] = {mk1, mk2, mk3}; for(size_t j = 0; j < 5; ++j) { cout << "obj[" << j << "].get_i(): " << obj[j].get_i() << "\n"; } return 0; } | Minuklass konstruktoris: 1 Minuklass konstruktoris: 2 Minuklass konstruktoris: 4 Minuklass vaikekonstruktoris Minuklass vaikekonstruktoris obj[0].get_i(): 1 obj[1].get_i(): 2 obj[2].get_i(): 4 obj[3].get_i(): 0 obj[4].get_i(): 0 |
Massiivi läbimine viida abil
Vaatame eelmise näite massiivi läbimist viida abil. Klassitüüpi viidale on võimalik omistada massiiv:
Minuklass obj[5] = {mk1, mk2, mk3}; Minuklass* p_massiiv = obj;
Viit p_massiiv
viitab nüüd massiivi obj
esimesele elemendile. Suurendades viita ühe võrra
p_massiiv++;
liigub viit massiivi järgmisele elemendile. Objekti liikmeid (isendimuutujaid ja -funktsioone) saab viita kasutades kätte operaatori ->
abil.
int main() { Minuklass mk1{1}; Minuklass mk2{2}; Minuklass mk3{4}; Minuklass obj[5] = {mk1, mk2, mk3}; Minuklass* p_massiiv = obj; for (int j = 0; j < 5; ++j) { cout << "p_massiiv->get_i(): " << p_massiiv->get_i() << "\n"; p_massiiv++; } return 0; } | Minuklass konstruktoris: 1 Minuklass konstruktoris: 2 Minuklass konstruktoris: 4 Minuklass vaikekonstruktoris Minuklass vaikekonstruktoris p_massiiv->get_i(): 1 p_massiiv->get_i(): 2 p_massiiv->get_i(): 4 p_massiiv->get_i(): 0 p_massiiv->get_i(): 0 |
Operaatorite üledefineerimine (overloading)
Klassi isendeid võib kasutada avaldistes operandidena. Näiteks, kui ob1
ja ob2
on mingi klassi objektid, siis avaldised
ob1 + ob2; ob1++;
võiksid anda tulemuseks sama tüüpi objekti. Selleks tuleb üle defineerida operaatorid klassi jaoks. Operaatoreid defineerime üle siis, kui tulemuseks on mõistlik sisu. Vaatame näiteks klassi Vektor
(NB! See ei ole andmestruktuur!).
#include <iostream> #include <cmath> using namespace std; class Vektor { public: Vektor(double x, double y) : m_x{x}, m_y{y} {} Vektor() = default; double pikkus() { return round(sqrt(m_x * m_x + m_y * m_y) * 100) / 100.0; } void trüki() { cout << "(" << m_x << ", " << m_y << ")\n"; } private: double m_x{}; double m_y{}; }; int main() { Vektor v1{1, 2}; Vektor v2{3, 5}; v1.trüki(); v2.trüki(); cout << "v1.pikkus(): " << v1.pikkus() << '\n'; cout << "v2.pikkus(): " << v2.pikkus() << '\n'; return 0; } | (1, 2) (3, 5) v1.pikkus(): 2.24 v2.pikkus(): 5.83 |
Tasandi vektoril on kaks koordinaati (isendimuutujad m_x
ja m_y
). Klassil on kaks konstruktorit - vaikekonstrukror ja kahe parameetriga konstruktor. Vektori pikkuse arvutamiseks on liikmefunktsioon pikkus
, kus arvutamiseks läheb vaja teegi <cmath>
funktsioone sqrt
ja round
. Funktsioon trüki
kuvab ekraanile vektori koordinaadid sobival kujul. Tasandi vektoritega saab teha tehteid, nt liita, lahutada ja skalaariga korrutada. Kõigepealt defineerime skalaariga korrutamise. Selleks tuleb klassi lisada funktsioon, mille nimeks on
operator*=
. Skalaariga korrutamisel korrutame mõlemat koordinaati parameetriks oleva arvuga ja funktsioon ei tagasta midagi
void operator*=(double arv){ m_x *= arv; m_y *= arv; }
Funktsioon main
:
int main() { Vektor v1{1, 2}; v1 *= 2.5; v1.trüki(); return 0; } | (2.5, 5) |
Vektorite liitmise defineerime väljaspool klassi. Selleks, et funktsioon väljaspool klassi saaks kasutada klassi privaatseid isendimuutujaid, tuleb funktsioon deklareerida klassi sõbraks (friend). Seda tehakse klassi sees deklaratsiooniga. Kuna vektorite liitmisel operande ei muudeta (tulemuseks on uus vektor), on mõistlik liidetavad vektorid kuulutada konstantseteks. Kopeerimise vältimiseks on liidetavad vektorid antud viidetena (references).
#include <iostream> #include <cmath> using namespace std; class Vektor { public: Vektor(double x, double y) : m_x{x}, m_y{y} {} Vektor() = default; double pikkus() { return round(sqrt(m_x * m_x + m_y * m_y) * 100) / 100.0; } void trüki() { cout << "(" << m_x << ", " << m_y << ")\n"; } friend Vektor operator+(const Vektor& vasak, const Vektor& parem); void operator*=(double arv) { m_x *= arv; m_y *= arv; } private: double m_x{}; double m_y{}; }; Vektor operator+(const Vektor& vasak, const Vektor& parem) { return Vektor{vasak.m_x + parem.m_x, vasak.m_y + parem.m_y}; } int main() { Vektor v1{1, 2}; Vektor v2{3, 5}; Vektor v3 = v1 + v2; v3.trüki(); return 0; } | (4, 7) |
Defineerime veel üle vektori väljundvoogu saatmise. Selleks lisame klassi deklaratsiooni operaatori <<
üledefineerimise kohta, kus ostream
on standardväljund.
friend ostream& operator<<(ostream& os, const Vektor& v);
Operaatori <<
üledefineerimine on väljaspool klassi, kus väljundvoogu os
saadetakse vektori koordinaadid sobival kujul.
ostream& operator<<(ostream& os, const Vektor& v){ os << "(" << v.m_x << ", " << v.m_y << ")\n"; return os; }
main
funktsioonis saab nüüd vektorit mugavalt ekraanile kuvada
Vektor v1{1, 2}; Vektor v2{3, 5}; cout << v1 << v2 << '\n';
Tulemuseks on
(1, 2) (3, 5)
Täpsemalt saab operaatorite üledefineerimist uurida lehel https://en.cppreference.com/w/cpp/language/operators