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)
- teab, mis on muutuja eluiga (lifetime) ja skoop (scope)
- oskab manipuleerida tekstifailidega
- oskab genereerida juhuarve
- oskab koostada mitmest failist koosnevaid programme ja neid kompileerida
- teab, kuidas organiseerida koodi mitmesse faili
Sisukord
Mitmest failist koosnev programm | Päisefailid | Projektifailid mitmes kaustas |
Enesetestid |
Eeldefineeritud funktsioonid
Eeldefineeritud funktsioonid on keeles C++ organiseeritud eraldi teekidesse. Toome siin väikese tabeli enamkasutatavatest funktsioonidest. Eraldi on välja toodud päisefaili nimi, funktsiooni eesmärk, parameetrite ja tulemuse tüübid.
Funktsioon | Päisefail | Eesmärk | Parameetrite tüübid | Tulemuse tüüp |
---|---|---|---|---|
floor(x) , alates C++11 ka floorf , floorl | <cmath> | Tagastab suurima täisarvu, mis ei ole x -st suurem, nt floor(25.85) = 25.00 , floor(-2.85) = -3.00 | float , double , long double | float , double , long double |
ceil(x) , alates C++11 ka ceilf , ceill | <cmath> | Tagastab vähima täisarvu, mis ei ole x -st väiksem, nt ceil(25.85) = 26.00 , ceil(-2.85) = -2.00 | float , double , long double | float , double , long double |
pow(x, y) , alates C++11 ka powf , powl | <cmath> | Tagastab x^y, nt pow(5, 3) = 125 , pow(5, -3) = 0.008 | float , double , long double | float , double , long double |
sqrt(x, y) , alates C++11 ka sqrtf , sqrtll | <cmath> | Tagastab ruutjuure x-st, nt sqrt(23.34) = 4.83115 | float , double , long double | float , double , long double |
isalpha(x) | <cctype> | Tagastab nullist erineva täisarvu, kui x on tähemärk ja vastasel juhul 0, nt isalpha('A') = 1 , isalpha (';') = 0 | char | int |
isdigit(x) | <cctype> | Tagastab nullist erineva täisarvu, kui x on number ja vastasel juhul 0, nt isdigit('4') = 1 , isdigit('b') = 0 | char | int |
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.
Koostame programmi käsurea argumentide kuvamiseks ekraanile ja salvestame faili main.cpp
#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; }
Kompileerime ja käivitame programmi käsurealt koos argumentidega:
Keskkonnas Clion
saab käsurea argumente sisestada menüüst Run -> Edit Configurations -> Program arguments
. Sisestame sinna "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 deklaratsiooni nimetatakse ka funktsiooni prototüübiks.
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 nimede jaoks soovitatakse kasutada camelCase
vormingut, st nimi algab väikese tähega ja mitmesõnalistes nimedes algavad järgmised sõnad suurte tähtedega, kusjuures tühikut vahele ei panda:
float arvutaKeskmine(float arv1, float arv2); // camelCase
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 |
Muutujate skoop (scope) ja eluiga (life time)
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.
Töö tekstifailidega
Failid on C++ kontekstis vood (stream), nagu ka std::cout
, std::cin
ja ka näiteks std::stringstream
. Failide kasutamiseks on päis <fstream>
.
Märkus: Failide päis sisaldab kolme tüüpi faile: ainult sisendiks (ifstream
), ainult väljundiks (ofstream
) ja nii sisendiks kui väljundiks (fstream
) mõeldud faile. Kasutame materjalis vaid fstream
tüüpi failiobjekte.
Faili avamine
Faili avamiseks piisab uue failiobjekti loomisest:
int main() { std::fstream fail("fail.txt"); }
Näites oleme loonud failiobjekti nimega fail
, mis avab faili nimega fail.txt
.
Kui avatav fail ei ole määratud absoluutse asukohana, üritatakse faili avada relatiivselt programmi töökataloogist.
CLioni korral on töökataloogiks cmake-build-debug
. Soovi korral on võimalik see jooksutuskonfiguratiooni sätetest ära muuta (vt Working Directory). VSCode'i puhul on töökataloog avatud kaust ning käsurealt jooksutades kaust, millest programm jooksutati.
Paneme tähele, et kui antud faili ei leidu, siis programm faili avamisel sellest veaga teada ei anna. Samamoodi ei anna veateadet (ega ka tulemust) failist lugemine. Seetõttu on vajalik enne failist lugemist kontrollida, kas fail on avatud. Faili avamise kontroll:
int main() { fstream fail("fail.txt"); if (fail.is_open()) { cout << "Fail 'fail.txt' leiti!\n"; } else { cout << "Faili 'fail.txt' ei leitud!\n"; cout << "Error: " << strerror(errno) << '\n'; } return 0; } | Faili 'fail.txt' ei leitud! Error: No such file or directory |
Siin kontrollime, kas fail fail
on avatud. Kui fail pole avatud, saame tegeliku vea (faili ei leidu, on lukus vmt) saamiseks kasutada funktsiooni strerror(errno)
, mis annab viimase juhtunud vea kohta sõnalise veateate. Funktsiooni strerror(errno)
kasutamiseks on vajalik päise <cstring>
olemasolu.
Paneme tähele, et näites ei ole faili suletud. Nimelt, C++ failid suletakse automaatselt skoobi lõppedes. See tähendab seda, et kui jõuame main
funktsiooni lõpus olevale loogelisele sulule, lõpeb antud skoop ning fail suletakse. Küll aga on hea tava failid käsitsi sulgeda, kui nendega enam midagi ei tehta. Selleks saab kasutada failiobjektil funktsiooni close
.
Failist lugemine
Primitiivseid tüüpe (nt int
, double
) ja muid sisseehitatud tüüpe (nt string
) on võimalik üsna mugavalt failist sisse lugeda. Küll aga eeldab see kindlat teadmist, mis struktuuriga antud fail on.
Vaatame näidet:
Olgu faili fail.txt sisu järgmine:
100 2.0 tere päevast! siin on ka teine rida ja kolmas! ja tühi rida ka
Järgmises näites loeme failist andmeid muutujatesse. Failist saame muutujaid sisse lugeda sarnaselt standardsisendist lugemisele operaatoriga >>
. Programmis on käsk return EXIT_FAILURE
, mis vea korral tagastab nullist erineva arvu, täpsemalt arvu 1.
int main() { fstream fail("fail.txt"); if (fail.is_open()) { cout << "Fail 'fail.txt' leiti!\n"; } else { cout << "Faili 'fail.txt' ei leitud!\n"; cout << "Error: " << strerror(errno) << '\n'; return EXIT_FAILURE; } int x; double y; string z; fail >> x >> y >> z; cout << "Sain failist:\n"; cout << "\ttäisarvu " << x << '\n'; cout << "\tujukomaarvu " << y << '\n'; cout << "\tsõne " << z << '\n'; fail.close(); return 0; } | Fail 'fail.txt' leiti! Sain failist: täisarvu 100 ujukomaarvu 2 sõne tere |
Sisse loetud tekstis on ainult sõne "tere"
. Seda seetõttu, et >>
operaatoriga lugemine loeb ainult järgmise tühikuni.
Failist lugemine reakaupa
Mingites olukordades on parem lugeda failist reakaupa. Vaatame näidet:
string rida; while (getline(fail, rida)) { cout << "Lugesin failist rea: '" << rida << "'\n"; } | Lugesin failist rea: '100 2.0 tere päevast!' Lugesin failist rea: 'siin on ka teine rida' Lugesin failist rea: 'ja kolmas!' Lugesin failist rea: '' Lugesin failist rea: 'ja tühi rida ka' |
Näites tühi rida on loetud eraldi ning sõned ei sisalda reavahetusi (\n
).
Faili sisu lugemine korraga
Seni toodud näidetes lugesime failist nn "jooksvalt". Failist on võimalik lugeda ka kogu sisu korraga.
Märkus: Suurte failide puhul ei ole see kõige parem, kuna kogu fail loetakse korraga mällu (reakaupa lugemise näite korral loetakse vaid üks rida korraga).
Kogu faili lugemiseks peame esmalt teada saama faili suuruse. NB! Suurus on baitides! Saame seda teha järgmiselt:
Siin on soovitatav kasutada võtmesõna auto
, et lasta kompilaatoril muutujate algus ja lõpp tüüp automaatselt määrata.
Märkus: Nüüd failist uuesti lugemise jaoks on vaja faili algusesse tagasi sõita. Uuri, kuidas kasutada funktsiooni seekg
, et minna tagasi faili algusesse. Mis saab siis, kui faili algusesse tagasi ei minda ja üritatakse failist lugeda (nt üritame lugeda ühe täisarvu)?
Selle koodi jooksutamisel saame väljundisse kirje, et faili sisu suurus on 75 baiti.
Märkus: Kogu faili lugemiseks kasutame veel õppimata kontseptsiooni: võtmesõnad new
ja delete
. Nende teemadeni jõuame tulevikus, aga lühidalt teeme allolevas näites märgimassiivi suurusega failiSuurus
, loeme kogu faili sisu massiivi ja pärast kustutame loodud massiivi ära.
NB! Võtmesõna delete
ei saa kasutada varasemalt õpitud massiivide kasutamisel.
int main{ fstream fail("fail.txt"); if (fail.is_open()) { cout << "Fail 'fail.txt' leiti!\n"; } else { cout << "Faili 'fail.txt' ei leitud!\n"; cout << "Error: " << strerror(errno) << '\n'; return EXIT_FAILURE; } auto algus = fail.tellg(); fail.seekg(0, ios::end); auto lopp = fail.tellg(); auto failiSuurus = lopp - algus; //baitide arv cout << "Faili suurus: " << failiSuurus << '\n'; fail.seekg(0); // tagasi faili algusse char *failiSisu = new char[failiSuurus]; fail.read(failiSisu, failiSuurus); cout << "Faili kogu sisu on '" << failiSisu << "'\n"; delete[]failiSisu; return 0; } | Fail 'fail.txt' leiti! Faili suurus: 75 Faili kogu sisu on '100 2.0 tere paevast! siin on ka teine rida ja kolmas! ja tuhi rida ka' |
Nüüd on kõik reavahetused säilinud.
NB! Probleeme võivad tekitada täpitähed. Antud näites on täpitähed failis asendatud tavaliste märkidega.
Faili salvestamine
Faili salvestamine töötab analoogselt standardäljundisse (std::cout
) saatmisele. Järgmises näites on vajalik päis <fstream>
ja fail uusfail.txt
peab olema olemas.
int main() { fstream fail("uusfail.txt"); if (fail.is_open()) { string s{"Selle sisu paneme\nfaili uusfail.txt."}; fail << s; fail.close(); } else{ cout << "Probleem faili avamisega\n"; } return 0; }
Juhul, kui fail uusfail.txt
oli enne programmi täitmist tühi, siis faili sisu peale programmi täitmist on järgmine:
Selle sisu paneme faili uusfail.txt.
Antud programmiga kirjutatakse fail algusest alates üle. Eelnev info failis, mida üle ei kirjutatud, säilib.
Täpsemalt saab uurida aadressil https://en.cppreference.com/w/cpp/io/basic_fstream
Faili loomine
Uue faili loomiseks piisab vaid väljundfaili objekti loomisest:
int main() { ofstream fail("test.txt"); // kindlasti ofstream fail << "olen uus fail\n"; return 0; }
Kindlasti oleks vaja kontrollida, kas antud fail sai loodud. Kasuta selleks failiobjektil funktsiooni is_open
.
Faili ülekirjutamine
Faili üle kirjutamiseks tuleb väljundfail avada kindlas režiimis.
int main() { ofstream fail("uusfail.txt", ios::out | ios::trunc); fail << "olen uus fail\n"; }
Paneme tähele, et teise argumendi konstrueerimisel on kasutatud üht püstjoont. Tegemist on bitioperatsiooniga või
.
Uuri lähemalt, mida antud režiimid ios::out
ja ios::trunc
tähendavad.
Päisefail <filesystem>
Failihaldusega käib käsikäes päisefail <filesystem>
, kus on mitmeid failidega töötamise tüüpe ja funktsioone.
Vaatame näitena funktsiooni file_size
:
int main() { auto suurus = filesystem::file_size("fail.txt"); cout << "Faili 'fail.txt' suurus on " << suurus << " baiti.\n"; return 0; }
Antud programmi väljund annab meile samuti tulemuseks 75 baiti. Nüüd saime faili suuruse teada ilma faili avamata.
Täpsemalt saab <filesystem>
võimalusi uurida aadressil
https://en.cppreference.com/w/cpp/filesystem
Juhuarvude genereerimine
Esmalt tutvume C-keelest pärit juhuslike täisarvude genereerimisega. Teegis <cstdlib>
on funktsioon rand
, mis genereerib juhusliku täisarvu vahemikus 0..RAND_MAX
. Genereerime kuus täisarvu vahemikus 0..RAND_MAX
ja kuus täisarvu vahemikus 5 .. 15
, kus otspunktid on kaasa arvatud. Viimasel juhul sobiva tulemuse saamiseks kasutame jagamise jääki ja nihutamist rand()%11 + 5
. Mõned näited juhuslike täisarvude arvutamisest vahemiku järgi
arv1 = rand() % 100; // arv1 vahemikus 0..99 arv2 = rand() % 100 + 1; // arv2 vahemikus 1..100 arv3 = rand() % 40 + 1985; // arv3 vahemikus 1985..2024
Tulemus võiks olla näiteks selline:
#include <iostream> #include <cstdlib> using namespace std; int main() { cout << "Juhuarvud vahemikus 0 .. RAND_MAX:" << "\n"; for (int i = 0; i < 6; ++i) { cout << rand() << " "; } cout << "\nJuhuarvud vahemikus 5 .. 15:" << "\n"; for (int i = 0; i < 6; ++i) { cout << rand()%11 + 5 << " "; } return 0; } | Juhuarvud vahemikus 0 .. RAND_MAX: 41 18467 6334 26500 19169 15724 Juhuarvud vahemikus 5 .. 15: 10 15 6 5 12 12 |
Kui seda programmi käivitada mitu korda, siis iga kord saame samad arvud. Selle vältimiseks kasutatakse funktsiooni srand
, mille abil saab genereerijale ette anda nn "seemne", et funktsioon tagastaks igal käivitamisel uued juhuarvud. Seemneks sobib funktsiooni time
(teegist <ctime>
) tagastatud hetkeaeg, mis on igal käivitusel erinev.
#include <iostream> #include <cstdlib> #include <ctime> using namespace std; int main() { srand(time(0)); cout << "Juhuarvud vahemikus 0 .. RAND_MAX:" << "\n"; for (int i = 0; i < 6; ++i) { cout << rand() << " "; } cout << "\nJuhuarvud vahemikus 5 .. 15:" << "\n"; for (int i = 0; i < 6; ++i) { cout << rand()%11 + 5 << " "; } return 0; } | Esimene kord käivitades: Juhuarvud vahemikus 0 .. RAND_MAX: 41 18467 6334 26500 19169 15724 Juhuarvud vahemikus 5 .. 15: 10 15 6 5 12 12 Teine kord käivitades: Juhuarvud vahemikus 0 .. RAND_MAX: 16987 22816 1300 21150 20282 23915 Juhuarvud vahemikus 5 .. 15: 10 10 9 14 13 15 |
NB! Juhuslike arvude genereerimisel rand
abil on mitmeid puudusi, seetõttu ei soovitata seda lähenemist reaalsetes projektides kasutada, vt
https://en.cppreference.com/w/c/numeric/random/rand
Alates C++11
-st on kasutusel teek <random>
, millel on palju rohkem võimalusi: mitmed juhuslike arvude generaatorid, erinevad juhuslike arvude jaotused jne. Toome siin väikese näite:
#include <iostream> #include <random> using namespace std; int main() { default_random_engine genereerija; uniform_int_distribution<int> jaotus(5,15); cout << "Teegi <random> abil genereeritud täisarvud vahemikus 5..15 (ühtlase jaotusega)\n"; for (size_t i = 0; i < 10; ++i) { cout << jaotus(genereerija) << " "; } return 0; } | Teegi <random> abil genereeritud täisarvud vahemikus 5..15 (ühtlase jaotusega) 5 6 13 10 10 7 5 12 12 15 |
Täpsemalt saab <random>
võimalusi uurida aadressil
https://en.cppreference.com/w/cpp/numeric/random
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
Näidisfail nimega liitja.h:
#ifndef LIITJA_H int liitja(int a, int b); #endif
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> ...
Enesetestid
NB! Enesetestides eeldame, et on kasutatud standardnimeruumi (using namespace std;
)