Keele põhikonstruktsioonid II
Pärast selle praktikumi läbimist üliõpilane
- oskab kasutada käsurea argumente
- oskab koostada funktsioone, neid üle defineerida
- teab, mis on pistikfunktsioon (inline function)
- oskab koostada mitmest failist koosnevaid programme ja neid kompileerida
- teab, kuidas organiseerida koodi mitmesse faili
- oskab manipuleerida sõnetüüpi (string) andmetega
- teab, mis on muutuja eluiga (lifetime) ja skoop (scope)
Funktsioonid
Funktsiooni üldkuju keeles C++ on järgmine:
tagastustüüp funktsiooni_nimi(parameetrite loetelu){ funktsiooni keha }
Tagastustüüp määrab andmetüübi, mida funktsioon tagastab. Kui funktsioon ei tagasta midagi, siis on tagastustüübiks void
. Tagastamine toimub käsuga return
.
C++ programmis on alati funktsioon, kust algab programmi töö - see on main
funktsioon. Funktsioonil main
parameetriteks võivad olla käsurea argumendid
int main(int argc, char* argv[]){ ... }
Parameeter argc
on käsurea argumentide arv (täisarv). Parameetri argc
väärtus on alati vähemalt 1, sest esimeseks käsurea argumendiks (indeksiga 0) on programmi nimi. Parameeter argv[]
on viit char
tüüpi massiivile. Viitasid käsitletakse hiljem.
Keskkonnas Clion
saab käsurea argumente sisestada menüüst Run -> Edit Configurations -> Program arguments
. Sisestame sinna "Pühapäev on tore päev!".
#include <iostream> using namespace std; int main(int argc, char* argv[]) { cout << "Käsureal on " << argc << " argumenti.\n"; for (int i{1}; i < argc; ++i) { cout << argv[i] << " "; } return 0; } | Käsureal on 5 argumenti. Pühapäev on tore päev! |
Kui väljundisse ei tule täpitähti, siis Windows korral tuleb seadistada süsteemilokatsiooni.
Meie näidetes tagastab main
meetod alati 0, mis näitab operatsioonisüsteemile, et programm lõpetas töö edukalt.
Et programm edukalt kompileeruks, peab funktsioon olema defineeritud või deklareeritud enne tema poole pöördumist. Lisame programmi funktsiooni, mis arvutab ruumala:
#include <iostream> using namespace std; double ruumala(double pikkus, double laius, double kõrgus){ return pikkus*laius*kõrgus; } int main() { double pikkus, laius, kõrgus; cout << "Sisesta risttahuka mõõtmed:"; cin >> pikkus >> laius >> kõrgus; double v = ruumala(pikkus, laius, kõrgus); cout << "Ruumala on " << v << "\n"; return 0; } | Sisesta risttahuka mõõtmed:1.1 2.2 3.3 Ruumala on 7.986 |
Väga levinud on programmeerimisstiil, kus main
funktsiooni ees on võimalikult vähe koodi. Et main
ees koodi lühendada, on võimalik funktsioonide deklareerimine eraldada funktsiooni definitsioonist.
Funktsiooni deklaratsioonis näidatakse ära tagastustüüp, funktsiooni nimi ja parameetrid, st esitatakse funktsiooni kirjeldus ilma funktsiooni kehata. Kirjutame programmi ümber kasutades funktsiooni deklaratsiooni:
#include <iostream> using namespace std; double ruumala(double pikkus, double laius, double kõrgus); int main() { double pikkus, laius, kõrgus; cout << "Sisesta risttahuka mõõtmed:"; cin >> pikkus >> laius >> kõrgus; double v = ruumala(pikkus, laius, kõrgus); cout << "Ruumala on " << v << "\n"; return 0; } double ruumala(double pikkus, double laius, double kõrgus){ return pikkus*laius*kõrgus; } | Sisesta risttahuka mõõtmed:1.1 2.2 3.3 Ruumala on 7.986 |
Funktsiooni ruumala
definitsioon on lisatud peale main
funktsiooni.
Funktsiooni deklareerimisel võib parameetritele anda ka vaikeväärtusi (default values). Oluline on, et vaikeväärtustega parameetrid oleksid funktsiooni parameetrite loetelu lõpus. Näiteks, eelmises näites saab funktsiooni ruumala
kasutada ka ristküliku pindala arvutamiseks, deklareerides
double ruumala(double pikkus, double laius, double kõrgus = 1);
Pöördudes selle funktsiooni poole kahe parameetriga
double s = ruumala(5, 3);
kasutatakse kõrguseks vaikeväärtust ja muutuja s
väärtuseks on 5 * 3 * 1
.
Funktsioonide üledefineerimine (overloading)
Sama nimega funktsiooni võib mitu korda defineerida, kui definitsioonid erinevad parameetrite arvu ja/või tüüpide poolest. NB! Tagastustüübi erinevus ei loe! Seda nimetatakse funktsiooni üledefineerimiseks. Järgmises näites on kolm funktsiooni keha (risttahukas, silinder, kera) ruumala arvutamiseks , mis erinevad parameetrite arvu poolest.
#include <iostream> #include <cmath> using namespace std; double ruumala(double pikkus, double laius, double kõrgus); double ruumala(double raadius, double kõrgus); double ruumala(double raadius); int main() { cout << "Risttahuka ruumala: " << ruumala(1.1, 2.2, 3.3) << "\n"; cout << "Silindri ruumala: " << ruumala(4.4, 3.3) << "\n"; cout << "Kera ruumala: " << ruumala(4.4) << "\n"; return 0; } double ruumala(double pikkus, double laius, double kõrgus){ return pikkus*laius*kõrgus; } double ruumala(double raadius, double kõrgus){ return M_PI * pow(raadius, 2) * kõrgus; } double ruumala(double raadius){ return 4*M_PI * pow(raadius, 3)/3; } | Risttahuka ruumala: 7.986 Silindri ruumala: 200.71 Kera ruumala: 356.818 |
Funktsioonide deklaratsioonid on enne main
funktsiooni ja definitsioonid on main
funktsiooni järel. Näites kasutatakse teegi <cmath>
konstanti M_PI
(π) ja astendamise jaoks funktsiooni pow
.
Pistikfunktsioonid (inline functions)
Tarkvaraarenduse hea praktika kohaselt koosneb programm funktsioonidest. Kuid lisaks funktsiooni täitmisele kulub aega ka funktsiooni väljakutsele. Keeles C++
on võimalik defineerida pistikfunktsioone, mis vähendavad väljakutsele kuluvat aega. Kirjutades võtmesõna inline
funktsiooni tagastustüübi ette, antakse kompilaatorile soovitus paigutada funktsiooni kood väljakutse kohale. Kuna programm muutub pikemaks (funktsiooni koodi võidakse paigutada paljudesse kohtadesse), siis on pistikfunktsioone mõistlik kasutada vaid väga lühikeste (nt üherealiste) funktsioonide korral. Näide pistikfunktsiooni kasutamisest.
#include <iostream> using namespace std; inline int suurim(int a, int b){ return a > b? a: b; } int main() { cout << "suurim(6, 2): " << suurim(6, 2) << " suurim(45, 100): " << suurim(45, 100); return 0; } | suurim(6, 2): 6 suurim(45, 100): 100 |
Mitmest failist koosnev programm
Suurema programmi korral on mõistlik funktsioonid paigutada eraldi faili ja funktsioonide deklaratsioonid eraldada definitsioonidest. Paigutame funktsioonide deklaratsioonid faili funktsioonid.h
ja funktsioonide definitsioonid (kehad) faili funktsioonid.cpp
. Keskkonnas Clion
saab päisefaile ja C++ lähtekoodifaile luua parema hiireklõpsuga projektil. Lähtekoodifaili loomisel tuleb märkida linnuke Add to targets
märkeruudul (vt joonis).
Märkeruudu Add to targets
valimine lisab faili CMakeLists.txt
käsule add_exeutable
uue argumendi faili nime näol.
Fail funktsioonid.h
:
#ifndef N3_1_FUNKTSIOONID_H #define N3_1_FUNKTSIOONID_H #include <cmath> double ruumala(double pikkus, double laius, double kõrgus); double ruumala(double raadius, double kõrgus); double ruumala(double raadius); #endif
Fail funktsioonid.cpp
:
#include "funktsioonid.h" #include <cmath> double ruumala(double pikkus, double laius, double kõrgus){ return pikkus*laius*kõrgus; } double ruumala(double raadius, double kõrgus){ return M_PI * pow(raadius, 2) * kõrgus; } double ruumala(double raadius){ return 4*M_PI * pow(raadius, 3)/3; }
Päisefailid
Eelprotsessori (preprocessor) käsk #include
annab kompilaatorile teada, et on vaja kaasata päisefail. Selleks, et oleks võimalik kasutada mingi teegi funktsioone, on vaja kaasata #include
käsuga teegi päisefail. Antud näites on üheks päisefailiks funktsioonid.h
, milles on funktsiooni ruumala
kolm deklaratsiooni. Failis funktsioonid.cpp
on kaks include
käsku
#include "funktsioonid.h" #include <cmath>
Esimene käsk kaasab päisefaili funktsioonid.h
ja teine päisefaili <cmath>
. See, kas faili nimi on jutumärkide (""
) või nurksulgude (<>
) sees määrab, kuidas faili otsima hakatakse. Kui faili nimi on nurksulgude sees, siis otsitakse faili vastavalt kompilaatori eelistustele. Kui faili nimi on jutumärkide sees, siis faili otsitakse teisel viisil, enamasti projekti kaustast.
Eelprotsessori käsud algavad märgiga #
ja käskudeks on veel
#define, #if, #ifdef, #ifndef, #elif, #else, #endif, #line, #pragma, #undef
Päisefail ise võib sisaldada käske. Käsk #ifndef N3_1_FUNKTSIOONID_H
on paaris käsuga #endif
ja kontrollib, kas N3_1_FUNKTSIOONID
ei ole juba olemas (not defined ). Võib juhtuda, et samu funktsioone on kasutatud juba mitmes teises lähtekoodifailis. Käsk #ifndef ... #endif
koos käsuga #define
garanteerib, et päisefaile ja neile vastavaid definitsioone ei kaasataks programmi mitmekordselt.
Käsk #define N3_1_FUNKTSIOONID_H
defineerib nime (identifikaatori) ja sisu, mida seda nime kasutades asendatakse lähtefaili. Tavaliselt on nimi suurte tähtedega. Keskkonnas Clion
määratakse nimi päisefaili loomisel automaatselt kujul projektinimi_päisefail_H
.
C++ standardnimeruumis sisalduvad päisefailid leiab siit https://en.cppreference.com/w/cpp/standard_library
Projektifailid mitmes kaustas
Et projekti paremini struktureerida, on mõistlik paigutada failid tüübi järgi eri kaustadesse. Teeme projektikausta kaks alamkausta: include
päisefailide ja src
lähtekoodifailide jaoks (main.cpp
) jätame juurkausta. Sellisel juhul peame muutma päisefailide sisselugemise käske:
failis main.cpp
on nüüd käsk #include "include/funktsioonid.h"
; failis CMakeLists.txt add_executable
käsus on nüüd src/funktsioonid.cpp
ja failis funktsioonid.cpp
on nüüd käsk #include "../include/funktsioonid.h"
. Toome siinkohal ära projekti struktuuri ja failide algused
main.cpp
#include <iostream> #include "include/funktsioonid.h" ...
funktsioonid.cpp
#include "../include/funktsioonid.h" #include <cmath> ...
Töö sõnedega: standardnimeruumi teek <string>
C++ standardnimeruumis on päisefail <string>
, mille kaasamine võimaldab sõnedega manipuleerida mugavamalt võrreldes C-keele märgimassiiviga. Päisefail <string>
deklareerib liittüübi string
. Tutvume näidete varal mõnede olulisemate võimalustega.
NB! Järgmised programmilõigud ja avaldised eeldavad käsu #include <string>
olemasolu.
Algväärtustamiseks on mitmeid võimalusi:
Avaldis | Selgitus |
---|---|
string t; | t on tühisõne "" |
string s{"Tere"}; | Algväärtustamine sõneliteraaliga. |
string u{s}; | Algväärtustamine olemasoleva sõne abil. |
string osa{"Rahva raamat", 5}; | Sõneliteraalist esimesed 5 märki: "Rahva" |
string lause{"Rahva raamat"};string lause_osa{lause, 6, 3}; | Sõnemuutujast alates 6ndast märgist 3 märki: "raa" |
Initsialiseerimine ja omistamine:
string rahvas{"rahvas"}; string raamat{"raamat"}; rahvas = raamat; cout << rahvas << "\n"; raamat = rahvas; cout << raamat << "\n"; | raamat raamat |
Sõnesid saab ühendada, eraldada alamsõnet, võrrelda ja märke asendada. Olgu meil kaks sõnemuutujat:
string s{"Programmeerimine"}; string t{"Kood"};
Avaldis | Väärtus | Selgitus |
---|---|---|
s.length() | 16 | Sõne pikkus baitides. NB! Täpiga tähed võtavad rohkem kui ühe baidi! |
t + " 007" | "Kood 007" | Sõnede ühendamine |
"Viis " + "pluss" | Viga! Sõneliteraale ei saa ühendada, üks operand peab olema muutuja | |
s + 5 | Viga! Sõnet ei saa arvuga ühendada. | |
s.substr(2, 3) | "ogr" | Alamsõne pikkusega 3 märki alates märgist indeksiga 2 |
(s < t) | false | Sõnesid võrreldakse leksikograafiliselt |
to_string(234) | "234" | Funktsioon to_string teisendab arvu (ka ujukomaarvu) sõneks |
t.starts_with("Ko") | true | Sõne alguse kontroll |
s[0] = 'T'; | s väärtus nüüd "Trogrammeerimine" |
Arvu teisendamine sõneks (string)
Toome siin eraldi välja, et arvu teisendamiseks sõneks (string) saab kasutada funktsiooni to_string
:
cout << to_string(42); << '\n'; // 42 cout << to_string(42.25) << '\n'; // 42.250000
Ujukomaarvude korral on tulemuseks saadud sõnes täpsus 6 kohta peale koma, vt https://cplusplus.com/reference/string/to_string/
Sõne (string) teisendamine arvuks
Sõne teisendamisel arvuks saab kasutada funktsioone, mis sõltuvad arvu tüübist:
stoi
- teisendamine täisarvuks (string to int)stof
- teisendamine float
tüüpi arvuks (string to float)stod
- teisendamine double
tüüpi arvuks (string to double)
Järgmine näide illustreerib string
teisendust arvutüübiks
string s1{"25"}; int a1 = stoi(s1); //int korral string s2{"25.2"}; float a2 = stof(s2); //float korral string s3{"35.2"}; double a3 = stod(s3); //double korral cout << "25 + 25.2 + 35.2 = " << a1 + a2 + a3 << '\n'; | 25 + 25.2 + 25.2 = 85.2 |
Täpsemalt saab <string>
võimalusi uurida siit: https://en.cppreference.com/w/cpp/header/string
Struktuur (structure)
Struktuur (struktuuritüüp) on pärit C-keelest. Struktuur on kogum nimedega varustatud komponentidest, mis võivad olla eri tüüpi. Struktuuri deklaratsioon kirjeldab malli, mida saab kasutada struktuuriobjektide (structure objects) loomiseks. Muutujaid, millest struktuur koosneb, nimetatakse struktuuri liikmeteks või elementideks. Tavaliselt on struktuuri elementidel omavahel loogiline seos. Struktuuri deklareeritakse võtmesõnaga struct
, mille lõpus on struktuurimuutujate loetelu. Struktuuri liikmetele pääseb juurde.
operaatori abil. Näiteks järgmine kood deklareerib kaks struktuurimuutujat
a_info
ja b_info
(kasutatakse teeki <string>
):
struct aadress{ string tänav; string linn; unsigned int postiindeks; }a_info, b_info; a_info.tänav = "Anne"; a_info.linn = "Tartu"; a_info.postiindeks = 50603; b_info.linn = "Teadmata"; cout << "Aadress:\n" << a_info.tänav + " tänav\n" + a_info.linn + "\n" + to_string(a_info.postiindeks) << "\n"; | Aadress: Anne tänav Tartu 50603 |
Struktuuri üldkuju on järgmine:
struct struktuuritüübi-nimi{ tüüp liikme-nimi; ... tüüp liikme-nimi; }struktuurimuutujate loetelu;
Struktuuritüübi nimi või struktuurimuutujate loetelu võivad puududa (kuid mitte mõlemad). Täpsemalt saab struktuuritüübi võimalustest uurida siit: https://en.cppreference.com/w/c/language/struct
Muutujate eluiga (life time) ja skoop (scope)
Kõigil muutujatel (objektidel, täpsemalt nimedel, mis neile viitavad) on lõplik eluiga. Muutuja hakkab eksisteerima sellest punktist, kus ta defineeritakse ja mingis punktis ta lakkab eksisteerimast - kõige hilisemaks punktiks on programmi töö lõpp. Käskude plokk on loogeliste sulgude { }
vahel olevate käskude kogum. Plokid võivad olla üksteise sees. Muutujad, mis defineeritakse ploki sees (ja ei ole static
), eksisteerivad ja on kättesaadavad vaid selle ploki sees. Neid nimetatakse lokaalseteks muutujateks.
Muutujatel on skoop ehk nähtavus (kehtivusala). Muutuja skoop on programmiosa, kus muutuja on kehtiv (nähtav). Väljapool skoopi ei saa muutujat kasutada. On võimalik defineerida muutujaid väljaspool programmi kõiki funktsioone. Neid nimetatakse globaalseteks muutujateks. Muutujate skoope illustreerib järgmine joonis:
Muutuja v1
faili algul on globaalne muutuja, samuti on globaalne muutuja v4
, mis on defineeritud peale main
funktsiooni. Globaalsetel muutujatel on vaikimisi algväärtused, arvuliste tüüpide korral on see 0
. Globaalsete muutujate eluiga on programmi töö algusest lõpuni. Globaalsete muutujate skoop algab punktist, kus nad on defineeritud ja lõpeb faili lõpus. Näiteks muutujat v4
ei saa main
funktsioonis kasutada, sest see jääb väljapoole tema skoopi. Lokaalne muutuja v1
funktsioonis fn
"varjab" ära samanimelise globaalse muutuja. Kui on vaja funktsiooni fn
sees kasutada globaalset muutujat v1
, siis saab seda teha kasutades skoobioperaatorit ::
:
cout << ::v1 << "\n"; //globaalne muutuja cout << v1 << "\n"; //lokaalne muutuja
NB! Globaalseid muutujaid tuleb vältida nii palju kui võimalik. Kasutades lokaalseid muutujaid funktsioonis või plokis, kaitseme neid väliste kõrvalmõjude eest.
Viitadest (pointers) ja viidetest (references)
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
Viited (references)
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). Kuidas saab viidamuutuja abil viidatavalt mäluaadressilt andmeid? Siin on abiks operaator *
.
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.
Sõltumatud viited (independent references)
On võimalik defineerida viide, mis on tavaline muutuja. Seda nimetatakse sõltumatuks viiteks. Sõltumatu viite loomisel antakse objektile lisaks teine nimi (alias).
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! Sõltumatu 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 peameetodisse.
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 originaale e 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 |