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
  • 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
14 Täiendavad teemad
14.1 Kodutöö
14.2 Harjutused
14.3 Videolingid
  • 15. Kontrolltöö 2

Viieteistkümnendal nädalal toimub 2. kontrolltöö

  • 16. Projekti esitlus
  • Mõned viited - vajalikud kaaslased
  • Vanad materjalid
  • Juhendid
  • Viited

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 algoritmidest5. Alammoodulitest
3. MoodulitestLõ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;)

<< 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