Arvutiteaduse instituut
  1. Kursused
  2. 2022/23 kevad
  3. Programmeerimine keeles C++ (LTAT.03.025)
EN
Logi sisse

Programmeerimine keeles C++ 2022/23 kevad

  • Pealeht
  • 1. Muutujad ja andmetüübid
  • 2. Keele põhikonstruktsioonid I
  • 3. Keele põhikonstruktsioonid II
  • 4. Funktsioonimallid, failitöötlus
  • 5. OOP I Klassid
  • 6. OOP II Pärilus ja polümorfism
  • 7. Kontrolltöö 1?

Seitsmendal nädalal toimub 1. kontrolltöö

7.1 1. kontrolltöö näide?
  • 9. Dünaamiline mäluhaldus II
  • 10. Klassimallid
  • 11. STL andmestruktuurid I
  • 12. STL andmestruktuurid II
  • 13. Erindite töötlemine
  • 14. Täiendavad teemad
  • 15. Kontrolltöö 2?

Viieteistkümnendal nädalal toimub 2. kontrolltöö

15.1 2. kontrolltöö näide?
  • 16. Projekti esitlus?
  • Viiteid
  • Vanad materjalid
  • Praktikumid
  • Juhendid
  • Viited

Erindid

Pärast selle praktikumi läbimist üliõpilane

  • teab, mis on erindid ja millal on neid vaja kasutada
  • oskab defineerida erindeid ja neid vajadusel visata
  • oskab vajadusel erindeid käsitleda

Erindite (exception) töötlemine

C++ keeles on võimalik kasutada erindeid (exceptions) programmi tõrgete ja erandlike olukordade käsitlemiseks. Erindid võimaldavad programmi töö ajal teatud vigu ja tõrkeid tuvastada ja neile reageerida, ilma et programm täielikult katki läheks.

Erindeid saab defineerida kasutades nii standardseid erindeid (nt exception) kui ka kohandatud erindeid (mis on spetsiaalselt loodud programmi vajadustele). Erindite kasutamine toimub järgmiselt:

1. Erindi defineerimine. Näiteks saab defineerida erindi MinuErind, millel on string tüüpi isendimuutuja m_sõnum, järgmiselt:

class MinuErind{
public:
    MinuErind(string sõnum) : m_sõnum{sõnum}{}
    string m_sõnum{};
};    

2. Erindi viskamine. Erindi viskamiseks kasutatakse võtmesõna throw ja erindi objekti. Näiteks saab visata erindi MinuErind järgmiselt:

if (y == 0) {
  throw MinuErind("Jagamine nulliga!");
}   

3. Erindi käsitlemine. Erindi käsitlemiseks kasutatakse try-catch plokki. try plokis määratakse kood, mis võib tekitada erindeid ja catch plokis määratakse, kuidas erinditele reageerida. catch plokki nimetatakse ka püüniseks. Näiteks saab käsitleda erindit MinuErind järgmiselt:

try{
    if (y == 0){
        throw MinuErind("jagamine nulliga!");
    }
}
catch (MinuErind& e){
    cout << "Viga: " << e.m_sõnum << '\n';
}

Paneme nüüd kogu koodi kokku:

#include <iostream>
#include <string>
using namespace std;
class MinuErind {
public:
    MinuErind(string sõnum) : m_sõnum{sõnum} {}

    string m_sõnum{};
};

int main() {
    int x{1}, y{}, z{};
    try {
        if (y == 0) {
            throw MinuErind("jagamine nulliga!");
        }
        z = x / y;
    }
    catch (MinuErind& e) {
        cout << "Viga: " << e.m_sõnum << '\n';
    }
    return 0;
}
Viga: jagamine nulliga!

Käsk throw võib visata ka lihtsalt täisarvu. Oluline on, et leiduks catch plokk, kus täisarv kinni püütakse. Järgnev näide illustreerib programmi täitmise järjekorda, kus erindite püüdmine on mitmel tasandil. Funktsioon f1() pöördub funktsiooni f2() poole, see omakorda f3() poole, kus visatakse erind ja püütakse ka kinni.

#include <iostream>

using namespace std;

void f1(); // Funktsioonide deklaratsioonid
void f2();

void f3();

void erindiviskaja() {
    cout << "erindiviskaja() alustab\n";
    throw 0; // 
    cout << "erindiviskaja() lõpetab\n";
}

int main() {
    try {
        f1();
        cout << "main peale f1() täitmist\n";
    } catch (int ex) {
        cout << "Erindi töötlemine main()-is\n";
    }
    cout << "main() lõpetab\n";
    return 0;
}
// Definitsioonid

