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; // throw a literal integer value
throw ENUM_INVALID_INDEX; // throw an enum value
throw "Can not take square root of negative number"; // throw a literal C-style (const char*) string
throw dX; // throw a double variable that was previously defined
throw MyException("Fatal Error"); // Throw an object of class MyExceptionKaż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 a 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
{
// Statements that may throw exceptions you want to handle go here
throw -1; // here's a trivial throw statement
}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)
{
// Handle an exception of type int here
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) // note: no variable name since we don't use it in the catch block below
{
// Handle exception of type double here
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
{
// Statements that may throw exceptions you want to handle go here
throw -1; // here's a trivial example
}
catch (double) // no variable name since we don't use the exception itself in the catch block below
{
// Any exceptions of type double thrown within the above try block get sent here
std::cerr << "We caught an exception of type double\n";
}
catch (int x)
{
// Any exceptions of type int thrown within the above try block get sent here
std::cerr << "We caught an int exception with value: " << x << '\n';
}
catch (const std::string&) // catch classes by const reference
{
// Any exceptions of type std::string thrown within the above try block get sent here
std::cerr << "We caught an exception of type std::string\n";
}
// Execution continues here after the exception has been handled by any of the above catch blocks
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; // throw exception of type double
std::cout << "This never prints\n";
}
catch (double x) // handle exception of type 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> // for sqrt() function
#include <iostream>
int main()
{
std::cout << "Enter a number: ";
double x {};
std::cin >> x;
try // Look for exceptions that occur within try block and route to attached catch block(s)
{
// If the user entered a negative number, this is an error condition
if (x < 0.0)
throw "Can not take sqrt of negative number"; // throw exception of type const char*
// Otherwise, print the answer
std::cout << "The sqrt of " << x << " is " << std::sqrt(x) << '\n';
}
catch (const char* exception) // catch exceptions of type 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.

