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
5 Dünaamiline mäluhaldus I
5.1 Kodutöö
5.2 Harjutused
5.3 Videolingid
  • 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

Dünaamiline mäluhaldus I

Pärast selle praktikumi läbimist üliõpilane

  • teab viida (pointer) mõistet ja oskab seda kasutada funktsioonile edastamisel ja funktsioonist tagastamisel
  • teab viite (reference) mõistet ja oskab seda edastada funktsioonile ja funktsioonist tagastada
  • oskab kasutada massiive koos viitadega ja edastada massiivi funktsioonile
  • oskab üle defineerida operaatoreid

Sisukord

1. Viidad (pointers)7. Kokkuvõte argumentide edastamisest funktsioonile
2. Viitade (pointers) aritmeetika8. Viite (reference) ja viida (pointer) tagastamine funktsioonist
3. Viited (references)9. Viit konstandile ja konstantne viit
4. Argumentide edastamine funktsioonile10. Viidad ja massiivid
5. Väärtuse järgi (pass by value)11. Massiivi edastamine funktsioonile
6. Viida (viite) järgi (pass by reference)12. Operaatorite üledefineerimine
Enesetestid

Viidad (pointers)

Viit on muutuja, mis hoiab mäluaadressi. See aadress on mingi objekti (tavaliselt teise muutuja) asukoht mälus. Näiteks, kui muutuja sisaldab teise muutuja aadressi, siis öeldakse, et esimene muutuja viitab teisele. Järgmine joonis illustreerib seda olukorda.

 

Viidast pole palju kasu, kui me ei tea, mis tüüpi andmed viidataval aadressil asuvad. Seega iga viidamuutuja viitab aadressile, kus on kindlat tüüpi andmed. Viidamuutuja defineeritakse * abil:

tüüp* nimi;

tüüp on suvaline andmetüüp ja näitab, mis tüüpi andmetele viidamuutuja saab viidata. nimi on viidamuutuja identifikaator. Näiteks,

long* p;

defineerib viidamuutuja p, mis saab viidata long tüüpi arvule. Viida defineerimisel ei eraldata andmetele mälu. Kui viidale defineerimisel aadressi ei omistata, siis on mõistlik viit initsialiseerida väärtusega nullptr, mis on spetsiaalne väärtus selleks, et viidamuutuja ei viita kuhugi. NB! Tärni võib panna ka tüübi ja muutuja vahele long * p või ka muutuja külge long *p, kuid levinud soovitus on kirjutada tärn vahetult tüübi järele long* p.

long* p{nullptr};

Viidamuutuja nimele pannakse tavaliselt ette prefiks p_. Viitadel on C++ keeles kolm olulist aspekti. Viidad võimaldavad

  • massiivi elementidele kiiret ligipääsu
  • funktsioonidel muuta väljakutse parameetreid
  • kasutada efektiivselt lingitud liste ja teisi dünaamilisi andmestruktuure

Täpsemalt saab uurida siit https://en.cppreference.com/w/cpp/language/lifetime https://en.cppreference.com/w/cpp/language/scope

Unaarne operaator & tagastab operandi mäluaadressi. Seda aadressi saab salvestada vaid sobivat tüüpi viidamuutujasse. Vaatame näidet

long arv{2345L};
long* p_arv{&arv};
cout << "arv: " << arv << "\n";
cout << "&arv: " << &arv << "\n";
cout << "p_arv: " << p_arv << "\n";
arv: 2345
&arv: 0xe2c6dff80c
p_arv: 0xe2c6dff80c

Väljundvoos &arv on muutuja arv mäluaadress. Näites on viidamuutuja nimi p_arv (* on tunnuseks, et tegemist on viidamuutujaga). Viidamuutuja hoiab mäluaadressi. Seda näeme viimasel real, kus trükitakse p_arv sisu.

Kuidas saab viidamuutuja abil viidatavalt mäluaadressilt andmeid?

Siin on abiks operaator *. Seda operaatorit nimetatakse inglise keeles kas dereference või ka indirection operator. Eesti keeles nimetame seda otsendamiseks.

Operaator * otsendamine (dereference)