void f1() {
    cout << "f1() alustab\n";
    try {
        f2();
        cout << "f1()-s peale f2() täitmist\n";
    } catch (int ex) {
        cout << "Erindi töötlemine f1()-s\n";
    }
    cout << "f1() lõpetab\n";
}

void f2() {
    cout << "f2() alustab\n";
    try {
        f3();
        cout << "f2()-s peale f3() täitmist\n";
    } catch (int ex) {
        cout << "Erindi töötlemine f2()-s\n";
    }
    cout << "f2() lõpetab" << endl;
}

void f3() {
    cout << "f3() alustab" << endl;
    try {
        erindiviskaja();
        cout << "f3()-s peale erindiviskamist\n";
    } catch (int ex) {
        cout << "Erindi töötlemine f3()-s\n";
    }
    cout << "f3() lõpetab" << endl;
}
f1() alustab
f2() alustab
f3() alustab
erindiviskaja() alustab
Erindi töötlemine f3()-s
f3() lõpetab
f2()-s peale f3() täitmist
f2() lõpetab
f1()-s peale f2() täitmist
f1() lõpetab
main peale f1() täitmist
main() lõpetab

Funktsioonis f3() peale erindiviskamist (erindiviskaja()) läheb juhtimine catch-plokki ja käsku cout << "f3()-s peale erindiviskamist\n" ei täideta. Peale seda täidetakse catch plokile järgnev käsk. Kuna erind on töödeldud, siis funktsioonis f2() jätkub töö normaalselt, st käsust peale f3() poole pöördumist, analoogiliselt funktsioonides f1() ja main().

Erindeid ei pea kohe viskamise juures töötlema, vaid selle võib jätta hilisemaks. Muudame funktsiooni f3() järgmiselt:

void f3() {
    cout << "f3() alustab" << endl;
    erindiviskaja();
    cout << "f3()-s peale erindiviskamist\n";
    cout << "f3() lõpetab" << endl;
}  

Nüüd funktsioonis f3() erindit ei töödelda, see delegeeritakse pöördujale, st funktsioonile f2(). Funktsiooni töö tulemuseks on

f1() alustab
f2() alustab
f3() alustab
erindiviskaja() alustab
Erindi töötlemine f2()-s
f2() lõpetab
f1()-s peale f2() täitmist
f1() lõpetab
main peale f1() täitmist
main() lõpetab

Kui erindi töötlejat ei leita, siis programm lõpetab töö veaga. Kui eemaldada erinditöötlus ka funktsioonidest f2(), f1() ja main(), siis programm lõpetab veateatega

 terminate called after throwing an instance of 'int'   

Mõnikord on vaja püünises (catch-plokis) visata uuesti sama erind. Seda saab teha operaatoriga throw. Järgmises näites on erindiklassiks klass näita, kus on konstruktor, koopiakonstruktor ja destruktor; lisaks staatiline väli number, mille abil loetakse klassist tehtud objekte (ka koopiaid). Funktsioon loenda on rekursiivne ja erind visatakse parameetri n = 1 korral. Püünises visatakse uuesti sama erind operaatoriga throw.

#include <exception>
#include <iostream>
#include <string>

using namespace std;

/// Näitab objektide loomist (konstruktor) ja hävitamist (destruktor)
class näita {
public:
    // konstruktor
    näita(string const &mis) : m_id{number}, m_mis{mis} {
        print(" näita konstruktor ");
        ++number;
    }
// koopiakonstruktor
    näita(näita const &ex) : m_id{number}, m_mis{ex.m_mis} {
         print("näita koopiakonstruktor ");
        ++number;
    }
// destruktor
    ~näita() {
        print("~ näita destruktor");
    }
    void print(string const &  silt) const{
        cout << silt << " (" << m_mis << ": " << m_id << ")\n";
    }
private:
    static int number; // loendur, mitmes objekt tehakse
    int m_id;
    string m_mis;
};
int näita::number = 1; //staatilise välja algväärtustamine
void loenda(int n) {
    cout << "algab loenda(" << n << ")\n";
    try {
        if (n == 1)
            throw näita(" exception ");
        else if (n > 0)
            loenda(n - 1);
    }
    catch (näita& ex) { // edastatakse viite abil
        cout << "catch n = " << n << '\n';
        throw;  // viskab uuesti sama erindi
    }
    cout << "loenda lõpp (" << n << ")\n";
}
int main() {
    try {
        loenda(3);
    }
    catch (näita ex) { // edastatakse koopia abil
       ex.print("catch main ");
    }
    cout << "Kõik valmis!\n";
}
algab loenda(3)
algab loenda(2)
algab loenda(1)
 näita konstruktor  ( exception : 1)
