27.2 — Podstawowa obsługa wyjątków

W poprzedniej lekcji na temat potrzeba wyjątków, rozmawialiśmy o tym, jak użycie kodów powrotnych powoduje pomieszanie przepływu sterowania i przepływu błędów, ograniczając oba. Wyjątki w C++ są implementowane przy użyciu trzech słów kluczowych, które działają ze sobą w połączeniu: throw, try, I catch.

Zgłaszanie wyjątków

W prawdziwym życiu używamy sygnałów przez cały czas, aby odnotować, że miały miejsce określone zdarzenia. Na przykład podczas futbolu amerykańskiego, jeśli zawodnik dopuścił się faulu, sędzia rzuci flagę na ziemię i gwizdnie zakończenie gry. Następnie kara jest naliczana i wykonywana. Po wykonaniu kary gra zazwyczaj wraca do normalnego stanu.

W C++ instrukcja throw służy do sygnalizowania, że ​​wystąpił wyjątek lub przypadek błędu (pomyśl o rzuceniu flagi kary). Sygnalizacja wystąpienia wyjątku jest również powszechnie nazywana zgłaszaniem wyjątku.

Aby użyć instrukcji rzutu, po prostu użyj słowa kluczowego rzut, po którym następuje wartość dowolnego typu danych, której chcesz użyć do zasygnalizowania wystąpienia błędu. Zazwyczaj tą wartością będzie kod błędu, opis problemu lub niestandardowa klasa wyjątku.

Oto kilka przykładów:

throw -1; // wrzuć dosłowną wartość całkowitą
throw ENUM_INVALID_INDEX; // wrzuć wartość wyliczeniową
throw "Can not take square root of negative number"; // wrzuć dosłowny ciąg znaków w stylu C (const char*)
throw dX; // wrzuć podwójną zmienną, która została wcześniej zdefiniowana
throw MyException("Fatal Error"); // Rzuć obiekt klasy MyException

Każda z tych instrukcji działa jako sygnał, że wystąpił jakiś problem, który wymaga obsługi.

Szukanie wyjątków

Zgłaszanie wyjątków to tylko jedna część procesu obsługi wyjątków. Wróćmy do naszej analogii z futbolem amerykańskim: co się dzieje, gdy sędzia rzuci flagę karną? Gracze zauważają, że nałożono karę i przerywają grę. Normalny przebieg meczu piłkarskiego zostaje zakłócony.

W C++ używamy słowa kluczowego try , aby zdefiniować blok instrukcji (zwany try block). Blok try pełni rolę obserwatora wyszukującego wyjątki zgłoszone przez którąkolwiek z instrukcji w bloku try.

Oto przykład bloku try:

try
{
    // Instrukcje, które mogą zgłaszać wyjątki, które chcesz obsłużyć, idź tutaj
    throw -1; // oto trywialna instrukcja rzutu
}

Zauważ, że blok try nie definiuje JAK mamy zamiar obsłużyć wyjątek. Mówi jedynie programowi: „Hej, jeśli którekolwiek ze stwierdzeń w tym bloku try zgłasza wyjątek, chwyć go!”.

Obsługa wyjątków

Na koniec koniec naszej analogii z futbolem amerykańskim: po zarządzeniu kary i zakończeniu gry sędzia ocenia karę i ją wykonuje. Innymi słowy, kara musi zostać rozpatrzona, zanim będzie można wznowić normalną grę.

Właściwie obsługa wyjątków jest zadaniem bloku(-ów) catch. Słowo kluczowe catch służy do definiowania bloku kodu (zwanego catch block), który obsługuje wyjątki dla pojedynczego typu danych.

Oto przykład bloku catch, który będzie przechwytywać wyjątki w postaci liczb całkowitych:

catch (int x)
{
    // Obsłuż tutaj wyjątek typu int
    std::cerr << "We caught an int exception with value" << x << '\n';
}

Bloki Try i bloki catch współpracują ze sobą — blok try wykrywa wszelkie wyjątki zgłaszane przez instrukcje w bloku try i kieruje je do bloku catch z pasującym typem do obsługi. Po bloku try musi znajdować się co najmniej jeden blok catch bezpośrednio po nim, ale może istnieć wiele bloków catch wymienionych w kolejności.

Gdy wyjątek zostanie przechwycony przez blok try i skierowany do pasującego bloku catch w celu obsługi, wyjątek uznaje się za obsłużony. Po wykonaniu pasującego bloku catch wykonywanie jest wznawiane normalnie, zaczynając od pierwszej instrukcji po ostatnim bloku catch.

