MySQL Java JavaScript PHP Python HTML-CSS C-sharp C++ Go

Excepții

Tratamentul excepțiilor

În timpul rulării unui program pot apărea diverse erori. De exemplu, la transmiterea unui fișier prin rețea se poate întrerupe conexiunea sau pot fi introduse date incorecte și nepermise, care pot duce la prăbușirea programului. Astfel de erori se numesc excepții. O excepție reprezintă un obiect temporar de orice tip, care este folosit pentru a semnala o eroare. Scopul unui obiect-excepție este de a transmite informația din punctul în care a apărut eroarea către codul care ar trebui să o trateze. Dacă excepția nu este tratată, atunci programul se închide la apariția acesteia.

De exemplu, în următorul program are loc o împărțire de numere:

#include <iostream>
 
double divide(int a, int b)
{
    return a / b;
}
 
int main()
{
    int x{500};
    int y{};
    double z {divide(x, y)};
 
    std::cout << z << std::endl;
    std::cout << "The End..." << std::endl;
}

Acest program se va compila cu succes, dar în timpul execuției va apărea o eroare deoarece în cod se face împărțire la zero, iar programul se va închide brusc.

Pe de o parte, putem adăuga în funcția divide o verificare și efectua împărțirea doar dacă parametrul b este diferit de 0. Totuși, trebuie oricum să returnăm un rezultat din funcția divide – un anumit număr. Adică nu putem scrie pur și simplu:

double divide(int a, int b)
{
    if (b)
        return a / b;
    else
        std::cout << "Error! b must not be equal to 0" << std::endl;
}

În acest caz trebuie să informăm sistemul despre eroare. Pentru asta se folosește operatorul throw.

Operatorul throw generează o excepție. Prin intermediul lui putem transmite informații despre eroare. De exemplu, funcția divide ar putea arăta astfel:

double divide(int a, int b)
{
    if (b)
        return a / b;
    throw "Division by zero!";
}

Adică, dacă b este 0, se generează o excepție.

Dar această excepție trebuie tratată în codul care apelează funcția divide. Pentru tratarea excepțiilor se folosește construcția try...catch. Are forma următoare:

try
{
    instrucțiuni care pot genera o excepție
}
catch(declarație_excepție)
{
    tratarea excepției
}

În blocul de după cuvântul-cheie try se află codul care poate genera o excepție.

După catch, între paranteze, este un parametru care primește informația despre excepție. Urmează apoi blocul unde are loc tratarea.

Să modificăm întreg codul astfel:

#include <iostream>
 
double divide(int a, int b)
{
    if (b)
        return a / b;
    throw "Division by zero!";
}
 
int main()
{
    int x{500};
    int y{};
     
    try
    {
        double z {divide(x, y)};
        std::cout << z << std::endl;
    }
    catch (...)
    {
        std::cout << "Error!" << std::endl;
    }
    std::cout << "The End..." << std::endl;
}

Codul care poate genera o excepție — apelul funcției divide — este plasat în blocul try.

În blocul catch are loc tratarea. Punctele de suspensie din catch(...) permit tratarea oricărei excepții.

Astfel, când se execută linia:

double z {divide(x, y)};

Se va genera o excepție, așa că instrucțiunile următoare din blocul try nu vor fi executate, iar controlul va fi transferat către catch, unde se afișează un mesaj de eroare. După blocul catch, programul nu se va închide brusc, ci își va continua execuția:

Error!
The End...

Totuși, în acest caz știm doar că a apărut o eroare, dar nu și ce fel de eroare. Prin urmare, în parametru la catch putem primi mesajul transmis de throw:

#include <iostream>
 
double divide(int a, int b)
{
    if (b)
        return a / b;
    throw "Division by zero!";
}
  
int main()
{
    int x{500};
    int y{};
     
    try
    {
        double z {divide(x, y)};
        std::cout << z << std::endl;
    }
    catch (const char* error_message)
    {
        std::cout << error_message << std::endl;
    }
    std::cout << "The End..." << std::endl;
}

Cu ajutorul parametrului const char* error_message obținem mesajul transmis de throw și îl afișăm. De ce const char*? Pentru că după throw avem un literal de tip șir de caractere, adică exact const char*. Ieșirea în acest caz:

Division by zero!
The End...

Astfel, putem afla natura exactă a excepției. În mod similar, putem transmite informații și prin alte tipuri, de exemplu, std::string:

throw std::string{"Division by zero!!"};

Și atunci în blocul catch folosim:

catch (std::string error_message)
{
    std::cout << error_message << std::endl;
}

Dacă excepția nu este tratată, se va apela funcția std::terminate() (din modulul <exception>), care, la rândul său, apelează funcția std::abort() (din <cstdlib>), ceea ce duce la închiderea programului.

