W dowolnym miejscu programu może być zgłoszony (wysłany) wyjątek (ang. throwing lub raising an exception). Często nawet nie wiemy, że funkcja biblioteczna, którą wywołujemy, może to zrobić. Lepiej jednak o tym wiedzieć. Autor takiej funkcji bibliotecznej nie znał naszego programu, więc nie mógł wiedzieć, jak obsłużyć daną sytuację wyjątkową. Aby nie przerywać programu, zgłasza więc wyjątek, a użytkownik korzystający z tej funkcji wiedząc, że taki wyjątek może zostać przez funkcję zgłoszony, sam definiuje sposób jego obsługi. Widzimy zatem, że mechanizm obsługi wyjątków daje możliwość pewnego rodzaju komunikacji między różnymi fragmentami kodu, być może pochodzącymi z różnych modułów i napisanych przez różnych autorów.
W funkcjach, które sami piszemy, możemy określić, kiedy i jaki wyjątek zostanie zgłoszony. Wyjątek może być opisany obiektem dowolnego typu, klasowego lub wbudowanego. Zazwyczaj tworzy się specjalne klasy, których obiekty będą opisywać wyjątki. Równie dobrze jednak wyjątek może być opisany po prostu liczbą lub napisem.
Wyjątek jest zgłaszany za pomocą operatora throw:
throw excpt;gdzie excpt może być obiektem dowolnego typu, również wbudowanego, jak int czy double. W momencie zgłoszenia wyjątku normalny przebieg programu jest przerywany i poszukiwana jest procedura obsługi danego wyjątku (czyli odpowiednia fraza catch, o czym powiemy w następnym podrozdziale). Jeśli taka procedura zostanie znaleziona, to wyjątek uważa się za obsłużony i wykonywana jest treść procedury. Nie ma automatycznego powrotu do miejsca zgłoszenia wyjątku!
Jeśli taka procedura nie zostanie znaleziona w funkcji, w której ta sytuacja wyjątkowa miała miejsce, to przepływ sterowania opuszcza kod funkcji, a ramka stosu związana z jej wywołaniem jest usuwana (stos jest „zwijany”). Oznacza to, między innymi, że zmienne lokalne zdefiniowane w tej funkcji są bezpowrotnie tracone. Dla usuwanych lokalnych zmiennych obiektowych wywoływane są, co bardzo ważne, ich destruktory. Jeśli funkcja była rezultatowa, to wartość zwracana jest nieokreślona i wobec tego bezużyteczna. Na tej samej zasadzie poszukiwanie procedury obsługi jest następnie kontynuowane w funkcji wywołującej. Jeśli i tam nie zostanie znaleziona, to i ta funkcja przerywa swoje działanie i jej ramka wywołania na stosie jest też zwijana. W ten sposób mamy dwie możliwości:
W poniższym programie podstawiamy funkcję
termin
zamiast
domyślnej
terminate
(linia 18):
1. #include <iostream> 2. #include <cmath> // sqrt 3. #include <cstdlib> // exit 4. #include <exception> 5. using namespace std; 6. 7. void termin() { 8. cout << "termin: exit(7)" << endl; 9. exit(7); 10. } 11. 12. double Sqrt(double x) { 13. if (x < 0) throw "x < 0"; 14. return sqrt(x); 15. } 16. 17. int main() { 18. set_terminate(&termin); 19. 20. double z, x; 21. 22. x = 16; 23. z = Sqrt(x); 24. cout << "Sqrt(" << x << ")=" << z << endl; 25. 26. x = -16; 27. z = Sqrt(x); 28. cout << "Sqrt(" << x << ")=" << z << endl; 29. }
cpp> g++ -pedantic-errors -Wall -o term term.cpp cpp> ./term Sqrt(16)=4 termin: exit(7) cpp> echo $? 7W tym przykładzie każde powstanie wyjątku kończy się przerwaniem programu (tyle że w sposób cywilizowany). Zwykle jednak nie o to nam chodzi: chcielibyśmy przechwytywać wyjątki i sami decydować, czy i jak program ma być kontynuowany.
T.R. Werner, 21 lutego 2016; 20:17