catch n = 1
catch n = 2
catch n = 3
näita koopiakonstruktor  ( exception : 2)
catch main  ( exception : 2)
~ näita destruktor ( exception : 2)
~ näita destruktor ( exception : 1)
Kõik valmis!

Seda erindit asutakse püüdma funktsiooni väljakutsete magasinis, kus püünises visatakse iga kord erind uuesti. Funktsioonis loenda on püünises erindi edastamine viite abil catch (näita& ex), aga funktsioonis main koopia abil catch (näita ex), st toimub koopiakonstruktori poole pöördumine ja koopia hiljem hävitatakse. Esialgselt visatud erind kustutatakse alles main funktsioonis.

Kui funktsioonis loenda kasutada erindi edastamist koopia abil catch (näita ex) , siis tehakse edastamiseks erindist iga kord koopia ja püünises see koopia hävitatakse:

algab loenda(3)
algab loenda(2)
algab loenda(1)
 näita konstruktor  ( exception : 1)
näita koopiakonstruktor  ( exception : 2)
catch n = 1
~ näita destruktor ( exception : 2)
näita koopiakonstruktor  ( exception : 3)
catch n = 2
~ näita destruktor ( exception : 3)
näita koopiakonstruktor  ( exception : 4)
catch n = 3
~ näita destruktor ( exception : 4)
näita koopiakonstruktor  ( exception : 5)
catch main  ( exception : 5)
~ näita destruktor ( exception : 5)
~ näita destruktor ( exception : 1)
Kõik valmis!

Seega mõistlik on võimalusel kasutada erindi edastamiseks viidet.

Teegis <exception> on C++ erindite klassid, mida saab erindite töötlemisel kasutada. Toome erindite klassi hierarhiast olulisema osa:

 

Oma erindiklassi saab pärida ka sisseehitatud klassist. Järgmises näites visatakse erind peale negatiivse arvu sisestamist. Programmi jooksutati kaks korda.

#include <exception>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class MinuErind : public exception {
public:
    MinuErind(string sõnum, int arv) : m_sõnum{sõnum}, m_arv{arv} {}
    string getSõnum() {
        return m_sõnum;
    }
    int getArv() {
        return m_arv;
    }
private:
    string m_sõnum{};
    int m_arv{};
};
int main() {
    int x{};
    try {
        cout << "Sisesta positiivne täisarv:\n";
        cin >> x;
        if (x < 0) {
            throw MinuErind("Sisestasid negatiivse arvu!", x);
        }
    }
    catch (MinuErind e) {
        cout << "Püüti MinuErind: " << e.getSõnum() << ": " << e.getArv();
        return 1;
    }
    return 0;
}
Sisesta positiivne täisarv:
2
Process finished with exit code 0

Sisesta positiivne täisarv:
-5
Püüti MinuErind: Sisestasid negatiivse arvu!: -5
Process finished with exit code 1

noexcept

Mõned funktsioonid ei tohiks kunagi erindit tekitada. Näiteks allolevas klassis Afunktsioon getA, mis tagastab täisarvu. Sellisele funktsioonile võib lisada täiendi noexcept. Kui kompilaator teab, et funktsioon ei viska kunagi erindit, loob ta efektiivsema objektikoodi.

class A{
    int m_a{};
public:
    A(int a) : m_a{a}{}
    int getA() noexcept{
        return m_a;
    }
};

Täpsemalt saab erindite kasutamist uurida aadressil

https://en.cppreference.com/w/cpp/language/exceptions

  • Arvutiteaduse instituut
  • Loodus- ja täppisteaduste valdkond
  • Tartu Ülikool
Tehniliste probleemide või küsimuste korral kirjuta:

Kursuse sisu ja korralduslike küsimustega pöörduge kursuse korraldajate poole.
Õppematerjalide varalised autoriõigused kuuluvad Tartu Ülikoolile. Õppematerjalide kasutamine on lubatud autoriõiguse seaduses ettenähtud teose vaba kasutamise eesmärkidel ja tingimustel. Õppematerjalide kasutamisel on kasutaja kohustatud viitama õppematerjalide autorile.
Õppematerjalide kasutamine muudel eesmärkidel on lubatud ainult Tartu Ülikooli eelneval kirjalikul nõusolekul.
Courses’i keskkonna kasutustingimused