Arvutiteaduse instituut
  1. Kursused
  2. 2023/24 kevad
  3. Programmeerimine keeles C++ (LTAT.03.025)
EN
Logi sisse

Programmeerimine keeles C++ 2023/24 kevad

  • Pealeht
  • 1. Muutujad ja andmetüübid
  • 2. Keele põhikonstruktsioonid I
  • 3. Keele põhikonstruktsioonid II
  • 4. Klass, struktuur, mallid

4 Klass, struktuur, mallid

4.1 Kodutöö
4.2 Harjutused
4.3 Videolingid
  • 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
  • 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

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

1. Klassid6. C++20 päis <concepts>11. Klassimalli defineerimine
2. Konstruktorid7. Mitu tüübiparameetrit 
3. Struktuurid8. Fikseeritud tüübiparameetrid 
4. Funktsioonimallid9. Tüüpide vaikeväärtused 
5. Funktsioonimallide kasutamine10. Varieeruvate parameetrite arvuga mallid 
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

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

<< Näita enesetesti >>

  • Arvutiteaduse instituut
  • Loodus- ja täppisteaduste valdkond
  • Tartu Ülikool
Tehniliste probleemide või küsimuste korral kirjuta:

Kursuse sisu ja korralduslike küsimustega pöörduge kursuse korraldajate poole.
Õppematerjalide varalised autoriõigused kuuluvad Tartu Ülikoolile. Õppematerjalide kasutamine on lubatud autoriõiguse seaduses ettenähtud teose vaba kasutamise eesmärkidel ja tingimustel. Õppematerjalide kasutamisel on kasutaja kohustatud viitama õppematerjalide autorile.
Õppematerjalide kasutamine muudel eesmärkidel on lubatud ainult Tartu Ülikooli eelneval kirjalikul nõusolekul.
Courses’i keskkonna kasutustingimused