Programmeerimine keeles C++
Praktikum 4 – viidad, viited ja nende kasutamine
Viidad (pointers)
C++ lubab sarnaselt C keelega programmeerijal mäluga töötada otse – viitade kaudu. Viit on aadress mälupesale, milles asuvad või mille juures algavad mingid andmed. Viida puhul on tegelikult vaja mõelda kahele eraldiseisvale väärtusele: aadressile ja selle poolt viidatud andmetele. Pane tähele, et viida defineerimisel ei eraldata vaikimisi andmetele mälu. Sellega peab programmeerija tegelema ise. Kui aadress viitab suvalisele mälule, siis andmete lugemisel mälukaitsega operatsioonisüsteemid peatavad programmi töö (veateadetega stiilis segmentation fault, page fault, general protection fault).
Järgneval joonisel on toodud näide mälust. Esimeses reas on mäluaadressid. Teises reas väärtused. Iga mälupesa salvestab ühe baidi. Mitmebaidiste väärtuste puhul on oluline, millises järjekorras üksikuid baite loetakse. Antud näites loeme baite vasakult paremale. Näiteks: kui kahes mälupesas on järjest baidid 2 ja 1, siis nende pesades asub kahebaidine täisarv 258 (256*1 + 2). Pikemalt baitide järjekorra kohta loe siit: http://en.wikipedia.org/wiki/Little_endian.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
34 | 4A | 11 | B5 | FF | 3 | 23 | F0 | C0 | 20 | 6D | 30 | 83 | 0 | 7 | 1 | 34 | 22 | BA | D |
Viida defineerimiseks kasutatakse tärni. Tärni kasutatakse hiljem ka viidatud mäluaadressilt andmete lugemiseks. Näidet viida defineerimisest ja kasutamisest näete järgmises koodilõigus. NB! Antud kood arvutil tööle ei lähe, sest mäluaadressile 15 ei lasta programmeerijat lihtsalt ligi. Vaadelge seda kui näidist.
unsigned short *somenumber = 15; /* Salvestame mäluaadressi */ cout << somenumber; /* Väljastab 15 */ cout << *somenumber; /* Väljastab 263 (miks?) */ somenumber = 14; /* Uus aadress. Mis on *somenumber nüüd? */ *somenumber = 1; /* Muudame väärtust. */ somenumber = 15; /* Mis on nüüd *somenumber väärtus? */
Millal viitasid kasutada?
Viitade kasutamine on keerulisem kui tavaline muutujate loomine ja seega tasub seda kasutada siis, kui sellega kaasneb parem disain või kõrgem efektiivsus. Enne iga viida kasutamist tuleb kontrollida, et see viit oleks väärtustatud ja ei oleks nullviit (C++11 lisab võtmesõna nullptr
, varasemalt kasutati väärtust 0 või konstant NULL). Lisaks tuleb viida jaoks küsitud mälu pärast kasutamist vabastada, sest muidu see „lekib“ ning kahandab edaspidi saadaval oleva mälu hulka.
Viited (references)
Viited lisati C++ keelde, et pakkuda mõningaid viitade võimalusi ilma kaasnevate ohtudeta. Viide on sisuliselt uus nimi, mille abil mingit muutujat või objekti kasutada saab. Viitel ei ole endal sisu ega aadressi – ta ainult viitab mingile juba olemasolevale objektile.
Viite defineerimiseks kasutatakse ampersandi. Lihtsat viite kasutamise näidet näed siin:
int a = 5, b = 9; // defineerime täisarvud a ja b int& refToA = a; // refToA on uus nimi muutujale a cout << a << "," << refToA << "," << b; // väljastab 5,5,9 a = 7; // muudame a väärtust cout << a << "," << refToA << "," << b; // väljastab 7,7,9 refToA = 11; // muudame refToA väärtust cout << a << "," << refToA << "," << b; // väljastab 11,11,9
Millal viiteid kasutada?
Kui sul on vaja mitmel pool viidata samadele andmetele või objektidele, siis võimalusel kasuta viiteid ja kui need ei sobi (nt on vaja eristada olemasolevat objekti sellisest, mida pole veel loodud), siis viitasid. Kasuta viiteid igal pool, kus nad aitavad vältida liigset väärtuste kopeerimist.
Viida jaoks mälu haaramine ja vabastamine: käsud new ja delete
Käsk new
arvutab välja, kui palju antud tüüpi objekti loomiseks mälu on vaja, haarab selle ning tagastab viida mäluosa algusele.
int *a = new int; // ilma konstruktorita väljakutse Vector2 *v = new Vector2 {}; // konstruktorile võib lisada ka parameetreid
Kui objekt on viit, siis tema andmetele või meetoditele ligipääsemiseks on tehe ->
cout << "Koordinaadid on " << v->x << " ja " << v->y << "." << endl;
Mälu haaramine võib ebaõnnestuda, seega tuleb alati kontrollida, kas haaramine õnnestus või mitte. Väärtused, mida oled new
abil loonud, tuleb kustutada delete
abil. NB! Kui kasutasid mitme objekti jaoks mälu haaramiseks käsku new[]
, pead mälu vabastamiseks kasutama delete[]
.
delete a; delete v;
Lisa loe siit:
Viidad: http://www.cplusplus.com/doc/tutorial/pointers.html
Dünaamiline mäluhaldus: http://www.cplusplus.com/doc/tutorial/dynamic.html
Viidad ja viited funktsioonide ja meetodite parameetrites
Funktsioonide argumentide tüübi määramisel tuleb otsustada, kas argument edastada viidana, viitena või tavalise väärtusena. Järgmised näited aitavad teil seda ehk otsustada.
Variant A: Parameetrid edastatakse väärtustena
void process_values (int a, string b, Vector2 c);
Funktsiooni välja kutsudes toimuvad järgmised asjad:
- Kõikidest väärtustest, mida väljakutses kasutatakse, tehakse automaatselt koopiad.
- Kompilaator optimeerida, loe lisaks liigutamissemantikast allpool.
- Funktsiooni sees nende koopiate muutmine ei muuda väärtuseid väljakutsuvas funktsioonis.
- Funktsiooni töö lõpus tehtud koopiad hävitatakse.
Variant B: Parameetrid edastatakse viidetena
void process_references (int &a, string &b, Vector2 &c);
Funktsiooni välja kutsudes toimuvad järgmised asjad:
- Väljakutses kasutatud muutujate väärtustest koopiaid ei tehta.
- Funktsioonis parameetreid muutes muutuvad ka algsed väärtused väljakutsuvas funktsioonis.
- Funktsiooni töö lõpus hävitatakse viited, algsed muutujad jäävad alles.
Variant C: Parameetrid edastatakse viitadena
void process_pointers (int *a, string *b, Vector2 *c);
Funktsiooni välja kutsudes toimuvad järgmised asjad:
- 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.
Viidad ja viited funktsioonide ja meetodite tagastusväärtustes
Ettevaatlik tuleb olla ka siis, kui viit või viide esineb meetodi või funktsiooni poolt tagastatavas väärtuses. Toon taas selgitavad näited, aga enne tutvustan kiirelt muutuja skoobi mõistet. Tavaline muutuja on „elus“ oma skoobi ulatuses – selle struktuuri või koodiosa sees, milles ta defineeriti. Kui struktuur hävib või koodiosa täitmine lõpeb, muutuja hävitatakse.
int arvuta_midagi () { ... int a = ... | for (int i = 0; i < 0; i++) { | | cout << i << endl; | i skoop | } | | a skoop return a; | } |
Muutuja a
on tavaline muutuja, tema jaoks haaratakse ja vabastatakse mälu automaatselt. Meetodist väljumisel a
hävitatakse ja tsüklist väljudes hävineb i
. Funktsioonis arvuta_midagi
tagastatakse täisarv a
. Kuna tagastamine toimub väärtuse järgi, siis tehakse a
-st koopia funktsiooni välja kutsunud funktsiooni või meetodi jaoks.
string& arvuta_teisiti () { string s = "C++"; return s; }
Funktsioon arvuta_teisiti
proovib tagastada viidet kohalikule muutujale. See on väga suure tõenäosusega vale ning kompilaator tavaliselt ka hoiatab programmeerijat selle eest. Muutuja s
all olev mälu vabastatakse ning funktsiooni poolt tagastatav viide jääb näitama ei-tea-kuhu. Viite tagastamisel tuleb jälgida, et see kehtiks ka pärast funktsiooni töö lõppu.
Vector2* tee_uus_vektor () { | Vector2* tee_uus_vektor2 () { Vector2 v; | Vector2* v = new Vector2 {}; return &v; // VALE | return v; } | }
Funktsioonis tee_uus_vektor
luuakse vektor tavalise muutujana ning seejärel tagastatakse selle aadress (saage tuttavaks aadressi võtmise tehtega &
, mis tagastab temast paremal pool oleva objekti aadressi mälus). Selle tegevusega on seotud sama oht nagu siis, kui me tagastaks viite. Palju suurem probleem on see, et siin kompilaator meid ei hoiata, sest mitte kuskile näitav viit on igati legaalne nähtus. Funktsioon tee_uus_vektor2
näitab, kuidas selliseid funktsioone korrektselt teha saab. Siin tuleb tähele panna, et näites toodud meetodi kasutamisel on programmeerijal kohustus ka mälu ise vabastada. Sobiv funktsioon on näiteks:
void vabasta_vektor (Vector2 *v) { if (v) delete v; }
Pane tähele, et tohib teha ka nii nagu järgmises näites. Meetodi poolt tagastatav viit kehtib nii kaua, kuni Someclass
instants, mille peal get_something
välja kutsuti, eksisteerib.
class Someclass { int a; int* get_something () { return &a; } };
Kuidas C++11 muudab viitade ja viidete kasutamist?
C++11 täiendab viitade ja viidete kasutamist mitmel moel. Kõiki uuendusi me siin aines katta ei jõua. Järgmised materjalid on neile, kes tunnevad keele võimaluste vastu huvi.
Liigutamiskonstruktor ja liigutav omistamiskäsk
Lisaks konstruktorile, destruktorile, koopiakonstruktorile ja omistamiskäsule saab arendaja nüüd kasutada liigutamiskonstruktorit ja liigutavat omistamiskäsku, mis väldib objekti kopeerimist. See on kasulik siis, kui objekti ei pea kopeerima, vaid teda võib ka liigutada.
SomeObject a, b, c; c = a + b;
Kui SomeObject
klassil on tavaline omistamiskäsk, siis tehakse mälus summa a + b
jaoks ajutine muutuja, mille sisu seejärel kopeeritakse c
sisse. Kui aga sellel klassil on olemas liigutav omistamiskäsk, siis välditakse a + b
tulemuse kopeerimist c
sisse ning seega tehakse vähem tööd. Loe näiteks:
http://stackoverflow.com/questions/4782757/rule-of-three-becomes-rule-of-five-with-c11
Liigutamissemantikaga objektide kasutamine parameetrites ja tagastusväärtustes
Liigutamissemantika aitab ka vähendada objektide kopeerimist parameetrites ja tagastusväärtustes. Siin tuleb aga hoolega vaadata seda, mida funktsioon või meetod sisemiselt teeb ja vastavalt sellele otsustada. Mõned head näited on toodud siin:
http://stackoverflow.com/questions/7592630/is-pass-by-value-a-reasonable-default-in-c11
Targad viidad
C++11 sisaldab tarkade viitade (smart pointer) tuge. Tark viit oskab ise jälgida, millal tema haaratud mälu tuleks vabastada. Tarkade viitade kasutamine aitab turvalisemalt viitadega programmeerida. Meie kasutame praktikumides siiski tavalisi viitu, sest programmeerija peab ikkagi aru saama sellest, mismoodi viidad töötavad. Samas võite oma projektides kasutada tarku viitu ning me pigem julgustame seda. Põhjalikku lugemismaterjali leiate siit:
http://www.codeproject.com/Articles/541067/Cplusplus11-Smart-Pointers