Täiendavad teemad
Pärast selle praktikumi läbimist üliõpilane
- oskab luua lambda avaldisi ja funktsioone (lambda-function)
- oskab lambda funktsioone rakendada töös andmekogumitega
- teab mõningaid STL algoritme ja oskab neid koos lambda-funktsioonidega kasutada
- teab moodulite kasutamise eeliseid
- oskab luua mooduleid ja neid kasutada
Sisukord
1. Lambda avaldis (funktsioon) | 4. Mallide defineerimine moodulites |
2. STL konteineri algoritmidest | 5. Alammoodulitest |
3. Moodulitest | Lõpetuseks |
Enesetestid |
Lambda avaldis (funktsioon)
Lambda-funktsioonid on mehhanism anonüümsete funktsioonide koostamiseks. Lambda-funktsiooni signatuur on järgmine (paralleelselt lisatud ka ingliskeelse tekstiga):
[kaasatud muutujate loetelu](parameetrid)->tagastustüüp{//funktsiooni keha}; [capture list](parameters)->return_type{//function body};
Näide lihtsast lambda-funktsioonist, kus ei ole parameetreid ega ka kaasatud muutujate loetelu. Paneme tähele, et funktsiooni keha järel on funktsiooni poole pöördumine ()
ning seejärel semikoolon ;
.
#include <iostream> using namespace std; int main() { [](){ cout << "Tere, maailm!" << '\n'; }(); return 0; } | Tere, maailm! |
Lambda-funktsioonile saab ette anda parameetreid ja nende väärtusi (argumente). Järgmises näites anname lambda-funktsioonile ette kaks parameetrit:
[](float a, float b){ //kaks parameetrit cout << "a + b = " << a + b << '\n'; }(2.5, 4.3); // argumendid | a + b = 6.8 |
Lambda funktsioonile võib anda nime ja pöörduda tema poole nime järgi. Samuti võib ta tagastada andmeid ja olla avaldise osa.
auto fun { [](float a, float b)->float{ // võib ka float fun return (a + b); }(2.5, 4.3) }; cout << fun << '\n'; // lambda-funktsiooni poole pöördumine cout << [](float a, float b)->float{return (a + b);}(2.5, 4.3) << '\n'; // lambda-funktsioon avaldises auto tul = 3.2 + [](float a, float b)->float{return (a + b);}(2.5, 4.3); // lambda-funktsioon avaldises auto tul1 = 3.2 + fun; cout << tul << '\n' << tul1 << '\n'; | 6.8 6.8 10 10 |
Kui defineerime argumentideta lambda-funktsiooni, siis pöördumisel tuleb argumendid ette anda:
auto fun { [](float a, float b)->float{ return (a + b); } }; // argumente ei ole cout << fun(2.5, 4.3) << '\n'; // argumendid pöördumisel auto tul1 = 3.2 + fun(2.5, 4.3); // argumendid pöördumisel cout << tul1 << '\n'; | 6.8 10 |
Muutujaid saab kaasata (capture) nii väärtuse kui ka viite (&) abil. Järgmistes näidetes jätame lihtsuse mõttes parameetrid ära. Muutujate kaasamisel väärtuse järgi kopeeritakse väärtus lambda funktsiooni loomisel ja edaspidi kasutatakse ainult seda väärtust:
int c = 10; auto fun { [c]() { // muutuja c väärtus kopeeritakse cout << "Funktsioonis: " << c << '\n'; }}; for (int i = 0; i < 3; ++i) { cout << "Väljaspool: " << c << '\n'; fun(); // lambda-funktsiooni poole pöördumine ++c; } | Väljaspool: 10 Funktsioonis: 10 Väljaspool: 11 Funktsioonis: 10 Väljaspool: 12 Funktsioonis: 10 |
Kui kaasame muutuja viite (&) abil, siis muutuja väärtuse muutmine programmis mõjub ka lambda-funktsioonis.
int c = 10; auto fun { [&c]() { // muutuja c väärtus viitena cout << "Funktsioonis: " << c << '\n'; }}; for (int i = 0; i < 3; ++i) { cout << "Väljaspool: " << c << '\n'; fun(); // lambda-funktsiooni poole pöördumine ++c; } | Väljaspool: 10 Funktsioonis: 10 Väljaspool: 11 Funktsioonis: 11 Väljaspool: 12 Funktsioonis: 12 |
Kandilistes sulgudes saab näidata, et kõiki väljaspool asuvaid muutujaid kaasatakse kopeerimise teel. Siis on kandilistes sulgudes =
.
Näites on kaasatud kopeerimise teel muutujad c
ja d
. Neid muudetakse väljaspool lambda-funktsiooni, kuid funktsiooni sees need ei muutu.
int c{10}; int d{20}; auto fun { [=]() { // kaasatakse kõik kopeerimise teel cout << "Funktsioonis: c = " << c << " d = " << d << '\n’; }}; for (int i{0}; i < 3; ++i) { cout << "Väljaspool: c = " << c << " d = " << d << '\n'; fun(); ++c; --d; } | Väljaspool: c = 10 d = 20 Funktsioonis: c = 10 d = 20 Väljaspool: c = 11 d = 19 Funktsioonis: c = 10 d = 20 Väljaspool: c = 12 d = 18 Funktsioonis: c = 10 d = 20 |
Kui kandilistes sulgudes on & (ampersand), siis kõiki väljaspool asuvaid muutujaid kaasatakse viite (reference) teel.
Siin kaasatakse muutujad c
ja d
, mis muutuvad funktsiooni sees kaasa.
int c{10}; int d{20}; auto fun { [&]() { // kaasatakse kõik viite (reference) abil cout << "Funktsioonis: c = " << c << " d = " << d << '\n’; }}; for (int i{0}; i < 3; ++i) { cout << "Väljaspool: c = " << c << " d = " << d << '\n'; fun(); ++c; --d; } | Väljaspool: c = 10 d = 20 Funktsioonis: c = 10 d = 20 Väljaspool: c = 11 d = 19 Funktsioonis: c = 11 d = 19 Väljaspool: c = 12 d = 18 Funktsioonis: c = 12 d = 18 |
Lambda-funktsioonis võib kasutada ka malliparameetreid. Näites on malliparameetriks vektori elemendi tüüp ja lambda-funktsioon leiab vektori elementide summa.
Defineerime double
tüüpi elementidega vektori dv
. Näites on cout
käsus lambda-funktsioon koos pöördumisega.
vector <double> dv{1.5, 2.5, 3.5}; cout << []<typename T>(vector<T> const& v) { T summa{0}; for (T el : v){ summa += el; } return summa;}(dv) << '\n' | 7.5 |
Väga mugav on kasutada lambda-funktsioone andmekogumite sortimisel. STL päises <algorithm> on palju valmisprogrammeeritud algoritme. Konkreetselt siin kasutame funktsiooni ranges::sort
, kus esimeseks argumendiks on andmekogum ja teiseks sortimiskriteerium, mida on mugav esitada lambda-funktsioonina. Siin on sõnede võrdlemine, võrdleme sõnesid märkide arvu järgi, st kogu vektor saab sorditud sõnede pikkuste järgi.
vector<string> lause{"Mäkradel", "läheb", "Eestis", "hästi", "ja", "see", "ajab", "neid", "seatempe", "tegema"}; ranges::sort (lause, [](const string& a, const string& b){ return a.length() < b.length(); }); for (string s: lause){ cout << s << " "; } | ja see ajab neid läheb Eestis hästi tegema seatempe Mäkradel |
Lambda-funktsioonidest saab lähemalt uurida järgmisel aadressil https://en.cppreference.com/w/cpp/language/lambda
STL konteineri algoritmidest
Lambda-funktsioone on mugav kasutada nii massiivide kui ka STL konteinerite korral. STL konteineri päised <algorithm>
ja <numeric>
sisaldavad üle 100 erineva algoritmi, mis lihtsustavad tööd andmekogumitega. Siinkohal vaatleme neist mõnda. Vaatame näidet algoritmiga all_of
. Esimesed kaks parameetrit määravad andmekogumil algus- ja lõpuiteraatori (st elemendid, mille peale algoritmi rakendada); kolmas parameeter on predikaat, mida rakendatakse kõikide eelnevalt määratud elementide peale. Algoritm all_of
tagastab true
, kui predikaat tagastab true
kõikide kontrollitud elementide korral. Predikaadiks on antud juhul lambda-funktsioon, mis tagastab true
, kui arv on paarisarv.
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v1{2, 8, 12, 6, 14}; if (all_of(v1.begin(), v1.end(), [](int i){return i%2 == 0;})){ // kolmas argument on lambda-funktsioon cout << "Vektoris v1 on kõik paarisarvud\n"; } else{ cout << "Vektoris v1 on ka paarituid arve\n"; } return 0; } | Vektoris v1 on kõik paarisarvud |
Märgime siinjuures, et predikaadiks sobib ka tavaline funktsioon, mis tagastab sobiva tõeväärtuse:
#include <iostream> #include <vector> #include <algorithm> using namespace std; bool kas_paaris(int n){ return (n % 2 == 0); } int main() { vector<int> v2{2, 8, 12, 6, 14, 1}; if (all_of(v2.begin(), v2.end(), kas_paaris)){ // predikaadiks on funktsioon kas_paaris cout << "Vektoris v2 on kõik paarisarvud\n"; } else{ cout << "Vektoris v2 on ka paarituid arve\n"; } return 0; } | Vektoris v2 on ka paarituid arve |
Algoritm for_each
rakendab kolmanda parameetrina antud funktsiooni kõigil elementidel, mis on määratud kahe esimese parameetriga:
#include <iostream> #include <set> #include <algorithm> using namespace std; int main() { set<int> s1{2, 53, 41, 334}; int sum{0}; for_each(s1.begin(), s1.end(), [&sum](int n){sum += n;}); // sum antakse edasi viitena cout << "Elementide summa = " << sum << '\n'; return 0; } | Elementide summa = 430 |
Lambda-funktsioonil on üks parameeter, milleks on täisarv. Funktsiooni täidetakse hulga kõikide elementide korral.
Andmekogumite korral läheb tihti vaja sortimist. Esimese näitena vaatame vektori elementide sortimist. Täisarvud on võrreldavad, seega pole vaja võrdlemiseeskirja ette anda.
vector<int> v{12, 8, 12, 6, 14, 1, 3, 4}; sort(v.begin(), v.end()); // STL sort algoritm for_each(v.begin(), v.end(), [](int n){cout << n << " ";}); // for_each elementide kuvamiseks ekraanile | 1 3 4 6 8 12 12 14 |
Teise näitena vaatame objektide sortimist algoritmi sort
abil. Kompaktsuse huvides on kogu kood ühes failis. Klassis Raamat
on väljad aasta ja pealkirja jaoks. Raamatuid võrreldakse aasta järgi.
#include <iostream> #include <ostream> #include <vector> #include <algorithm> #include <string> using namespace std; class Raamat{ int m_aasta{}; string m_pealkiri{}; public: Raamat() = default; Raamat(int aasta, string pealkiri) : m_aasta{aasta}, m_pealkiri{pealkiri}{} int getAasta(){ return m_aasta; } friend ostream& operator<<(ostream& os, Raamat r){ os << r.m_pealkiri << " " << r.m_aasta; return os; } }; int main() { // Raamatute vektor vector<Raamat> raamatud {Raamat{2021, "Füüsika õhtuõpik"}, Raamat{2022, "Mikside raamat"}, Raamat{2015, "Teatrielu"}}; // lambda-funktsioon raamatute võrdlemiseks auto vrdl = [](Raamat r1, Raamat r2){ return (r1.getAasta() < r2.getAasta()); }; // raamatute kuvamine for_each abil, kasutab operaatorit << for_each(raamatud.begin(), raamatud.end(), [](Raamat r){cout << r << '\n';}); // raamatute sortimine lambda-funktsiooni vrdl abil sort(raamatud.begin(), raamatud.end(), vrdl); cout << "=========\n"; // raamatute kuvamine for_each abil for_each(raamatud.begin(), raamatud.end(), [](Raamat r){cout << r << '\n';}); return 0; } | Füüsika õhtuõpik 2021 Mikside raamat 2022 Teatrielu 2015 ========= Teatrielu 2015 Füüsika õhtuõpik 2021 Mikside raamat 2022 |
Kombineerides lambda-funktsioone ja kasutades päist <ranges>
ja nn "torusümbolit" (pipe symbol) |
, on võimalik andmekogumite sisu filtreerida ja teisendada:
#include <iostream> #include <vector> #include <ranges> using namespace std; int main() { vector<int> täisarvud{0, 1, 2, 3, 4, 5, 6, 7}; auto paaritu = [](int i) { return i % 2 != 0; }; auto ruut = [](int i) { return i * i; }; for (int i: täisarvud | views::filter(paaritu) | views::transform(ruut)) { cout << i << ' '; } return 0; } | 1 9 25 49 |
Lambda-funktsioon paaritu
annab tulemuseks true
, kui argument on paaritu arv. Lambda-funktsioon ruut
tagastab argumendi ruudu. for
tsükkel demonstreerib lambda-funktsioonide kombineerimist, mida tuleb lugeda vasakult paremale: for (int i: täisarvud | views::filter(paaritu) | views::transform(ruut))
. Rakendame vektori täisarvud
igale elemendile filtrit paaritu
ja filtrit läbinud elementidele funktsiooni ruut
. Tulemuseks kuvatakse paaritute arvude ruudud.
STL algoritmide kohta saab uurida aadressil
https://en.cppreference.com/w/cpp/algorithm
Moodulitest
Alates C++20 on võimalik kasutada mooduleid.
Kompilaatorid toetavad mooduleid erineval tasemel, vt
https://en.cppreference.com/w/cpp/compiler_support
Selle peatüki näidetes on kasutatud arenduskeskkonda MS Visual Studio versiooni 17.9.6 ja terminaliakent.
Vaatame lihtsat näidet moodulist minu_math_moodul
:
fail math.ixx
:
export module minu_math_moodul; export int liida(int a, int b) { return a + b; } export int korruta(int a, int b) { return a * b; }
Lause export module minu_math_moodul;
deklareerib mooduli nimega minu_math_moodul
. Funktsiooni ees olev export
ekspordib funktsiooni. Mooduli kasutamine funktsioonis main
:
#include <iostream> import minu_math_moodul; using namespace std; int main(){ int summa = liida(5, 6); int korrutis = korruta(5, 6); cout << summa << '\n'; cout << korrutis << '\n'; return 0; }
Lause import minu_math_moodul;
impordib mooduli ja teeb selles eksporditud funktsioonid kättesaadavaks. Tulemuseks on
Milleks on mooduleid vaja?
Olgu meil lihtne programm failis TereMaailm.cpp
:
#include <iostream> int main() { std::cout << "Tere, maailm!\n"; return 0; }
Jooniselt
näeme, et lähteteksti suurus on 90 baiti. Kompileerides kompilaatoriga GCC
saame käivitatava faili suuruseks 118044 baiti. Peale käivitamist kuvatakse tekst "Tere, maailm!".
Klassikaline käivitatava faili loomise protsess koosneb kolmest etapist: eeltöötlusest preprotsessori poolt, kompileerimisest ja linkimisest.
Eeltöötlus
Eeltöötluse ajal töödeldakse preprotsessori käske, nagu näiteks #include
ja #define
. Preprotsessor asendab #include
käsud vastavate päisefailidega ja #define
asendab makrod. Seda asendamist juhitakse käskudega #if, #else, #ifdef, #ifndef,...
, mistõttu osa koodist kaasatakse ja osa mitte.
Seda asendusprotsessi saab jälgida kompilaatorite GCC/Clang
korral lipuga -E
.
Preprotsessori väljundi suurus on muljet avaldav. Preprotsessori tulemuseks on transleerimisühik.
Kompileerimine
Igat preprotsessori väljundit kompileeritakse eraldi. Kompilaator genereerib tulemused objektkoodi failideks (.o
). Objektkoodi fail võib viidata deklareeritud nimedele (nt funktsioonidele), millel puudub definitsioon. Objektkoodi failid on sisendiks linkijale.
Linkimine
Linkija tulemuseks on kas täidetav fail või teek. Linkija ülesandeks on otsida nimedele vastavaid definitsioone.
Käivitatava faili loomise protsess töötab hästi, kui meil on ainult üks transleerimisühik. Kui transleerimisühikuid on palju, siis võib tekkida erinevaid probleeme.
- Korduv asendamine
Kui päisefailid kaasavad samu teeke, nt #include <iostream>
, siis võib juhtuda, et sama teek kaasatakse mitu korda, sest preprotsessor töötleb igat lähtefaili eraldi. See suurendab oluliselt lähtefaile ja pikendab kompileerimisaega.
Erinevalt päisefailidest, moodul kaasatakse ainult üks kord.
- Korduv (funktsioonide) defineerimine
ODR (One Definition Rule) korral https://en.cppreference.com/w/cpp/language/definition
- Klassidel/struktuuridel ja funktsioonidel saab olla ainult üks definitsioon terves programmis.
- Mallidel ja tüüpidel saab olla ainult üks definitsioon transleerimisühikus.
Olgu meil esimeses päisefailis header1.h
funktsiooni fun
definitsioon
void fun(){}
ja teises päisefailis header2.h
kaasatakse esimene
#include "header1.h"
Failis main.cpp
kaasatakse mõlemad:
#include "header1.h" #include "header2.h" int main(){}
Kui üritame käivitatavat faili teha, siis linkija kaebab funktsiooni fun
uuesti defineerimise pärast:
Sellest üle saamiseks kasutasime seni käske #ifndef
ja #define
.
Moodulite korral on korduvate nimede tekkimine vähetõenäoline.
Võtame kokku moodulite eelised:
- Mooduleid imporditakse ainult üks kord.
- Moodulite importimise järjekord ei ole oluline.
- Korduvate nimede tekkimine on vähetõenäoline.
- Moodulid võimaldavad parandada programmi loogilist struktuuri.
- Kiireneb kompileerimisaeg.
Moodulifaili laiend sõltub kompilaatorist:
- MS kompilaator kasutab laiendit
.ixx
- Clang kompilaator kasutab laiendit
.cppm
- GCC kompilaatoril ei ole mooduli jaoks spetsiaalset laiendit
NB! Mooduli faili nimel ei ole seost mooduli nimega.
Moodulist saab nimesid eksportida kolmel viisil:
1. Iga üksus eraldi:
fail math.ixx
export module minu_math_moodul; export int liida(int a, int b); export int korruta(int a, int b);
2. Eksport rühma kaupa:
fail math.ixx
export module minu_math_moodul; export{ int liida(int a, int b); int korruta(int a, int b); }
3. Nimeruumi eksport:
fail math.ixx
export module minu_math_moodul; export namespace minu_math_moodul{ int liida(int a, int b); int korruta(int a, int b); }
Näidetes eksporditakse ainult funktsioonide deklaratsioone. Kui failis on ainult deklaratsioonid, siis nimetatakse seda faili mooduli liideseks. Funktsioonide definitsioonid võivad olla tavalises lähtekoodifailis, mille esimeseks käsuks on module <mooduli nimi>;
. Paneme nimeruumi ekspordi korral definitsioonid faili math_moodul.cpp
:
module minu_math_moodul; namespace minu_math_moodul { int liida(int a, int b) { return a + b; } int korruta(int a, int b) { return a * b; } }
Nimeruumi abil eksporditud funktsioonide kasutamisel funktsioonis main
impordime mooduli ja funktsioonide ees kasutame nimeruumi koos skoobioperaatoriga minu_math_moodul::
.
#include <iostream> import minu_math_moodul; using namespace std; int main(){ int summa = minu_math_moodul::liida(5, 6); int korrutis = minu_math_moodul::korruta(5, 6); cout << summa << '\n'; cout << korrutis << '\n'; return 0; }
Tulemuseks on samuti
Moodulis võib olla ka komponente, mida ei ekspordita, kuid mis on vajalikud mooduli eksporditavate komponentide tööks. Mooduli struktuur võib olla keerulisem. Moodulid võivad moodustada hierarhia.
module; // globaalne mooduli osa, võib puududa #include <päised või teegid, mis ei ole veel mooduli kujul> export module minu_math_moodul; // mooduli deklaratsioon import <teiste moodulite importimine> // eksporditavad ja mitteeksporditavad deklaratsioonid
Lisame oma moodulisse funktsiooni kuva
, mida ei ekspordita, kuid mida kasutavad eksporditavad funktsioonid liida
ja korruta
.
Fail math.ixx
module; export module minu_math_moodul; import <iostream>; import <string>; export int liida(int a, int b); export int korruta(int a, int b); void kuva(int a, int b);
Fail on mooduli liides, siin on ainult funktsioonide deklaratsioonid. Failis on käsud import <iostream>;
ja import <string>;
, kuna neid vajab funktsioon kuva
.
Failis math_moodul.cpp
on mooduli funktsioonide definitsioonid.
module; module minu_math_moodul; int liida(int a, int b) { kuva(a, b); return a + b; } int korruta(int a, int b) { kuva(a, b); return a * b; } void kuva(int a, int b) { std::cout << "a = " << a << " b = " << b << '\n'; }
Failis main.cpp
me ei saa pöörduda otse funktsiooni kuva
poole, sest seda moodul minu_math_moodul
ei ekspordi.
#include <iostream> import minu_math_moodul; using namespace std; int main(){ int summa = liida(5, 6); cout << summa << '\n'; int korrutis = korruta(5, 6); cout << korrutis << '\n'; return 0; }
Programmi käivitamisel saame:
Mallide defineerimine moodulites
Moodulites saab ja tuleb kasutada malle. Üldistame eelmist näidet kasutades funktsioonimalle.
Fail math_template.ixx
:
module; export module math_template; import <iostream>; import <string>; export{ template <typename T> void kuva(T a, T b); template <typename T> T liida(T a, T b) { kuva(a, b); return a + b; } template <typename T> T korruta(T a, T b) { kuva<T>(a, b); return a * b; } } template <typename T> void kuva(T a, T b) { std::cout << "a = " << a << " b = " << b << '\n'; }
Kuna funktsioonimallid liida
ja korruta
kasutavad funktsioonimalli kuva
, siis tuleb see enne deklareerida:
template <typename T> void kuva(T a, T b);
Fail main.cpp
:
#include <iostream> import math_template; using namespace std; int main(){ auto summa = liida<int>(5, 6); cout << "summa = " << summa << '\n'; auto korrutis = korruta<double>(5.5, 6.2); cout << "korrutis = " << korrutis << '\n'; return 0; }
Funktsioonis main
kasutame malliparameetrit funktsioonide liida
ja korruta
poole pöördumiseks.
Programmi käivitamisel saame:
Alammoodulitest
Moodulil saab olla alammooduleid. Toome siin lihtsa näite mooduliga math
. Alammoodulid math.liida
ja math.korruta
sisaldavad mõlemad ühe eksporditava funktsiooni, vastavalt liida
ja korruta
.
Fail math_liida.ixx
:
export module math.liida; export int liida(int a, int b) { return a + b; }
Fail math_korruta.ixx
:
export module math.korruta; export int korruta(int a, int b) { return a * b; }
Ülemmoodulis on käsud alammoodulite importimiseks ja eksportimiseks. Lisame veel eksporditava funktsiooni kuva
.
Fail math.ixx
:
export module math; import <iostream>; import <string>; export import math.liida; export import math.korruta; export void kuva(std::string s) { std::cout << s << '\n'; }
Põhiprogrammis impordime esialgu ülemmooduli. Sellisel juhul on kättesaadavad funktsioonid liida
, korruta
ja kuva
.
Fail main.cpp
:
#include <iostream> import math; using namespace std; int main(){ cout << liida(5, 10) << '\n'; cout << korruta(5, 10) << '\n'; kuva("Moodul math"); }
Tulemuseks saame:
Importida saame ka ainult alammooduleid. Impordime moodulid math.liida
ja math.korruta
. Kuna ülemmoodulit ei ole eksporditud, siis funktsiooni kuva
me kasutada ei saa.
#include <iostream> import math.liida; import math.korruta; using namespace std; int main(){ cout << liida(5, 10) << '\n'; cout << korruta(5, 10) << '\n'; // kuva("Moodul math"); }
Tulemuseks saame:
Täpsemalt saab moodulite kohta uurida aadressil https://en.cppreference.com/w/cpp/language/modules
Veel kord kopeerimisest ja teisaldamisest
Objektide kopeerimisest ja teisaldamisest oli juttu 11. nädalal. Kes tunneb end kindlalt, võib selle peatüki vahele jätta.
Loome klassi Isik
, milles on üks väli nime jaoks. Klassis Raamat
on isendiväljad aasta ja pealkirja jaoks. Lisaks on veel üks väli, milleks on autor, st viit (pointer) isikule. Funktsioonis main
loome ühe raamatu. Teise raamatu loome käsuga Raamat r2{r1}
, kus toimub pöördumine C++ kompilaatori poolt tehtud koopiakonstruktori poole. Kuvame ekraanile mõlema objekti aadressi ja mõlema autori aadressi.
#include <iostream> #include <ostream> #include <string> using namespace std; class Isik{ string m_nimi{}; public: Isik() = default; Isik(string nimi) : m_nimi{nimi}{} string getNimi(){ return m_nimi; } string to_String(){ return m_nimi; } }; class Raamat{ int m_aasta{}; string m_pealkiri{}; Isik* m_autor{}; public: Raamat() = default; Raamat(int aasta, string pealkiri, Isik* autor) : m_aasta{aasta}, m_pealkiri{pealkiri}, m_autor{autor}{} int getAasta(){ return m_aasta; } string getPealkiri(){ return m_pealkiri; } Isik* getAutor(){ return m_autor; } friend ostream& operator<<(ostream& os, Raamat r){ os << r.m_pealkiri << " " << r.m_aasta << " " << r.getAutor()->to_String(); return os; } }; int main() { Isik* autor1 = new Isik{"Jüri Liiv"}; Raamat r1{2021, "Füüsika õhtuõpik", autor1}; Raamat r2{r1}; //koopiakonstruktori poole pöördumine cout << "r1 aadress: " << &r1 << " " << r1 << " " << r1.getAutor() << '\n'; cout << "r2 aadress: " << &r2 << " " << r2 << " " << r2.getAutor() << '\n'; delete autor1; return 0; } | r1 aadress: 0xee8bff860 Füüsika õhtuõpik 2021 Jüri Liiv 0x1b8dd6618f0 r2 aadress: 0xee8bff830 Füüsika õhtuõpik 2021 Jüri Liiv 0x1b8dd6618f0 |
Näeme, et raamatute aadressid on erinevad, aga viit autorile on sama. See tähendab, et vaikimisi koopiakonstruktor teeb objektist lameda (madala) (shallow) koopia. Lameda koopia korral kopeeritakse objekt bait-baidilt, sealhulgas ka viidad. Seega mõlemad koopiad viitavad samale Isik
objektile. Selleks, et teha täielikku (sügavat) koopiat (deep copy), tuleb üle defineerida koopiakonstruktor. Koopiakonstruktorile tuleb objekt ette anda viitena (&
). Lisame ka ekraanile kuvamise, et täidetakse koopiakonstruktorit.
Raamat(const Raamat& r) : m_aasta{r.getAasta()}, m_pealkiri{r.getPealkiri()}, m_autor{new Isik(*(r.getAutor()))} { cout << "Koopiakonstruktoris " << m_pealkiri << '\n'; }
Nüüd on programmi väljundiks
Koopiakonstruktoris Füüsika õhtuõpik r1 aadress: 0x6d03bff680 0x17109c518f0 r2 aadress: 0x6d03bff650 0x17109c51a90
Näeme, et ka autorist on tehtud koopia, st et on tegemist täieliku koopiaga (deep copy).
Koopiakonstruktor kutsutakse välja enamasti järgmistel juhtudel:
- kui objekt tagastatakse väärtusena
- kui objekt edastatakse (funktsioonile) väärtusena
- kui objekt luuakse teise objekti pealt
Koopiakonstruktori korral tehakse objektist koopia ja vana objekt jääb alles. Teisalduskonstruktori korral tehakse uus objekt ja vana objekt muudetakse kehtetuks.
Raamat(Raamat&& r); //teisalduskonstruktor
Teisalduskonstruktori parameetriks on siin Raamat&&
, kus &&
tähistab rvalue reference
. Mõiste rvalue
on täiendiks mõistele lvalue
, mis laias laastus tähendab "miski, mis saab olla omistamise vasakul poolel". Ehk siis (esimeses lähenduses), rvalue
on miski, mida ei saa muutujale omistada ja me saame "kaaperdada" tema väärtuse. Täidame konstruktori sisuga:
// teisalduskonstruktor Raamat(Raamat&& r) : m_aasta{r.m_aasta}, m_pealkiri{r.m_pealkiri}, m_autor{r.m_autor}{ r.m_aasta = 0; r.m_pealkiri = ""; r.m_autor = nullptr; cout << "Teisalduskonstruktoris " << m_pealkiri << '\n'; }
Teisalduskonstruktorit (kui see on defineeritud) kasutab vector
elementide lisamisel.
int main() { vector<Raamat> raamatud{}; Isik* autor1 = new Isik{"Jüri Liiv"}; raamatud.push_back(Raamat{2021, "Füüsika õhtuõpik", autor1}); cout << "\n"; for (Raamat& r: raamatud) { cout << r << '\n'; } delete autor1; return 0; } | Teisalduskonstruktoris Füüsika õhtuõpik Füüsika õhtuõpik 2021 Jüri Liiv |
Kui lisame vektorisse mitu raamatut, siis mõnikord pöördutakse teisalduskonstruktori poole ka juba lisatud raamatute korral. Põhjus on selles, et vektori elementide jaoks võetakse ruumi juurde tükkide kaupa. Vektori mahutavuse saame funktsiooni capacity
abil. Kui elemendi lisamisel vektorisse on mahtuvus täis, siis võetakse ruumi juurde ja elemendid tõstetakse ringi. Järgmises näites kuvatakse enne elemendi lisamist vektorisse vektori mahutavus.
int main() { vector<Raamat> raamatud{}; Isik* autor1 = new Isik{"Jüri Liiv"}; Isik* autor2 = new Isik{"J.K. Rowling"}; cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2021, "Füüsika õhtuõpik", autor1}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{1997, "Harry Potter ja tarkade kivi", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{1998, "Harry Potter ja saladuste kamber", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{1999, "Harry Potter ja Azbakani vang", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2000, "Harry Potter ja tulepeeker", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2003, "Harry Potter ja Fööniksi ordu", autor2}); cout << "raamatud.capacity() = " << raamatud.capacity() << '\n'; raamatud.push_back(Raamat{2005, "Harry Potter ja segavereline prints", autor2}); cout << "\n"; for (Raamat& r: raamatud) { cout << r << '\n'; } delete autor1; delete autor2; return 0; } | raamatud.capacity() = 0 Teisalduskonstruktoris Füüsika õhtuõpik raamatud.capacity() = 1 Teisalduskonstruktoris Harry Potter ja tarkade kivi Teisalduskonstruktoris Füüsika õhtuõpik raamatud.capacity() = 2 Teisalduskonstruktoris Harry Potter ja saladuste kamber Teisalduskonstruktoris Füüsika õhtuõpik Teisalduskonstruktoris Harry Potter ja tarkade kivi raamatud.capacity() = 4 Teisalduskonstruktoris Harry Potter ja Azbakani vang raamatud.capacity() = 4 Teisalduskonstruktoris Harry Potter ja tulepeeker Teisalduskonstruktoris Füüsika õhtuõpik Teisalduskonstruktoris Harry Potter ja tarkade kivi Teisalduskonstruktoris Harry Potter ja saladuste kamber Teisalduskonstruktoris Harry Potter ja Azbakani vang raamatud.capacity() = 8 Teisalduskonstruktoris Harry Potter ja Fööniksi ordu raamatud.capacity() = 8 Teisalduskonstruktoris Harry Potter ja segavereline prints Füüsika õhtuõpik 2021 Jüri Liiv Harry Potter ja tarkade kivi 1997 J.K. Rowling Harry Potter ja saladuste kamber 1998 J.K. Rowling Harry Potter ja Azbakani vang 1999 J.K. Rowling Harry Potter ja tulepeeker 2000 J.K. Rowling Harry Potter ja Fööniksi ordu 2003 J.K. Rowling Harry Potter ja segavereline prints 2005 J.K. Rowling |
Lõpetuseks
Olemegi jõudnud kursuse lõppu. Kahjuks jäid seekord kursusel tähelepanuta (või väga põgusa tähelepanuga) mitmed olulised teemad. Püüa nendega vajadusel ise tutvust teha. Nimetame siin mõnda:
- ranges
https://en.cppreference.com/w/cpp/ranges
- coroutines
https://en.cppreference.com/w/cpp/language/coroutines
- threads (lõimed)
https://en.cppreference.com/w/cpp/thread/thread/thread
- ...
Mõnusat programmeerimist keeles C++!
Enesetestid
NB! Enesetestides eeldame, et on kasutatud standardnimeruumi (using namespace std;
)