Există foarte multe funcții în biblioteca standard C++ și în biblioteci externe. Se pune întrebarea: pe care să le plasăm într-un bloc try-catch pentru a evita închiderea neașteptată a programului? În primul rând, documentația funcției (dacă există) poate ajuta. Un alt semnal este cuvântul-cheie noexcept, care indică faptul că o funcție nu va genera excepții. De exemplu:

void print(int argument) noexcept;

Aici indicăm că funcția print() nu va genera niciodată excepții. Așadar, dacă vedem o astfel de funcție, nu este necesar să o plasăm într-un bloc try-catch.

Crearea unui obiect de excepție

În tratarea excepțiilor este important de reținut că atunci când un obiect este transmis operatorului throw, blocul catch primește o copie a acelui obiect. Iar această copie există doar în interiorul blocului catch.

Pentru tipuri primare, precum int, copierea valorii poate să nu afecteze performanța programului. Însă, când este vorba de obiecte de clasă, costurile pot fi mai mari. Prin urmare, în acest caz, obiectele sunt de obicei transmise prin referință, de exemplu:

#include <iostream>
 
double divide(int a, int b)
{
    if (b)
        return a / b;
    throw std::string{"Division by zero!"};
}
  
int main()
{
    int x{500};
    int y{};
     
    try
    {
        double z {divide(x, y)};
        std::cout << z << std::endl;
    }
    catch (const std::string& error_message)    // șirul este transmis prin referință
    {
        std::cout << error_message << std::endl;
    }
    std::cout << "The End..." << std::endl;
}

Tratarea și generarea mai multor tipuri de excepții

Putem genera și trata mai multe situații excepționale. Să presupunem că vrem ca, la împărțire, împărțitorul (al doilea număr) să nu fie mai mare decât deîmpărțitul (primul număr):

#include <iostream>
 
double divide(int a, int b)
{
    if(!b)  // dacă b == 0
    {
        throw 0;
    }
    if(b > a) 
    {
        throw "The second number is greater than the first one";
    }
    return a / b;
}
 
void test(int a, int b)
{
    try
    {
        double result {divide(a, b)};
        std::cout << result << std::endl;
    }
    catch (int code)
    {
        std::cout << "Error code: " << code << std::endl;
    }
    catch (const char* error_message)
    {
        std::cout << error_message << std::endl;
    }
}
  
int main()
{
    test(100, 20);      // 5
    test(100, 0);       // Error code: 0
    test(100, 1000);    // The second number is greater than the first one
}

În funcția divide, în funcție de valoarea lui b, operatorul throw primește fie un număr:

throw 0;

fie un literal de tip șir de caractere:

throw "The second number is greater than the first one";

Pentru a testa funcția divide s-a definit o altă funcție – test, unde apelul către divide() este plasat într-un bloc try...catch. Deoarece putem avea două tipuri de excepții — int și const char*, definim două blocuri catch pentru fiecare tip:

catch (int code)
{
    std::cout << "Error code: " << code << std::endl;
}
catch (const char* error_message)
{
    std::cout << error_message << std::endl;
}

În main, apelăm funcția test cu diverse valori. La:

test(100, 20);

nu apar excepții, iar rezultatul este afișat.

La:

test(100, 0);

se aruncă o excepție int, deci se va trata în:

catch (int code)

La:

test(100, 1000);

b este mai mare ca a, deci se aruncă un const char* și se tratează în:

catch (const char* error_message)

Ieșirea va fi:

5
Error code: 0
The second number is greater than the first one

Poate exista și situația în care o excepție este generată într-un bloc try-catch, dar nu există un bloc catch compatibil:

void test(int a, int b)
{
    try
    {
        double result {divide(a, b)};
        std::cout << result << std::endl;
    }
    catch (const char* error_message)
    {
        std::cout << error_message << std::endl;
    }
}

Aici, dacă se aruncă throw 0;, nu există un catch (int) corespunzător și programul se va închide brusc.

try-catch și destructori

Este important de știut că dacă într-un bloc try se creează obiecte, atunci la apariția unei excepții li se vor apela destructorii. De exemplu:

#include <iostream>
 
class Person
{
public:
    Person(std::string name) :name{ name }
    {
        std::cout << "Person " << name << " created" << std::endl;
    }
    ~Person()
    {
        std::cout << "Person " << name << " deleted" << std::endl;
    }
    void print()
    {
        throw "Print Error";
    }
private:
    std::string name;
};
 
int main()
{
    try
    {
        Person tom{ "Tom" };
        tom.print();    // aici se aruncă excepția
    }
    catch (const char* error)
    {
        std::cerr << error << std::endl;
    }
}

Clasa Person are un destructor care afișează un mesaj. În funcția print se aruncă o excepție.

În main, în blocul try se creează un obiect Person, apoi se apelează print, care generează excepția. Înainte ca controlul să ajungă în catch, destructorul obiectului este apelat automat. Ieșirea va fi:

Person Tom created
Person Tom deleted
Print Error

Acest comportament garantează eliberarea resurselor chiar și când apar excepții.