Programmeerimine keeles C++
Praktikum 3 – mallid, C++ standardteegi konteinerid, iteraatorid
Funktsioonimallid
Funktsioonimallid (function templates) võimaldavad taaskasutada funktsiooni koodi mitme erineva andmetüübi jaoks. Vaatame näidet, mis tagastab kahest antud elemendist väiksema.
template <typename T> T getMin (T a, T b) { return ((a < b) ? a : b ); }
Antud näites defineeritakse funktsioon, mis leiab väiksema kahest ette antud väärtusest, kusjuures ainus nõue andmetüübile on võrdluse <
olemasolu. Kui näiteks meie kodutöödest tuntud Line2
klassil selline võrdlus defineerida (näiteks pikkuse järgi), siis saaks funktsioonile getMin
anda argumentidena kaks sirglõiku ning tagastataks lühem. Funktsiooni kutsutaks sellisel juhul välja nii:
Line2 l1, l2; Line2 theShorterOne = getMin <Line2> (l1, l2);
Pane tähele, et malli parameetriks ei pea olema ainult andmetüübid. Funktsioone ja klasse võib algväärtustada ka teiste väärtustega. Näiteks võime kirjutada funktsiooni, mis korrutab sisendeid etteantud täisarvudega.
template <int multiplier> int doMultiplication (int a) { return (a * multiplier); } int a = 6; int b = doMultiplication<7> (a); // b väärtuseks saab 42
Antud näide on toodud intuitsiooni loomiseks. Reaalsuses soovitan selliseid võimalusi kasutada abstraktsioonide loomiseks ning koodi kordamise vältimiseks. Jäta meelde, et väärtuseid ja andmetüüpe saab mallides kasutada läbisegi.
NB! Pange tähele, et mallide kasutamisel kirjutatakse realisatsioonid päistesse! Kompilaator genereerib spetsiifilised realisatsioonid kompileerimise ajal etteantud malli põhjal. Kui selgub, et mõni mall on parametriseeritud andmetüübiga, mis ei vasta klassi nõuetele, näiteks proovime kasutada funktsiooni getMin
andmetüübil, millel puudub võrdlustehe, annab kompilaator veateate. Lisaks – pange tähele, et päiseid ei pea kompileerima! Lõpuks – kui kõik kood õnnestub paigutada päistesse (mallide puhul vahel saab), siis ei peagi teegifaili tegema.
Pikemalt loe funktsioonimallide kohta: http://www.cplusplus.com/doc/tutorial/functions2/
Klassimallid
Klassimallid (class templates) annavad võimaluse klassi koodi kohandada erinevat tüüpi objektide töötlemiseks. Illustreerime seda kahe näitega C++ standardteegist. Konteinerklass vector
salvestab etteantud tüüpi andmeid muudetava pikkusega massiivi. Klass stack
realiseerib pinu andmestruktuuri.
std::vector<std::string> laused; // defineerime vektori sõnedest std::string la = "Aias sadas saia.“; laused.push_back (la); // lause lisatakse vektori lõppu std::string rida = laused[0]; // loeme vektori esimese sõne std::stack<int> arvudepinu; // defineerime pinu täisarvude jaoks int a = 7; arvudepinu.push(a); // lükkame pinusse täisarvu int b = arvudepinu.top (); // teisendust pole vaja, kompilaator teab tüüpi
Noolsulgudes antud andmetüüp määrab, millise andmetüübiga antud konteineri instants töötab. Näiteks ei ole võimalik pinusse arvudepinu
lisada sõnesid. Kompilaator saab definitsioonist teada andmetüübi ning genereerib selle põhjal klassimallist stack
täisarvude töötlemiseks mõeldud versiooni. Sellise konteineriga töötades ei ole vaja teha tüübiteisendusi, sest kompilaator teab, millist andmetüüpi kasutada. Järgneb näide tüübiparameetriga klassimallist.
template <class T> // malli parameetriks on punkti tüüp class Line { // punkti tüübi abil defineerime sirglõigu public: T p1; // lõigu esimene tipp T p2; // lõigu teine tipp Line () = default; // T vaikekonstruktorid kutsutakse välja "nähtamatult" Line (T np1, T np2) // parameetritega konstruktor : p1 (np1) , p2 (np2) {} float length () { // sirglõigu pikkuse arvutamine return p1.distanceFrom (p2); // punktil peab olema meetod } };
See mall esitab parameetrina antud punkti tüübile järgmised nõuded: punktil peab olema vaikekonstruktor, omistamiskäsk ja meetod distanceFrom
, mis tagastab punkti kauguse teisest samasugusest punktist. Malli võiks kasutada näiteks nii:
Line<Point2> l = Line<Point2>{p1, p2}; // sirglõik kahemõõtmelises ruumis.
Loe klassimallide kohta: http://www.cplusplus.com/doc/tutorial/templates.html
Iteraatorid C++ standardteegis
Iteraator C++ mõistes on objekt, mis viitab mõnele teisele objektile konteineris (konteinerid on näiteks vector
, map
jne). Võib öelda, et iteraator seob omavahel konteineri ja positsiooni selles konteineris. Iteraatorit saab nihutada edasi ja tagasi üle konteineri elementide, mis annab täiendavad võimalused konteineris asuvate elementide töötlemiseks. C++11 standard teeb meie elu lihtsamaks
lubades võtmesõna auto
abil tüübi automaatselt tuletada. Tsükleid saab nüüd teha nii:
vector<float> polynoomiKordajad; // vektor murdarvudest (polünoomi kordajad) polynoomiKordajad.push_back (1.7); // lisame vektorisse ... // mingi hulga murdarve polynoomiKordajad.push_back (-5.1); // push_back paneb need vektori lõppu for (float kordaja : polynoomiKordajad) { // tsükkel üle konteineri (C++11) ... // teeme midagi kordajaga } auto it = polynoomiKordajad.begin (); // tuletame iteraatori tüübi while (it != polynoomiKordajad.end ()) { // kuni pole jõutud lõpuni float kordaja = *it; // loeme arvu, mille kohal iteraator ... // hetkel on ja teeme sellega midagi it++; // liigume edasi elementhaaval }
Pange tähele, et tärn ei tähenda automaatselt seda, et iteraator oleks viit! Iteraatori klassil on defineeritud tehe *
, mis tagastab objekti, mille „kohal“ iteraator parajasti on.
Konteineritel on tavaliselt vähemalt kaks meetodit – meetod begin()
tagastab iteraatori, mis viitab konteineri esimesele elemendile ning end()
, mis viitab konteineri lõpule. Iteraatoril on defineeritud kas üks või mõlemad liikumise tehted, +
ja -
. Käsk it += 7
nihutab iteraatorit seitsme elemendi võrra edasi. Tähele tuleb panna seda, et ka iteraatorite kasutamisel ei peata miski programmeerijat „üle ääre“ mineku eest – iteraatorit saab nihutada üle konteineri piiri.
Veel kasutatakse iteraatoreid vahemike määramiseks. Oletame, et me soovime kustutada kordajate vektorist esimesed 5 elementi. Selleks saame kasutada vektori käsku erase
, mille parameetriks on iteraatoritega määratud vahemik.
polynoomiKordajad.erase(polynoomiKordajad.begin(), polynoomiKordajad.begin()+5);
Selline vahemikega töötamine muudab konteinerite kasutamise väga mugavaks. Näiteks ei pea vektori alamhulga eraldamiseks elemente ükshaaval kopeerima. Piisab vahemiku omistamisest:
vector<float> esimesedViis; esimesedViis.assign (polynoomiKordajad.begin(), polynoomiKordajad.begin()+5);
Erinevad konteinerid pakuvad erinevaid võimalusi. Mõni konteiner on iteraatoriga mõlemas suunas läbitav (vector
), mõni ei ole (list
). Seda kõike tuleb programme kirjutades arvesse võtta.
Pikemalt saab konteineritest lugeda siit: http://en.cppreference.com/w/cpp/container
Algoritmid C++ standardteegis
C++ standardteek sisaldab tervet hulka algoritme, mis lihtsustavad tööd konteineritega. Algoritmid ei ole seotud konkreetsete konteineritega (nad ei ole nende klasside meetodid) vaid tuginevad iteraatorite kasutamisele. Algoritmid on defineeritud päises <algorithm>
ning asuvad nimeruumis std
. Hea ülevaate algoritmidest ja nende kasutamisest annab veebileht http://en.cppreference.com/w/cpp/algorithm
Funktsiooniobjektid ehk funktorid
Enne konkreetsete algoritmide vaatamist on kasulik tutvuda veel ühe C++ konstruktsiooniga. Funktsiooniobjektiks nimetame klassi, millel on defineeritud tehe ()
mingi hulga parameetritega. Funktsiooniobjekte kasutatakse algoritmide kohandamiseks konkreetsetele vajadustele. Näiteks toome ühe funktsiooniobjekti, mida saab kasutada arvude paarituteks ja paarisarvudeks jaotamisel.
class OddEvenPredicate { // defineerime klassi, mis jaotab arve paarituteks public: bool operator() (int i) { // ja paarisarvudeks – paarisarvud on if ((i % 2) == 0) return true; // eespool kui paaritud arvud else return false; } };
Kui ülesande lahendamisel ei ole funktsiooniobjektil vaja olekut, võib teda tihtipeale asendada ka tavalise funktsiooniga. Kuid tasub meeles pidada, et funktsiooniobjekti kasutamine pakub rohkem võimalusi, näiteks ei ole funktsiooni kasutades võimalik lihtsasti olekut säilitada üle mitme väljakutse. Viimast omadust võib vaja minna mitmete standardteegi algoritmide (nt for_each
) kasutamisel.
Näide algoritmide kasutamisest: sorteerimine ja jaotamine
Sorteerimine on üks operatsioone, mida tihti vaja. C++ pakub selleks mitut algoritmi, millest kõige enam kasutatakse ilmselt variante nimedega sort
ja stable_sort
. Erinevus seisneb selles, et teine tagab objektide esialgse järjekorra, kui need on sorteerimise seisukohalt võrdsed. Olgu meil arvud:
vector<int> arvuvektor {32,71,12,45,26,80,53}; // defineerime vektori arvudega
Kui me soovime seda vektorit sorteerida, siis saame seda teha nii:
sort(arvuvektor.begin(), arvuvektor.end()); // tulemus: 12,26,32,45,53,71,80
Algoritmi sort kolmandaks parameetriks saab anda kahe parameetriga funktsiooniobjekti (või siis funktsiooni), mis oskab sorteeritavaid objekte võrrelda.
Proovime nüüd ära kasutada funktsiooniobjekti OddEvenPredicate
. Jaotame jada nii, et paarisarvud on enne paarituid.
partition(arvuvektor.begin(), arvuvektor.end(), OddEvenPredicate ()); // tulemus: 12,26,32,80,45,53,71
Näide λ-avaldiste kasutamisest (C++11)
C++11 võimaldab kasutada ka anonüümseid funktsioone. Nii on võimalik vältida eraldi funktsiooniobjektide tegemist algoritmide jaoks. Vaata järgmist näidist.
partition(arvuvektor.begin(), arvuvektor.end(), [](int x) {return (x % 2 == 0); } ); // tulemus: 12,26,32,80,45,53,71
Näide erindite kasutamisest C++ keeles
Erindite mõte on anda kasutajale teada, et asi on katki. Näiteks mingi teegi kasutamisel võiks panna rakenduses ühte try blokki objekti loomise ja sellega midagi tegemise. C++ erindite mehhanism on mõnevõrra keerukam Java omast. Käesolevas materjalis on toodud vaid üks näide, mida võite rakendada näiteks kodutöös konstruktoris vigade teatamiseks. Kõigepealt erindite viskamine:
Klass::Klass(int n) { if (n <1) { throw string ("Parameeter n oli negatiivne!"); } ... }
Ja nüüd kinnipüüdmine
int n = -1; try { Klass k (n); } catch (string& str) { cout << "Klassi loomine ebaõnnestus – veateade oli: " << str << endl; }
Märkus konstruktorite kohta – () vs {}
Kui tüübil on olemas eraldi initsialiseerimisnimekirjaga konstruktor, siis objekti loomine loogeliste ja ümarate sulgudega võib anda erinevaid tulemusi. Näiteks
vector<int> x (5); // tulemuseks {0, 0, 0, 0, 0} vector<int> y {5}; // tulemuseks {5}
Vaata vector
i kohta lisaks: http://en.cppreference.com/w/cpp/container/vector
Märkus üle konteineri käiva tsükli kohta
Eespool kirjeldatud koodis
for (float kordaja : polynoomiKordajad) { // tsükkel, mis ei tee midagi kordaja += 5; }
kordaja muutmine ei muuda konteineri sees olevate elementide väärtusi, sest kordaja loomisel tehakse koopia. Kui tahta konteineri sisu muuta võib kirjutada näiteks nii
for (float &kordaja : polynoomiKordajad) { // muudame polünoomi kordajaid kordaja += 5; }
Märk &
siin tähistab viidet muutujale ehk ei tehta koopiat vaid viidatakse originaalsele muutujale. Täpsemalt tutvume selle teemaga neljandas praktikumis.
Juhuarvude kasutamise näide (C++11)
#include <random> #include <iostream> int main() { std::mt19937 gen; // Loome juhuarvugeneraatori std::random_device rd; // mittedeterministlike juhuarvude loomiseks gen.seed(rd()); // Kui juhuarvu generaatorit mitte algväärtustada, saame // alati sama juhuarvude jada - kommenteeri testimiseks see rida välja std::uniform_int_distribution<short> dis{1, 5}; // Ühtlane jaotus [1, 5] for (int n = 0; n < 10; ++n) { std::cout << dis(gen) << ' '; // loome juhusliku arvu hulgast [1, 5] } std::cout << std::endl; }