Institute of Computer Science
  1. Courses
  2. 2019/20 spring
  3. Programming in C++ (MTAT.03.158)
ET
Log in

Programming in C++ 2019/20 spring

  • Pealeht
  • Praktikumid
    • Lahenduste esitamine
    • Tulemused
  • Eksamiajad
  • Juhendid
  • Viited

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 vectori 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;
}
  • Institute of Computer Science
  • Faculty of Science and Technology
  • University of Tartu
In case of technical problems or questions write to:

Contact the course organizers with the organizational and course content questions.
The proprietary copyrights of educational materials belong to the University of Tartu. The use of educational materials is permitted for the purposes and under the conditions provided for in the copyright law for the free use of a work. When using educational materials, the user is obligated to give credit to the author of the educational materials.
The use of educational materials for other purposes is allowed only with the prior written consent of the University of Tartu.
Terms of use for the Courses environment