Operaator * võimaldab juurdepääsu andmetele, millele viidamuutuja viitab. Et saada kätte andmeid, millele viidamuutuja p_arv viitab, kasutame avaldist *p_arv. Täiendame eelmist näidet

long arv{2345L};
long* p_arv{&arv};
cout << "arv: " << arv << "\n";
cout << "&arv: " << &arv << "\n";
cout << "*p_arv: " << *p_arv << "\n";
arv: 2345
&arv: 0xe2c6dff80c
*p_arv: 2345

Operaator * võimaldab ka viidamuutuja poolt viidatavaid andmeid muuta:

double hind;
double* p_hind{&hind};
int mitu;
int* p_mitu{&mitu};
cout << "Sisesta toote hind ja toodete arv:";
cin >> *p_hind >> *p_mitu;
double summa;
auto p_summa{&summa};
*p_summa = *p_hind * *p_mitu;
cout << "Hind kokku: " << *p_summa << "\n";
Sisesta toote hind ja toodete arv:2.345 5
Hind kokku: 11.725

NB! Kahjuks on korrutamismärk ja viidamuutuja tunnus sama märk *. Sama kehtib ka viite & ja bitikaupa korrutamise märgi kohta.

Viitade (pointers) aritmeetika

Kuna viit on alati seotud tüübiga, siis tulemus sõltub viida tüübist.

int a{5};
int* p = &a;
cout << p << '\n'; 
cout << *p << '\n';
cout << p + 1 << '\n';
0xb6f11ffd84
5
0xb6f11ffd88

Näeme, et p ja p + 1 erinevad 4 võrra, mis on baitide arv int tüüpi arvu kujutamiseks. Kui viit (pointer) on double tüüpi, siis viida ühe võrra suurendamisel suureneb aadress 8 võrra, sest double tüüpi arvu esitamiseks läheb vaja 8 baiti.

double a{5.5};
double* p = &a;
cout << p << '\n';
cout << *p << '\n';
cout << p + 1 << '\n';
0xa3f9bff850
5.5
0xa3f9bff858

Järgmises näites on viit unsigned short tüüpi ja viida suurendamisel ühe võrra suureneb aadress kahe võrra.

unsigned short a{5};
unsigned short* p = &a;
cout << p << '\n';
cout << *p << '\n';
cout << p + 1 << '\n';
0xdde65ffdb6
5
0xdde65ffdb8

NB! Mitu baiti antud tüübi arvu esituseks vaja läheb, sõltub kompilaatorist ja arvutist.

Viited (references)

On võimalik defineerida viide (reference), mis on tavaline muutuja. Seda nimetatakse viiteks. Viite loomisel antakse objektile lisaks teine nimi (alias).

Viide (reference) on nimi, mida saab kasutada teise muutuja aliasena. Ta on sarnane viidale (pointer), sest viitab mingile mäluosale, kuid tal on ka olulised erinevused:

  • viidet(reference) ei saa deklareerida ilma initsialiseerimata
  • viidet (reference) ei saa muuta. Kui viide on ühe muutuja aliaseks defineeritud, siis ta jääbki selle muutuja aliaseks.

Viidet defineeritakse kasutades märki & tüübi järel.

int a{10};
int& ref_a{a};

Nüüd muutujad a ja ref_a on teineteise aliased ja nende aadressid on samad. Kui ühte neist muuta, siis muutub ka teine.

int a{10};
int& ref_a{a};
cout << "a: " << a << " ref_a: " << ref_a << "\n";
cout << "&a: " << &a << " &ref_a: " << &ref_a << "\n";
a = 15;
cout << "a: " << a << " &ref_a: " << ref_a << "\n";
cout << "&a: " << &a << " &ref_a: " << &ref_a << "\n";
ref_a = 20;
cout << "a: " << a << " &ref_a: " << ref_a << "\n";
cout << "&a: " << &a << " &ref_a: " << &ref_a << "\n";
a: 10 ref_a: 10
&a: 0xbcfe3ff9f4 &ref_a: 0xbcfe3ff9f4
a: 15 &ref_a: 15
&a: 0xbcfe3ff9f4 &ref_a: 0xbcfe3ff9f4
a: 20 &ref_a: 20
&a: 0xbcfe3ff9f4 &ref_a: 0xbcfe3ff9f4

Muutuja a väärtuse muutmisel muutub ka muutuja ref_a väärtus ja vastupidi. Mõlemal on sama aadress.

NB! Viite loomisel tuleb see alati initsialiseerida ja ta jääb seotuks sama objektiga.

Viitasid ja viiteid käsitletakse detailsemalt edaspidi, ise võib täpsemalt uurida https://en.cppreference.com/w/cpp/language/pointer

Argumentide edastamine funktsioonile

Funktsiooni poole pöördumise argumendid peavad üldjuhul vastama funktsiooni definitsioonis olevatele parameetritele nii tüüpide kui ka järjekorra poolest. Kui funktsioonile edastada argument, mille tüüp ei vasta funktsiooni parameetrile, siis kompilaator püüab teostada teisenduse ühest tüübist teise (nt long -> int). Kui see ei õnnestu, siis kompilaator annab vea.

Argumente edastatakse funktsioonile kahe skeemi järgi

  • väärtuse järgi (pass by value)
  • viite (viida) järgi (pass by reference)

Vaatame mõlemat skeemi lähemalt.

Väärtuse järgi (pass by value)

Sellisel juhul edastatakse funktsioonile argumentide koopiad.

#include <iostream>
using namespace std;

int muuda(int a);

int main() {
    int arv{5};
    int tulemus {muuda(arv)};
    cout << "<arv> <main> funktsioonis peale funktsiooni <muuda>: " << arv << "\n";
    cout << "Tagastati tulemus: " << tulemus << "\n";
    return 0;
}
int muuda(int arv){
    // funktsioon muudab koopiat
    arv *= 3;
    cout << "<arv> funktsioonis <muuda> peale muutmist: " << arv << "\n";
    return arv;
}
<arv> funktsioonis <muuda> peale muutmist: 15
<arv> <main> funktsioonis peale funktsiooni <muuda>: 5
Tagastati tulemus: 15

Funktsiooni main muutuja arv väärtus ei muutunud peale funktsiooni muuda täitmist.

Viida (viite) järgi (pass by reference)

Funktsiooni parameetriks saab kasutada nii viita (pointer) kui ka viidet (reference). Süntaktiliselt on lihtsam kasutada viidet (reference). Viite tüüpi parameeter on argumendi alias (mõlemad viitavad samale muutujale). Funktsiooni väljakutsel viitetüüpi parameeter initsialiseeritakse argumendiga ja funktsioon saab muuta argumenti otse. Funktsiooni

void suurenda(int& a){
++a;
}

korral käsk suurenda(arv) suurendab muutujat arv ühe võrra.

Kui funktsiooni parameeter on viidatüüpi (pointer), siis funktsioonile edastatakse argumendi aadress, mis kopeeritakse funktsiooni parameetrisse. Nüüd argumendi aadress ja funktsiooni parameeter viitavad samale aadressile, st samale muutujale. Muudame funktsiooni selliselt, et parameetriks on viidatüüpi muutuja.

#include <iostream>
using namespace std;
int muuda(int* a);
int main() {
    int arv{5};
    int tulemus {muuda(&arv)};
    cout << " <arv> main funktsioonis peale funktsiooni <muuda>: " << arv << "\n";
    cout << " Tagastati tulemus: " << tulemus << "\n";
    return 0;
}
int muuda(int* p_arv){
    // funktsioon muudab originaali
    *p_arv *= 3;
    cout << "<arv> funktsioonis <muuda> peale muutmist: " << *p_arv << "\n";
    return *p_arv;
}
<arv> funktsioonis <muuda> peale muutmist: 15
<arv> main funktsioonis peale funktsiooni <muuda>: 15
Tagastati tulemus: 15

Funktsioonile muuda edastatakse muutuja arv aadress. Aadress kopeeritakse funktsiooni parameetriks p_arv. Nüüd p_arv viitab muutujale arv. Funktsioonis käsk *p_arv *= 3; korrutab muutuja arv väärtuse kolmega. Käsk return *p_arv; teeb koopia *p_arv väärtusest ja tagastab selle väljakutsujale.

