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
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
jab
). - 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 massiivii
-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