Parametry catch działają podobnie jak parametry funkcji, przy czym parametr jest dostępny w kolejnym bloku catch. Wyjątki typów podstawowych można przechwytywać według wartości, natomiast wyjątki typów innych niż podstawowe należy wychwytywać poprzez odwołanie do stałej, aby uniknąć tworzenia niepotrzebnej kopii (a w niektórych przypadkach zapobiec przecinaniu).

Podobnie jak w przypadku funkcji, jeśli parametr nie będzie używany w bloku catch, nazwę zmiennej można pominąć:

catch (double) // uwaga: brak nazwy zmiennej, ponieważ nie używamy jej w bloku catch poniżej
{
    // Obsługa wyjątku typu double tutaj
    std::cerr << "We caught an exception of type double\n";
}

Może to pomóc w uniknięciu ostrzeżeń kompilatora o nieużywanych zmienne.

W przypadku wyjątków nie jest wykonywana żadna konwersja typu (więc wyjątek int nie zostanie przekonwertowany w celu dopasowania bloku catch z parametrem double).

Łączenie rzutu, próby i złapania

Oto pełny program, który wykorzystuje bloki rzutu, try i wielu bloków catch:

#include <iostream>
#include <string>

int main()
{
    try
    {
        // Instrukcje, które mogą zgłaszać wyjątki, które chcesz obsłużyć, idź tutaj
        throw -1; // oto trywialny przykład
    }
    catch (double) // brak nazwy zmiennej, ponieważ nie używamy samego wyjątku w bloku catch poniżej
    {
        // Wszelkie wyjątki typu double zgłoszone w powyższym bloku try zostaną wysłane tutaj
        std::cerr << "We caught an exception of type double\n";
    }
    catch (int x)
    {
        // Wszelkie wyjątki typu int zgłoszone w powyższym bloku try zostaną wysłane tutaj
        std::cerr << "We caught an int exception with value: " << x << '\n';
    }
    catch (const std::string&) // złap klasy przez odwołanie do const
    {
        // Wszelkie wyjątki typu std::string zgłoszone w powyższym bloku try zostaną wysłane tutaj
        std::cerr << "We caught an exception of type std::string\n";
    }

    // Wykonywanie jest kontynuowane tutaj po obsłużeniu wyjątku przez którykolwiek z powyższych bloków catch
    std::cout << "Continuing on our merry way\n";

    return 0;
}

Na komputerze autora uruchomienie powyższego bloku try/catch daje następujący wynik:

We caught an int exception with value -1
Continuing on our merry way

Rzut Instrukcja została użyta do zgłoszenia wyjątku o wartości -1, który jest typu int. Instrukcja rzutu została następnie przechwycona przez otaczający blok try i skierowana do odpowiedniego bloku catch, który obsługuje wyjątki typu int. Ten blok catch wyświetlił odpowiedni komunikat o błędzie.

Po obsłużeniu wyjątku program po blokach catch działał normalnie, wyświetlając „Kontynuuj wesołą podróż”.

Podsumowanie obsługi wyjątków

Obsługa wyjątków jest w rzeczywistości dość prosta i poniższe dwa akapity opisują większość tego, co należy o tym pamiętać:

Kiedy zostanie zgłoszony wyjątek (używając throw), działający program znajduje najbliższy blok obejmujący try (w razie potrzeby propagując stos w celu znalezienia otaczającego bloku try - omówimy to bardziej szczegółowo w następnej lekcji), aby sprawdzić, czy którakolwiek z procedur obsługi catch jest dołączona do try block może obsłużyć tego typu wyjątek. Jeżeli tak, wykonanie przeskakuje na początek bloku catch, wyjątek uznaje się za obsłużony.

Jeśli w najbliższym otaczającym bloku try nie istnieją odpowiednie procedury obsługi catch, program kontynuuje przeglądanie kolejnych obejmujących bloków try w poszukiwaniu procedury obsługi catch. Jeśli przed zakończeniem programu nie zostaną znalezione żadne odpowiednie procedury obsługi catch, program zakończy się niepowodzeniem i zgłosi błąd wyjątku w czasie wykonywania.

Zauważ, że program nie będzie wykonywał niejawnych konwersji ani promocji podczas dopasowywania wyjątków do bloków catch! Na przykład wyjątek char nie będzie pasował do bloku int catch. Wyjątek int nie będzie pasował do bloku float catch. Jednakże zostaną wykonane rzuty z klasy pochodnej na jedną z jej klas nadrzędnych.