Kasutame nüüd funktsioonis viitetüüpi (reference) parameetrit. Sellise funktsiooni poole pöördumisel antakse argumendiks muutuja nimi. Funktsiooni parameeter on viitetüüpi (reference); funktsiooni sees töötame parameetri nagu tavalise muutujaga.

#include <iostream>
using namespace std;
int muuda(int& arv);
int main() {
    int arv{5};
    int tulemus{muuda(arv)};
    cout << "<arv> main funktsioonis peale funktsiooni <muuda>: " << arv << "\n";
    cout << "Tagastati tulemus: " << tulemus << "\n";
    return 0;
}
int muuda(int& arv){
    // funktsioon muudab originaali
    arv *= 3;
    cout << "<arv> funktsioonis <muuda> peale muutmist: " << arv << "\n";
    return arv;
}
<arv> funktsioonis <muuda> peale muutmist: 15
<arv> main funktsioonis peale funktsiooni <muuda>: 15
Tagastati tulemus: 15

Kokkuvõte argumentide edastamisest funktsioonile

(Siin on kasutatud Dan Bogdanovi konspekti https://courses.cs.ut.ee/MTAT.03.158/2015_spring/uploads/Main/cpp-praktikum04.pdf):

1. Parameetrid edastatakse väärtustena (values), nt void fun (int a, string b);

Funktsiooni välja kutsudes:

  • Kõikidest argumentide väärtustest tehakse automaatselt koopiad (näites a ja b).
  • Funktsiooni sees nende koopiate muutmine ei muuda originaalide algseid väärtusi.
  • Funktsiooni töö lõpus tehtud koopiad hävitatakse.
2. Parameetrid edastatakse viidetena (references), nt void fun(int& a, string& b);

Funktsiooni välja kutsudes:

  • Argumentide väärtustest koopiaid ei tehta.
  • Viiteparameeter hakkab viitama väljakutse argumendile.
  • Funktsioonis parameetreid muutes muutuvad ka algsed väärtused.
  • Funktsiooni töö lõpus hävitatakse viited, algsed muutujad jäävad alles.
3. Parameetrid edastatakse viitadena (pointers), nt void fun(int* a, string* b);

Funktsiooni välja kutsudes:

  • Väljakutses kasutatud viitadest tehakse koopiad, kuid nad jäävad viitama samadele väärtustele.
  • Parameetrina antud viitade aadresse muutes ei juhtu algsete viitadega midagi.
  • Parameetrina antud viitade poolt adresseeritud väärtuseid muutes algsed väärtused muutuvad.
  • Funktsiooni töö lõpus hävitatakse kopeeritud viidad. Algsed viidad ja väärtused jäävad alles.

Kui väärtus on suur, näiteks palju andmeid sisaldav objekt, siis on mõistlik kasutada viidet või viita, sest siis peab arvuti vähem andmeid kopeerima. Samuti on vaja viidet või viita kasutada, kui väljakutsutavalt funktsioonilt oodatakse argumentide muutmist.

Viite (reference) ja viida (pointer) tagastamine funktsioonist

Seni oleme käsitlenud funktsioone, mis ei tagasta midagi void või tagastavad mingi väärtuse. See väärtus tagastatakse kopeerimise teel, st kui tegemist on mahuka objektiga, siis kopeerimist on palju ja programm töötaks kiiremini, kui tagastataks väärtuse asemel viide või viit.

  • Märkus. Kaasaegsed kompilaatorid optimeerivad võimaluse korral väärtuse kopeerimise viite kasutamisega.

Funktsioon võib tagastada ka viida või viite objektile. Siin on oluline, et funktsioon ei tagastaks viita (viidet) lokaalsele muutujale, sest peale funktsiooni töö lõppu lokaalne objekt hävitatakse ja viit (viide) kaotab kehtivuse ja võib põhjustada programmi kokkujooksmise. Sellist viita nimetatakse rippuvaks viidaks (dangling pointer). Kompilaator võib anda ka vastavasisulise vea.

Järgmises näites tagastatakse viit kahest arvust suurimale. Funktsioonis suurim on mõlemad parameetrid antud viidetena ja käsk return tagastab viite ühele neist. Esimene pöördumine funktsiooni poole on otse väljundvoogu. Teisel pöördumisel toome sisse uue viitemuutuja c, mis antud näites peale funktsiooni tööd on b alias.

#include <iostream>
using namespace std;

int& suurim(int& a, int& b){
    return a > b ? a: b;
}

int main() { 
    int a = 5;
    int b = 8;
    cout << "suurim(a, b): " << suurim(a, b) << "\n";
    int& c{suurim(a, b)};
    cout << "int& c{suurim(a, b)}: " << c << "\n";
    return 0;
}
suurim(a, b): 8
int& c{suurim(a, b)}: 8

Sama näide kasutades viitasid (pointers)

#include <iostream>
using namespace std;

int* suurim(int* a, int* b){
    return *a > *b ? a: b;
}

int main() {
    int a = 5;
    int b = 8;
    cout << "suurim(a, b): " << *suurim(&a, &b) << "\n";
    int* c{suurim(&a, &b)};
    cout << "int* c{suurim(a, b)}: " << *c << "\n";
    return 0;
}
suurim(a, b): 8
int* c{suurim(a, b)}: 8

Vigane on aga järgmine funktsioon, mis arvutab summa ja tagastab viite lokaalsele muutujale. Keskkonnas Clion saame väljundi, kus funktsiooni tagastust ei väljastata.

#include <iostream>
using namespace std;

int& summa(int& a, int& b){
    int summa = a + b; 
    return summa; // VIGA! Tagastatakse viide lokaalsele muutujale.
}

int main() {
    int a = 5;
    int b = 9;
    cout << "summa(a, b): " << summa(a, b) << "\n";
    return 0;
}
summa(a, b):

Sama ülesanne, kus tagastatakse viit lokaalsele muutujale:

#include <iostream>
using namespace std;

int* summa(int* a, int* b){
    int c{*a + *b};
    return &c; // VIGA! Viit lokaalsele muutujale 
}
int main() {
    int a = 5;
    int b = 9;
    int* sum{summa(&a, &b)};
    cout << "summa(&a, &b): " << sum << "\n";
    return 0;
}
summa(&a, &b): 0

Siin programm kokku ei jooksnud, aga tulemus on vigane ja ettearvamatu.

Sellist viga saab vältida näiteks väljundparameetrite kasutamisega. Funktsioonis main defineeritakse muutuja sum ja tema viide edastatakse funktsioonile. Funktsioon muudab muutuja sum väärtust ja funktsioon ei pea tagastama midagi.

#include <iostream>
using namespace std;

void summa(int& a, int& b, int& sum) {
     sum = a + b;
}

int main() {
    int a = 5;
    int b = 9;
    int sum{};
    summa(a, b, sum);
    cout << "summa(a, b, sum): " << sum << "\n";
    return 0;
}
summa(a, b, sum): 14

Viit konstandile ja konstantne viit

Viida defineerimisel saab fikseerida nii viidatavat kui viita ennast, st neid (kas ühte või mõlemat) ei saa enam muuta. Vaatame näidet, kus meil on int tüüpi muutuja arv1 ja viit p_1, mis viitab sellele muutujale. Muutuja arv1 sisu on võimalik muuta nii muutuja enda kui ka viida (pointer) abil. Samuti on võimalik muuta viidamuutujat, et see viitaks uuele aadressile.

int arv1{5};
int arv2{10};
int* p_1{&arv1};
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame muutujat arv1 viida abil
*p_1 = 6;
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame viita p_1
p_1 = &arv2;
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
p_1 0x5d9f9ff6c4 *p_1 5
p_1 0x5d9f9ff6c4 *p_1 6
p_1 0x5d9f9ff6c0 *p_1 10

Defineerime viida, kus viidatavat ei saa viida kaudu muuta. Selleks kirjutame võtmesõna const viidatava tüübi ette. Nüüd ei saa otsendamisega (dereference) viidatavat muuta. Muutujale endale saab uut väärtust omistada. Viita võib muuta.

int arv1{5};
int arv2{10};
const int* p_1{&arv1}; // viidatavat ei saa enam viida abil muuta
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame arv1 viida abil
//*p_1 = 6; // viga, ei saa viida abil muuta
arv1 += 1; // arvu ennast saab muuta
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame viita p_1
p_1 = &arv2; // viita on võimalik muuta
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
p_1 0xd0807ff984 *p_1 5
p_1 0xd0807ff984 *p_1 6
p_1 0xd0807ff980 *p_1 10

Kirjutame nüüd const viida ette. Sellisel juhul ei saa muuta viita, st ta jääb viitama muutujale arv1, küll aga on võimalik muuta viidatavat viida kaudu.

int arv1{5};
int arv2{10};
int* const p_1{&arv1}; // viit on konstantne, jääb viitama samasse kohta
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame arv1 viida abil
*p_1 = 6; // viidatav on muudetav viida kaudu
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame viita p_1
//p_1 = &arv2; // viga, viita ei ole võimalik muuta
p_1 0x836d3ffcac *p_1 5
p_1 0x836d3ffcac *p_1 6

Lõpuks, fikseerime nii viida kui ka viidatava. Selleks kirjutame võtmesõna const nii tüübi kui ka viida ette.

int arv1{5};
int arv2{10};
const int* const p_1{&arv1}; // viit ja viidatav on konstantsed
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame arv1 viida abil
//*p_1 = 6; // viga, viidatav ei ole muudetav viida abil
arv1 += 1; // arvu ennast saab muuta
cout << "p_1 " << p_1 << " *p_1 " << *p_1 << '\n'; // viit ja viidatav
// muudame viita p_1
//p_1 = &arv2; // viga, viita ei ole võimalik muuta
p_1 0xa2d17ff71c *p_1 5
p_1 0xa2d17ff71c *p_1 

Viidad (pointers) ja massiivid

Viit (pointer) on alati seotud kindla andmetüübiga. Viit võib viidata ka massiivile ja veel enam, viita saab käsitleda massiivina. Defineerime täisarvumassiivi arvud. Sellisel juhul arvud on massiivi esimese elemendi aadress, st viit massiivile. Viidamuutujale massiivimuutuja omistamisel ei kirjutata massiivimuutujale ette &, sest massiivimuutuja juba ongi ise aadress int* p_m{arvud}. Siis viidamuutuja viitab massiivi esimesele elemendile.

int arvud[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//NB! järgmises käsus massiivimuutuja arvud ees ei ole & 
int* p_m{arvud}; // viit massiivile arvud
// massiivi aadress ja massiivi 1.element
cout << "arvud " << arvud << " arvud[0] " << arvud[0] << '\n';
cout << "p_m " << p_m << " *p_m " << *p_m << '\n'; // viit massiivile ja massiivi 1.element
arvud 0x942d5ff9a0 arvud[0] 1
p_m 0x942d5ff9a0 *p_m 1

Kuidas saada viida abil kätte massiivi järgmisi elemente? Selleks on kaks võimalust:

  • kasutada viidamuutujat koos indeksiga nagu massiivimuutuja korral
  • kasutada viida (pointer) aritmeetikat. Viidamuutujale ühe liitmine muudab aadressi vastavalt viidatavale tüübile. Täisarvu int (võtab mälus 4 baiti) korral liidetakse juurde 4. Kui liita juurde 2, siis aadress suureneb 8 võrra jne. See variant on väga mugav ja pole vaja nurksulge [] kasutada. Näiteks massiivi i-nda elemendi saab viida kaudu kätte avaldisega *(p_m + i). Analoogiliselt saab massiivimuutujat kasutada nagu viita, i-nda elemendi saab kätte avaldisega *(arvud + i).
int arvud[5]{1, 2, 3, 4, 5};
int* p_m{arvud}; // viit massiivile arvud
for (size_t i{}; i < 5; i++ ){
    // elementide aadressid
    cout << i << " " << &p_m[i] << " " << p_m + i << " " << &arvud[i] << '\n';
    // elemendid ise
    cout << i << " " << p_m[i] << " " << *(p_m + i) << '\n';
    cout << i << " " << arvud[i] << " " << *(arvud + i) <<'\n';
}
0 0x7586bffad0 0x7586bffad0 0x7586bffad0
0 1 1
0 1 1
1 0x7586bffad4 0x7586bffad4 0x7586bffad4
1 2 2
1 2 2
2 0x7586bffad8 0x7586bffad8 0x7586bffad8
2 3 3
2 3 3
3 0x7586bffadc 0x7586bffadc 0x7586bffadc
3 4 4
3 4 4
4 0x7586bffae0 0x7586bffae0 0x7586bffae0
4 5 5
4 5 5

Toome veel ühe näite massiivi elementide muutmisest. Defineerime massiivi ja anname elementidele väärtused tsüklis viida abil.

int arvud[5];
int *p_m{arvud}; // viit massiivile arvud
for (size_t i{}; i < 5; i++) {
    *(p_m + i) = i * i; // muudame elementi i
}
// kuvame elemendid ekraanile
for (size_t i{}; i < 5; i++) {
    cout << i << " " << *(arvud + i) << " " << *(p_m + i) << '\n';

}
0 0 0
1 1 1
2 4 4
3 9 9
4 16 16

Massiivi edastamine funktsioonile

Massiivi edastamisel funktsioonile ei kopeerita massiivi, vaid alati edastatakse funktsioonile viit (pointer), mis on massiivi esimese elemendi aadress. Funktsiooni päises ei kirjutata massiiviparameetri ette *, vaid kasutatakse lihtsalt massiivi nime, nt

void print(int m[], size_t pikkus);

Kui deklareerida parameetreid antud tüübi T jaoks, siis deklaratsioonid T massiiv[] ja T *massiiv on ekvivalentsed. Funktsiooni sees võib kasutada massiivi elementide kättesaamiseks nii indekseid kui ka viita (pointer). Esitame siin kaks varianti funktsioonist, mis teise parameetrina antud massiivist kopeerib elemendid esimesse massiivi tagurpidises järjekorras, st viimane element esimeseks, eelviimane teiseks, jne. Samuti on kaks varianti funktsioonist, mis kuvab massiivi ekraanile.

void tagurpidi1(int m1[], int m2[], size_t pikkus){
    for (int i = 0; i < pikkus; ++i) {
        m1[i] = m2[pikkus - i - 1];
    }
}
void tagurpidi2(int* m1, int* m2, size_t pikkus){
    for (int i = 0; i < pikkus; ++i) {
        *(m1 + i) = *(m2 + pikkus - i - 1);
    }
}

void print1(int m[], size_t pikkus){
    for (int i = 0; i < pikkus; ++i) {
        cout << m[i] << " ";
    }
    cout << '\n';
}
void print2(int m[], size_t pikkus){
    for (int i = 0; i < pikkus; ++i) {
        cout << *(m + i) << " ";
    }
    cout << '\n';
}

Rakendame funktsioone konkreetsete massiivide korral

int m2[5]{1, 2, 3, 4, 5};
int m1[5];
tagurpidi1(m1, m2, 5); // m2-st kopeeritakse tagurpidi m1-te
print1(m1, 5);
tagurpidi2(m2, m1, 5); // m1-st kopeeritakse tagurpidi m2-te
print2(m2, 5);
5 4 3 2 1 
1 2 3 4 5

NB! Funktsioon ei saa tagastada massiivi. C++ ei luba funktsioonist tagastada massiivi tüüpi väärtust.

Operaatorite üledefineerimine (overloading)

Klassi isendeid võib kasutada avaldistes operandidena. Näiteks, kui ob1 ja ob2 on mingi klassi objektid, siis avaldised

ob1 + ob2;
ob1++;

võiksid anda tulemuseks sama tüüpi objekti. Selleks tuleb üle defineerida operaatorid klassi jaoks. Operaatoreid defineerime üle siis, kui tulemuseks on mõistlik sisu. Vaatame näiteks klassi Vektor (NB! See ei ole andmestruktuur!).

#include <iostream>
#include <cmath>
using namespace std;
class Vektor {
public:
    Vektor(double x, double y) : m_x{x}, m_y{y} {}
    Vektor() = default;

    double pikkus() {
        return round(sqrt(m_x * m_x + m_y * m_y) * 100) / 100.0;
    }

    void trüki() {
        cout << "(" << m_x << ", " << m_y << ")\n";
    }

private:
    double m_x{};
    double m_y{};
};

int main() {

    Vektor v1{1, 2};
    Vektor v2{3, 5};
    v1.trüki();
    v2.trüki();
    cout << "v1.pikkus(): " << v1.pikkus() << '\n';
    cout << "v2.pikkus(): " << v2.pikkus() << '\n';
    return 0;
}
(1, 2)
(3, 5)
v1.pikkus(): 2.24
v2.pikkus(): 5.83

Tasandi vektoril on kaks koordinaati (isendimuutujad m_x ja m_y). Klassil on kaks konstruktorit - vaikekonstrukror ja kahe parameetriga konstruktor. Vektori pikkuse arvutamiseks on liikmefunktsioon pikkus, kus arvutamiseks läheb vaja teegi <cmath> funktsioone sqrt ja round. Funktsioon trüki kuvab ekraanile vektori koordinaadid sobival kujul. Tasandi vektoritega saab teha tehteid, nt liita, lahutada ja skalaariga korrutada. Kõigepealt defineerime skalaariga korrutamise. Selleks tuleb klassi lisada funktsioon, mille nimeks on operator*=. Skalaariga korrutamisel korrutame mõlemat koordinaati parameetriks oleva arvuga ja funktsioon ei tagasta midagi

void operator*=(double arv){
    m_x *= arv;
    m_y *= arv;
}

Funktsioon main:

int main() {
    Vektor v1{1, 2};
    v1 *= 2.5;
    v1.trüki();
    return 0;
}
(2.5, 5)

Vektorite liitmise defineerime väljaspool klassi. Selleks, et funktsioon väljaspool klassi saaks kasutada klassi privaatseid isendimuutujaid, tuleb funktsioon deklareerida klassi sõbraks (friend). Seda tehakse klassi sees deklaratsiooniga. Kuna vektorite liitmisel operande ei muudeta (tulemuseks on uus vektor), on mõistlik liidetavad vektorid kuulutada konstantseteks. Kopeerimise vältimiseks on liidetavad vektorid antud viidetena (references).

#include <iostream>
#include <cmath>
using namespace std;

class Vektor {
public:
    Vektor(double x, double y) : m_x{x}, m_y{y} {}
    Vektor() = default;
    double pikkus() {
        return round(sqrt(m_x * m_x + m_y * m_y) * 100) / 100.0;
    }
    void trüki() {
        cout << "(" << m_x << ", " << m_y << ")\n";
    }
    friend Vektor operator+(const Vektor& vasak, const Vektor& parem);
    void operator*=(double arv) {
        m_x *= arv;
        m_y *= arv;
    }
private:
    double m_x{};
    double m_y{};
};
Vektor operator+(const Vektor& vasak, const Vektor& parem) {
    return Vektor{vasak.m_x + parem.m_x, vasak.m_y + parem.m_y};
}

int main() {
    Vektor v1{1, 2};
    Vektor v2{3, 5};
    Vektor v3 = v1 + v2;
    v3.trüki();
    return 0;
}

(4, 7)

Defineerime veel üle vektori väljundvoogu saatmise. Selleks lisame klassi deklaratsiooni operaatori << üledefineerimise kohta, kus ostream on standardväljund.

friend ostream& operator<<(ostream& os, const Vektor& v);

Operaatori << üledefineerimine on väljaspool klassi, kus väljundvoogu os saadetakse vektori koordinaadid sobival kujul.

ostream& operator<<(ostream& os, const Vektor& v){
    os << "(" << v.m_x << ", " << v.m_y << ")\n";
    return os;
}

main funktsioonis saab nüüd vektorit mugavalt ekraanile kuvada

Vektor v1{1, 2};
Vektor v2{3, 5};
cout << v1 << v2 << '\n';

Tulemuseks on

(1, 2)
(3, 5)

Täpsemalt saab operaatorite üledefineerimist uurida lehel https://en.cppreference.com/w/cpp/language/operators

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