Klass, struktuur, mallid
Pärast selle praktikumi läbimist üliõpilane
- teab, mis on klass ja kuidas klassi liikmetele juurde pääseda
- teab, mis on struktuur ja kuidas seda kasutada
- teab, mis on funktsioonimallid ja mille poolest erineb funktsioon funktsioonimallist
- oskab funktsioonimalle luua ja kasutada nii ühe kui mitme parameetriga
- oskab analüüsida mallide kasutamisel tekkivaid tüüpidega seotud veateateid
- teab, mis on klassimall ja oskab klassimalli defineerida
Sisukord
Enesetestid |
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
.
abil (ka−>
abil, kuid sellest hiljem).
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, kuid privaatsed liikmed (võtmesõna private
võib ka puududa) on kättesaadavad ainult klassi sees. 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{ // privaatsed isendimuutujad double m_pikkus{}; double m_laius{}; double m_kõrgus{}; // avalik funktsioon ruumala 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{}; // või Kast kast;
Klassi mitteprivaatseid liikmeid saab kätte operaatori .
abil. Järgmises näites on klassis Tudeng
avalikud liikmed eesnimi
, perenimi
ja sünniaasta
, mida saab väljaspool klassi main
funktsioonis muuta.
#include <iostream> #include <string> #include <format> using namespace std; class Tudeng{ public: string eesnimi{}; string perenimi{}; unsigned int sünniaasta{}; }; int main() { Tudeng tudeng{}; tudeng.eesnimi = "Mai"; tudeng.perenimi = "Maasikas"; tudeng.sünniaasta = 2005; cout << format("Tudeng: {} {}, sünniaasta: {}", tudeng.eesnimi, tudeng.perenimi, tudeng.sünniaasta) << '\n'; return 0; } | Tudeng: Mai Maasikas, sünniaasta: 2005 |
Järgmises klassis Kast
on privaatsed isendiväljad m_pikkus
, m_laius
ja m_kõrgus
, aga liikmefunktsioon ruumala
on public
, st kättesaadav kõikjalt. Privaatseid isendivälju saab väärtustada kas isendi loomisel konstruktori abil või avalike liikmefunktsioonide abil. Funktsioonis main
luuakse klassi Kast
isend käsugaKast kast;
. Siin isendimuutujate eraldi väärtustamist ei toimu, aga vaikimisi on arvuliste isendimuutujate väärtuseks null.
#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; // 'Kast' isendi loomine double ruumala = kast.ruumala(); cout << "Kasti ruumala: " << ruumala << "\n"; //kast.m_pikkus = 1; // viga, sest m_pikkus on privaatne 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: // funktsiooni 'ruumala' deklaratsioon double ruumala(); }; // funktsiooni 'ruumala' definitsioon 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); // funktsiooni 'ruumala' deklaratsioon double ruumala(); }; // funktsiooni 'ruumala' definitsioon 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
Struktuurid (structures)
Struktuur (struktuuritüüp) on pärit C-keelest. Struktuur on kogum nimedega varustatud komponentidest, mis võivad olla eri tüüpi. Struktuuri deklaratsioon kirjeldab malli, mida saab kasutada struktuuriobjektide (structure objects) loomiseks. Muutujaid, millest struktuur koosneb, nimetatakse struktuuri liikmeteks või elementideks. Tavaliselt on struktuuri elementidel omavahel loogiline seos. Struktuuri deklareeritakse võtmesõnaga struct
, millele järgneb struktuuri nimi ja loogelistes sulgudes struktuuri liikmed. Peale sulgevat loogelist sulgu võib olla struktuurimuutujate loetelu. Struktuuri liikmetele pääseb juurde.
operaatori abil. Näiteks järgmine kood deklareerib kaks struktuurimuutujat
a_info
ja b_info
(kasutatakse teeki <string>
):
struct aadress{ string tänav; string linn; unsigned int postiindeks; }a_info, b_info; a_info.tänav = "Anne"; a_info.linn = "Tartu"; a_info.postiindeks = 50603; b_info.linn = "Teadmata"; cout << "Aadress:\n" << a_info.tänav + " tänav\n" + a_info.linn + "\n" + to_string(a_info.postiindeks) << "\n"; | Aadress: Anne tänav Tartu 50603 |
Struktuuri üldkuju on järgmine:
struct struktuuritüübi-nimi{ tüüp liikme-nimi; ... tüüp liikme-nimi; }struktuurimuutujate loetelu;
Struktuuritüübi nimi või struktuurimuutujate loetelu võivad puududa (kuid mitte mõlemad).
Klasside ja struktuuride nimede korral soovitatakse kasutada CamelCase
vormingut, st ka esimene sõna algab suure tähega:
class ArvudePaar{ int x{}; int y{}; };
Täpsemalt saab struktuuritüübi võimalustest uurida siit: https://en.cppreference.com/w/c/language/struct
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õltumatult. 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.
Funktsioonimallid (generic functions)
Funktsioonimallid on, nagu nimi ütleb, funktsioonide mallid. Kui senini oleme pidanud sama sisuga, kuid erinevat tüüpi parameetrite korral funktsioonid üle defineerima, siis funktsioonimalli abil on võimalik kõikide tüüpide jaoks kirjutada üks funktsioonimall.
Olgu meil näiteks kaks funktsiooni:
void printNum(int num) { cout << num << '\n'; } void printNum(double num) { cout << num << '\n'; }
Funktsioonimalli abil on võimalik kirjutada need funktsioonid ühe mallina järgmiselt:
template<typename T> void printNum(T num) { cout << num << '\n'; }
Funktsioonimalli päises on template<typename T>
, kus T
tähistab üldistatud tüüpi. Tavaliselt tähistatakse üldistatud tüüpe suurte tähtedega. Selle malli alusel oskab kompilaator genereerida vastavad funktsioonid tüüpidele, millega seda funktsiooni välja kutsutakse.
Funktsioonimallide kasutamine
Funktsioonimallide kasutamine on sarnane tavalise funktsioonide väljakutsega. Erinev on see, et nüüd tuleb kompilaatorile teada anda mallis kasutatav andmetüüp. Eelnevalt loodud malli saame kasutada järgmiselt:
int main() { printNum<double>(2.5); // 2.5 printNum<int>(1); // 1 return 0; }
Funktsiooni nime järel noolsulgudes on tüüp, mis annab kompilaatorile teada, mis tüübiga funktsioon oleks mallist vaja luua.
Kui aga kompilaator suudab tüübid ise välja mõelda (type inference), ei pea kasutaja tüüpi määrama. Sellist väljakutset nimetatakse ilmutatamata (implicit) väljakutseks. Etteantud tüübiga väljakutseid nimetatakse ilmutatud (explicit) väljakutseteks.
Näited ilmutamata väljakutsete kohta:
int main() { printNum(2.5); // tüübiks double printNum(1); // tüübiks int printNum<>(0); // noolsulud on lubatud ka tühjaks jätta, tüübiks int return 0; }
Selguse mõttes on soovitatav siiski pöördumisel tüüp ilmutatud kujul ette anda.
Muutuja tüüpi saab kindlaks teha funktsiooniga typeid
, mille väljundile rakendada funktsiooni name()
. Esitame siin kogu programmi. Väljundis vastab tüübile double
nimi d
, tüübile int
nimi i
.
#include <iostream> using namespace std; template<typename T> void printNum(T num) { cout << num << ' ' << typeid(num).name() << '\n'; } int main() { printNum<double>(2.5); printNum<int>(1); printNum<int>(0); return 0; } | 2.5 d 1 i 0 i |
Loodud funktsioonimalli on võimalik kasutada ka näiteks sõne (string) korral:
int main() { printNum<string>("Tere, maailm!"); // Tere, maailm! return 0; }
Tegelikult antud mallis parameetriteks sobivad praegu kõik tüübid, mida on võimalik saata väljundvoogu (suur osa praeguseni kasutatud tüüpe).
Mallifunktsiooni parameetriks võib olla ka tagastustüüp:
template <typename T> T suurem(T esimene, T teine){ return (esimene >= teine) ? esimene : teine; }
Järgmine joonis illustreerib kompilaatori poolt genereeritud koodi:
NB! Kui kasutada päisefaili (.h), siis peab mallifunktsioon tervikuna asuma päisefailis.
C++20 päis <concepts>
Alates C++20 versioonist on keele standardteegis olemas päis <concepts>
, kus on defineeritud mõned piirangud, mida saab funktsioonimallide parameetritele seada. Piirame malli parameetreid selliselt, et funktsiooni saab välja kutsuda vaid täisarvu (int) või ujukomaarvu (double) korral.
Lisame malli päisesse requires integral<T> || floating_point<T>
template<typename T> requires integral<T> || floating_point<T> // piirab võimalikud tüübid (täisarv või ujukomaarv) void printNum(T num) { cout << num << '\n'; }
Selle malli kasutamisel string
tüübiga saame kompileerimisvea.
int main() { printNum<double>(2.0); printNum<int>(1); //printNum<string>("Tere, maailm!"); //viga kompileerimisel return 0; }
Paneme tähele, et nüüd ei ole võimalik funktsiooni kutsuda sõnega.
CLion annab näiteks sellise teavituse:
Teiste tööriistade ja kompilaatorite veateated võivad olla täiesti erinevad.
Mitu tüübiparameetrit
Mallidele on võimalik ette anda ka mitu erinevat tüübiparameetrit.
Järgmises näites on tüübiargumente kolm. Selles näites on oluline, et parameetrite tüübid sobiksid funktsiooni to_string
argumentideks.
template<typename T, typename U, typename V> requires integral<T> || floating_point<T> string sonena(T t, U u, V v){ return to_string(t) + "; " + to_string(u) + "; " + to_string(v); }
Funktsiooninmalli kasutamisel argumentide tüübid noolsulgudes peavad vastama argumentide tüüpidele funktsiooni poole pöördumisel.
cout << sonena<double, int, int>(25.1, 12, 34) << '\n'; cout << sonena<int, double, double>(25, 12.34, 34.5) << '\n'; | 25.100000; 12; 34 25; 12.340000; 34.500000 |
Funktsiooni to_string
väljund erineb ujukomaarvude korral sellest, mida me näeme siis, kui saadame ujukomaarvu otse väljundvoogu cout
.
Fikseeritud tüübiparameetrid
Oleme siiani T
ette kirjutanud võtmesõna typename
(typename
asemel võib kirjutada ka class
, aga soovitatav on siiski typename
). See ütleb lihtsalt, et tegemist on mingi tüübiga. Antud võtmesõna asemele on võimalik kirjutada ka fikseeritud tüübi, mis tüüpi antud tüübiargument olema peaks.
Vaatame näidet:
template<typename T, int N> void erinevadTyybid(T t) { cout << "Sain väärtuse: " << t << '\n'; cout << "Lisaks sain täisarvu: " << N << '\n'; }
Antud mallil on kaks tüübiparameetrit: T
ja N
. Parameeter T
võib olla suvalist tüüpi ja N
peab olema täisarv. Paneme tähele, et N
ei ole funktsiooni parameeter.
Malli saame kasutada järgmiselt. Noolsulgude vahel tuleb määrata nii tüüp kui ka konkreetset tüüpi argument.
int main() { erinevadTyybid<string, 10>("Tere!"); erinevadTyybid<double, 100>(0.5); return 0; } | Sain väärtuse: Tere! Lisaks sain täisarvu: 10 Sain väärtuse: 0.5 Lisaks sain täisarvu: 100 |
Tüüpide vaikeväärtused
Funktsioonimalli parameetritele on võimalik anda ka vaikimisi väärtusi. Vaikeväärtused tulevad kasutusele siis, kui funktsiooni poole pöördumisel noolsulgude vahel tüüpi ei määrata või (mingitel tingimustel) tüüpi pole võimalik tuletada. Kõige kasulikum on vaikimisi väärtuseid kasutada kindlat tüüpi argumentide korral (mitte typename
korral).
Vaatame funktsioonimalli, kus konkreetse tüübi int
korral on ette antud vaikeväärtus
template<int N = 1> void printN() { std::cout << N << '\n'; }
Malli saame kasutada järgmiselt:
int main() { printN(); //tüübiargument puudub, kasutatakse vaikeväärtust printN<10>(); return 0; } | 1 10 |
Võtmesõnaga typename
määratud tüübile saame samuti anda vaikimisi väärtuse:
template<typename T = const char*> T printAndReturn(T t) { std::cout << t << '\n'; return t; } int main() { auto tulemus = printAndReturn<string>("olen std::string"); auto tulemus2 = printAndReturn("olen char"); return 0; } | olen std::string olen char |
Märkus: Tüüp char*
on C keele ajast sõnede defineerimiseks.
Programmeerimiskeskkonnad näitavad funktsiooni poole pöördumisel argumentide tüübid ette, nt CLion korral näeme pöördumisi järgmiselt:
Funktsiooni poole pöördumistes on näha, et ilmutatult argumendi määramisel valitakse see tüübiks. Argumenti määramata võeti kasutusele vaikimisi määratud tüüp const char*
(rohkem juttu tulevastes praktikumides).
Märkus: C++ oskab automaatselt char*
muuta tüübiks string
. Seetõttu saame anda funktsiooni argumendiks jutumärkide vahel oleva sõne.
Varieeruva parameetrite arvuga (variadic arguments) mallid
Malle on võimalik luua ka selliseid, kus ei ole vaja täpselt kirja panna, mitu tüüpi (ja ka mitu parameetrit) funktsioonile ette antakse.
Loome sellise malli
template<typename ...T> void mulOnSuvalineArvArgumente(T ...argumendid) { for (auto argument : {argumendid...}) { std::cout << argument << '\n'; } }
Siin on kasutatud forEach
tsüklit kõikide argumentide läbimiseks.
Malli kasutamine:
int main() { mulOnSuvalineArvArgumente<string, string>("tere", "maailm"); mulOnSuvalineArvArgumente<int, int, int>(1, 2, 3); return 0; } | tere maailm 1 2 3 |
Oluline on, et ühel pöördumisel kõik argumendid oleksid sama tüüpi. Näiteks annab pöördumine mulOnSuvalineArvArgumente<int, double>(1, 2.0)
veateate forEach tsüklis. Seda küll nüüd selle pärast, et seal üritame koostada standardteegis olevat listi. Listidega tegeleme juba järgnevates praktikumides.
Täpsemalt kutsutakse siin parameetreid parameetrite pakiks: https://en.cppreference.com/w/cpp/language/parameter_pack.
Lisaks forEach
tsüklile on võimalik kasutada ka varieeruvate argumentide jaoks loodud päist <cstdarg>
. Uuri lähemalt: https://en.cppreference.com/w/cpp/utility/variadic.
NB! Siin peab olema tüüpidega hästi ettevaatlik!
Klassimalli defineerimine
Klassimall on kavand klassi genereerimiseks. Klassimallid erinevad funktsioonimallidest selle poolest, et kompilaator ei saa tuletada malliparameetrite tüüpe klassimalli jaoks. Klassimall muudab klassi definitsiooni üldisemaks. Klassimalli definitsioon algab päisega
template <typename tüübiparameeter>
Võtmesõna typename
asemel võib kasutada ka võtmesõna class
, aga tuleks eelistada typename
.
Klassi enda definitsioon on sama nagu varem, ainult tüübi asemel kasutatakse tüübiparameetrit. Tüübiparameetreid võib olla ka mitu. Vaatame näidet klassimallist. Olgu meil klass Paar
, mis hoiab kahte sama tüüpi elementi.
NB! Nii funktsioonimallid kui ka klassimallid koos liikmefunktsioonide definitsioonidega paigutatakse päisefaili. Kompilaator teeb klassist malliisendi iga kasutatava tüübi jaoks ja seetõttu on kompilaatoril vaja klassi liikmete kohta kogu infot.
template <typename T> class Paar{ public: Paar(); Paar(T esimene, T teine); void setEsimene(T esimene); void setTeine(T teine); T getEsimene(); T getTeine(); private: T m_esimene{}; T m_teine {}; };
Näites olevas klassimallis on mallitüübiks T
ja mall sisaldab vaid konstruktorite ja liikmefunktsioonide deklaratsioone. Funktsioonide definitsioonid võivad olla ka klassimalli sees, kuid nii nagu tavalise klassi puhul, kõik klassi sees defineeritud funktsioonid on automaatselt pistikfunktsioonid (inline functions).
Väljaspool klassimalli liikmefunktsioonide defineerimisel on eripära, et liikmefunktsioonid on ise mallid ja algavad päisega template <typename T>
. Funktsiooni tagastustüüp on enne funktsioonimalli skoobimäärangut Paar<T>::
:
template <typename T> void Paar<T>::setEsimene(T esimene) { m_esimene = esimene; }
Klassimall Paar
tervikuna on järgmine (nt failis deklaratsioonid.h
):
template <typename T> class Paar{ public: Paar(); Paar(T esimene, T teine); void setEsimene(T esimene); void setTeine(T teine); T getEsimene(); T getTeine(); private: T m_esimene{}; T m_teine {}; }; template <typename T> Paar<T>::Paar(T esimene, T teine) : m_esimene{esimene}, m_teine{teine}{} template <typename T> void Paar<T>::setEsimene(T esimene) { m_esimene = esimene; } template <typename T> void Paar<T>::setTeine(T teine) { m_teine = teine; } template <typename T> T Paar<T>::getEsimene(){ return m_esimene; } template <typename T> T Paar<T>::getTeine(){ return m_teine; }
Kui klassimall on defineeritud, siis saab klassist teha objekte. Objekti loomisel peab ette andma tüübi:
Paar<double> paar{};
Testime klassimallist objektide loomist funktsioonis main
:
#include <iostream> #include <string> #include "deklaratsioonid.h" using namespace std; int main() { Paar<int> täisarvupaar{2, 5}; täisarvupaar.setEsimene(3); täisarvupaar.setTeine(6); cout << "Paar: (" << täisarvupaar.getEsimene() << ", " << täisarvupaar.getTeine() << ")\n"; Paar<string> nimepaar{"Madli", "Madis"}; cout << "Paar: (" << nimepaar.getEsimene() << ", " << nimepaar.getTeine() << ")\n"; return 0; } | Paar: (3, 6) Paar: (Madli, Madis) |
Enesetestid