To naprawdę wszystko, co w tym temacie. Pozostała część tego rozdziału zostanie poświęcona pokazaniu przykładów działania tych zasad.

Wyjątki są obsługiwane natychmiast

Oto krótki program, który pokazuje, jak natychmiast obsługiwane są wyjątki:

#include <iostream>

int main()
{
    try
    {
        throw 4.5; // zrzuć wyjątek typu double
        std::cout << "This never prints\n";
    }
    catch (double x) // obsługa wyjątku typu double
    {
        std::cerr << "We caught a double of value: " << x << '\n';
    }

    return 0;
}

Ten program jest tak prosty, jak to tylko możliwe. Oto co się dzieje: instrukcja rzutu jest pierwszą wykonywaną instrukcją — powoduje to zgłoszenie wyjątku typu double. Wykonanie natychmiast przechodzi do najbliższego otaczającego go bloku try, który jest jedynym blokiem try w tym programie. Następnie sprawdzane są procedury obsługi połowów, aby sprawdzić, czy którykolwiek z nich pasuje. Naszym wyjątkiem jest typ double, dlatego szukamy modułu obsługi catch typu double. Mamy taki, więc się wykonuje.

W rezultacie wynik tego programu jest następujący:

We caught a double of value: 4.5

Zauważ, że „To nigdy nie jest drukowane” nigdy nie jest drukowane, ponieważ wyjątek spowodował, że ścieżka wykonania natychmiast przeskoczyła do procedury obsługi wyjątków dla deblerów.

Bardziej realistyczny przykład

Przyjrzyjmy się przykładowi, który nie jest do końca taki akademicki:

#include <cmath> // dla funkcji sqrt()
#include <iostream>

int main()
{
    std::cout << "Enter a number: ";
    double x {};
    std::cin >> x;

    try // Poszukaj wyjątków występujących w bloku try i trasie do dołączone bloki catch
    {
        // Jeśli użytkownik wprowadził liczbę ujemną, jest to warunek błędu
        if (x < 0.0)
            throw "Can not take sqrt of negative number"; // zrzuć wyjątek typu const char*

        // W przeciwnym razie wydrukuj odpowiedź
        std::cout << "The sqrt of " << x << " is " << std::sqrt(x) << '\n';
    }
    catch (const char* exception) // złap wyjątki typu const char*
    {
        std::cerr << "Error: " << exception << '\n';
    }
}

W tym kodzie użytkownik proszony jest o wprowadzenie numeru. Jeśli wprowadzą liczbę dodatnią, instrukcja if nie zostanie wykonana, nie zostanie zgłoszony żaden wyjątek i wypisany zostanie pierwiastek kwadratowy z liczby. Ponieważ w tym przypadku nie zostanie zgłoszony żaden wyjątek, kod znajdujący się w bloku catch nigdy nie zostanie wykonany. Wynik jest mniej więcej taki:

Enter a number: 9
The sqrt of 9 is 3

Jeśli użytkownik wprowadzi liczbę ujemną, zgłaszamy wyjątek typu const char. Ponieważ znajdujemy się w bloku try i zostaje znaleziony pasujący program obsługi wyjątków, kontrola jest natychmiast przekazywana do procedury obsługi wyjątków const char . Wynik jest następujący:

Enter a number: -4
Error: Can not take sqrt of negative number

Do tej pory powinieneś już poznać podstawowe pojęcie o wyjątkach. W następnej lekcji zrobimy jeszcze kilka przykładów, aby pokazać, jak elastyczne są wyjątki.

Co zwykle robią bloki catch

Jeśli wyjątek jest kierowany do bloku catch, jest on uważany za „obsługiwany”, nawet jeśli blok catch jest pusty. Jednak zazwyczaj będziesz chciał, aby bloki catch zrobiły coś pożytecznego. Istnieją cztery typowe działania bloków catch, gdy przechwytują wyjątek:

Po pierwsze, bloki catch mogą wypisać błąd (do konsoli lub do pliku dziennika), a następnie pozwolić na wykonanie funkcji.

Po drugie, bloki catch mogą zwrócić wartość lub kod błędu z powrotem do osoby wywołującej.

Po trzecie, blok catch może zgłosić kolejny wyjątek. Ponieważ blok catch znajduje się poza blokiem try, nowo zgłoszony wyjątek w tym przypadku nie jest obsługiwany przez poprzedni blok try — jest obsługiwany przez następny obejmujący blok try.

Po czwarte, blok catch w funkcji main() może zostać użyty do wyłapania błędów krytycznych i zakończenia programu w czysty sposób.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
108 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze