Rozdział 12
Wyjątki
Bł~dy czasu wykonania 12 ~ 1 Program źródłowy przygotowany w języku C++
(pliki z rozszerzeniem .cpp) jest przetwarzany przez system programowania na równoważny program zapisany w języku wewnętrznym komputera. Przekształcenie to zachodzi w dwóch etapach. Najpierw kompilator analizuje program źródłowy zgłaszając wszystkie wykryte błędy leksykalne i składniowe. Są to blędy czasze kompilacji - programista może je usunąć za pomocą edytora tekstów będącego najczęściej składnikiem systemu programowania. Poprawny leksykalnie i składniowo program źródłowy jest przez kompilator przekształcany na równoważny program półskompilowany
j zawarty w plikach o rozszerzeniu .obj. Pliki te są następnie przetwarzane przez program łączący, który dodaje zadeklarowane biblioteki funkcji i klas (zawarte również w plikach .obj). Rezultatem pracy programu łączącego jest gotowy do wykonania przekład wyjściowego programu źródłowego zapisany w pliku o rozszerzeniu .exe. Przekład ten jest następnie wykonywany pod nadzorem systemu operacyjnego.
Podczas wykonania przekładu programu mogą wystąpić blędy czasu ~~ykohaiaia. Błędy te można podzielić na dwie grupy
!i ,i !j
12. 1. Błędy czasu wykonania 2,G~1
• błędy wykrywane sprzętowo - na przykład przekroczenie zakresu liczb zmiennopozycyjnych czy próba odwołania do miejsca pamięci niedostępnego dla wykonywanego programu,
• błędy wykrywane przez oprogramowanie - na przykład przekroczenie zakresu tablicy czy niewłaściwe argumenty przekazywane funkcjom (szczególnie funkcjom bibliotecznym).
Wystąpienie błędu wykrywanego sprzętowo powoduje najczęściej przerwanie wykonywania programu i wyprowadzenie standardowej sygnalizacji systemu operacyjnego opisującej rodzaj błędu. Błędy wykrywane przez oprogramowanie są najczęściej spowodowane przekazywaniem niewłaściwych argumentów funkC~onl. Typowy przykład to przekazanie funkcji bibliotecznej realizującej otwarcie pliku dyskowego w trybie odczytu nazwy pliku, który nie został jeszcze utworzony.
Rozważmy możliwe sposoby reakcji funkcji na błędne argumenty przekazane w jej wywołaniu. Po pierwsze funkcja może zasygnalizować błąd za pomocą wyróżnionej wartości swego wyniku. Często jest to wartość 0 lub wartość -1. Nie zawsze jednak metoda ta daje się zastosować, bowiem niekiedy wartości te mogą być również poprawnymi wynikami funkcji. Druga możliwość sygnalizowania błędów to rozszerzenie listy argumentów funkcji. Do podstawowych argumentów można dodać jeszcze jeden argument typu wskaźnikowego. Może to być na przykład wskaźnik zmiennej typu chat zadeklarowanej w programie źródłowym. Po zakończeniu wykonywania funkcji wartość tej zmiennej sygnalizuje poprawne lub błędne wykonanie (np. wartość 0 - poprawnie, wartość większa od zera numer błędu). Wskaźnik będący dodatkowym arg~nnentem funkcji może też być wskaźnikiem funkcji obsługi błędu, zawartej w programie źródłowym, która ma zostać wywołana w przypadku wykrycia błędu. Taka metoda sygnalizowania błędów czasu wykonania jest bardzo efektywna, ale wymaga od programisty stosowania dosyć skomplikowanych konstrukcji. Jeszcze inne proste rozwiązanie problemu sygnalizowania błędów wykrywanych szczególnie podczas wykonywania funkcji bibliotecznych to wprowadzenie w danej bibliotece funkcji globalnej zmiennej sygnalizującej błędy. Każdorazowo po wykonaniu jednej z funkcji bibliotecznych należy sprawdzić, czy zmienna ta nie sygnalizuje wystąpienia błędu.
Przekazywanie informacji o wykryciu błędu za pomocą zmiennej wymaga każdorazowo po wywołaniu funkcji wstawienia do programu wywołującego instrukcji testujących wartość tej zmiennej. Instrukcje te zwiększają objętość programu i czas jego wykonania. Konsekwentne stosowanie tej metody wymaga od programisty dużej dyscypliny i jest szczególnie niewygodne, gdy na przykład funkcja jest wywoływana wielokrotnie w złożonym wyrażeniu arytmetycznym.
242 Ro=dział 12. Wvjqtki
Z przedstawionych rozwiązań najbardziej efektywna jest metoda przekazywania w wywołaniu funkcji wskaźnika dodatkowej funkcji obsługi błędu. Funkcja obsługi jest bowiem wykonywana tylko wtedy, gdy jest to konieczne, a dodatkoWO Unlkallly wielokrotnego wpisywania instrukcji testujących wystąpienie błędu. Przedstawiona w następnym punkcie metoda obsługi wyjątków, dostępna w języku C++, jest równoważna metodzie przekazywania wskaźnika funkcji obsługi
błędu a z drugiej strony jest znacznie prostsza w zapisie. ;
Obsługa wyjątków 12 ~ f~ Mechanizm obs~ztgi wyjcitków składa się z trzech części:
• zgłoszenia wyjątku, dokonywanego po wykryciu błędu, • sygnalizacji wywołania funkcji mogącej zgłosić wyjątek, • sekcji obsługi wyjątku.
Zgłoszenie wyjątku następuje w definicji funkcji wywoływanej, pozostałe ;,,
i dwie części obsługi wyjątków należy umieścić w funkcji wywołującej.
Instrukcja zgłoszenia wyjątku ma postać:
throw wyrażenie ;
Wyrażenie występujące po słowie kluczowym throw umożliwia sygnalizowanie typu wyjątku. Wartością tego wyrażenia może być wartość dowolnego typu lub obiekt pewnej klasy.
throw 12 ; // wyjątek typu int throw 1.5 ; // wyjątek typu float throw "Błąd" ; // wyjątek typu char* //
class OpisBłędu ;
throw OpisBłędu ( ) ; // wyjatek typu OpisBłędu
12.2. Obsluga wyjqt/ców 243
Wyrażenie określające typ wyjątku może nie występować i wówczas instrukcja zgłoszenia wyjątku sprowadza się do postaci
throw ;
W jednej funkcji można zgłaszać wiele wyjątków różnych typów. float Dzielenie ( float flDzielna, float flDzielnik )
f
if ( flDzielnik != 0 )
return flDzielna / flDzielnik ; else
f throw flDzielnik ; // typ float return 0 ;
// class BrakOdbioru f ?~
class BłądKomunikatu f
public: chat m cSumaKontrolna ; chat m cNagłówek ;
int m_nLiczbaZnaków ;
BłądKomunikatu ( chat cSuma, chat cNagłówek, int nDługość) ( m cSumaKontrolna ) cSuma,
( m cNagłówek ) cNagłówek, (m_nLiczbaZnaków ) nDługc~ć { }
244 Rozdział l2. Wyjątki
'J void OdbierzKomunikat ( char` pcBufor ) {
// oczekiwanie na przybycie komunikatu
if ( CzasMinąłKomunikatuNieOdebrano ( ) ) throw BrakOdbioru ( ) ;
else {
// analiza komunikatu
if ( KomunikatPoprawny ( ) ) return ;
else {
BłądKomunikatu *pOpisBłędu = new OpisBłędu
( TestSumyKontrolnej ( ), TestNagłówka ( ), !' Długc~ćKomunikatu ( ) )
throw pOpisBłędu ; return ;
Funkcja OdbierzKomunikat korzysta z dwóch klas określających typ wyjątku. Klasa BrakOdbioru nie zawiera żadnych składowych - jej pusty obiekt sygnalizuje jedynie rodzaj błędu. Natomiast klasa BłądKomunikatu zawiera trzy składowe, które opisują rodzaj błędu - po utworzeniu obiekt tej klasy jest przekazywany jako wartość wyrażenia w instrukcji throw.
Sygnalizacja wywołania funkcji, która może zgłosić wyjątek ma postać: try { blok }
gdzie blok jest ciągiem deklaracji i instrukcji.
12.2. Obsłztga wyjqfków 245
try {
NiebezpiecznaFunkcja ( ) ; }
ll try {
float flWanośćTransakcji, flCenaJednejSztuki ; int nLiczbaSztuk ;
char acBufor [ 1024 J ; OdbierzKomunikat ( acBufor ) ;
l/ przepisz wartość transakcji i liczbę sztuk do odpowiednich zmiennych flCenaJednejSztuki = Dzielenie ( flWartośćTransakcji, flLiczbaSztuk ) ; }
Sekcja obsługi ma następująca postać: catch ( deklaracją wyjcttku ) { blok }
Sekcja ta rozpoczynająca się od słowa kluczowego catch, może wystąpić jedynie bezpośrednio po zgłoszeniu wywołania funkcji mogącej powodować wyjątek lub po innej sekcji obsługi wyjątku.
try catch (...)
Il
i
2G~6 Rozdział l2. Wyjątki
try catch ( typ, ) catch ( typ2 )
catch ( typ„ )
W nawiasach okrągłych występujących za słowem kluczowym catch wpisywana jest deklaracja uryjdtku, która może być:
(a) trójkropkiem ... i
(b) identyfikatorem dowolnego typu liczbowego lub identyfikatorem klasy (c) argumentem o postaci
' identyfckator_typu ideny ckator argumentu identyfikator klasy identyfikator obiektcc
I W przypadku (a) blok zawarty w sekcji obsługi wyjątków jest wykonywany po wystąpieniu dowolnego wyjątku z bloku sygnalizacyjnego poprzedzającego tę sekcję. Trójkropek służy więc do przechwytywania wszystkich zgłoszonych wyjątków niezależnie od ich typu.
double KwadratElementu ( double adbTablica [ ], int nRozmiar, int nlndeks ) I, {
', if ( nlndeks >= nRozmiar )
throw nlndeks ; // typ int I return 0;
','; I )
12.2. Obsltsga wyjątków 247
if ( adbTablica [ nlndeks ] > MAKSYMALNY_DOPUSZCZALNY ) i
throw anTablica [ nlndeks ] ; // typ double return 0 ;
return adbTablica [ nlndeks ] * adbTablica [ nlndeks ] ;
i
// try i
KwadratElementu ( adbWektor, nDługość, nPozycja ); // następne instrukcje bloku
i
catch (...) i
cout « "\n Oj błąd!" ;
]
// następne instrukcje programu
Każdy z wyjątków zgłoszonych przez funkcję KwadratElementu zostanie obsłużony przez jedyną sekcję obsługi związaną z tym wywołaniem. Niezależnie od tego, czy wyjątek wystąpił i został obsłużony, czy też żaden z wyjątków nie został zgłoszony sterowanie przechodzi do następnych instrukcji bloku, a gdy ich już uie ma do następnych instrukcji programu.
Gdy deklaracja wyjątku jest identyfikatorem typu lub identyfikatorem klasy (przypadek (b)) wyjątki są rozróżniane z dokładnością do typu - nie są natomiast przekazywane wartości wyrażeń występujących w zgłoszeniu wyjątku.
try KwadratElementu ( adbWektor, nDługość, nPozycja );
248 Rosc~iałl2. Wyjątki
catch ( int )
cout « "\n Zbyt duży indeks." ;
)
catch ( double ) i
cout « "\n Zbyt duża wartość elementu." ;
Najbardziej dokładną informację o rodzaju błędu, którego wykrycie powoduje zgłoszenie wyjątku można uzyskać stosując jako deklarację wyjątku argument zawierający określenie typu lub klasy oraz identyfikator argumentu formalnego (wersja (c)).
try KwadratElementu ( anWektor, nDługość, nPozycja ); )
catch ( int nlndeks )
cout « "\n Indeks o wart~ci "
« nlndeks « " przekracza rozmiar wektora. " ;
i
catch ( double dbElement ) i
cout « "\n Podniesienie warta5ci " « dbElement
« " do kwadratu spowoduje przekroczenie zakresu liczb typu double. "
//
12.2. Obsfhga wyjqtków 249
try f
OdbierzKomunikat ( acBufor ) ; i
catch ( BrakOdbioru ) f
cout « "1n Upłyrx~ł zadany czas oczekiwania na odebranie komunikatu." ;
) catch ( BłądKomunikatu *pOpisBłędu ) f
cout « "1n Odebrano bidny komunikat o długości " « pOpisBłędu -> m nLiczbaZnaków ;
if ( pppisBłędu -> m cSumaKontrolna )
cout « "1n Błędna suma kontrolna komunikatu." ; if ( pOpisB~du -> m cNagłowek )
cout « "\n Błędny nagłówek komunikatu." ; delete pOpis~du ;
i
W podanych przykładach obsługa wyjątku sprowadzała się jedynie do wyświetlenia odpowiedniego tekstu. Najczęściej reakcja taka jest niewystarczająca. Nie można bowiem kontynuować obliczeh, gdy na przykład wykryta została próba odwołania do nieistniejącego elementu tablicy. W takich przypadkach wykonanie programu powinno zostać zakończone. Niekiedy możliwe jest zwrócenie się do użytkownika z prośbą o skorygowanie błędnych wartości, ale na poprawną odpowiedź można jedynie liczyć, gdy użytkownikiem jest autor programu.
Rozdzia~ 12. Wyjątki
Hierarchia wyjątków 12.3
Rozważmy ciąg wywołań fiu~kcji FunkcjaPierwsza ~ FunkcjaDruga ~ FunkcjaTrzecia ~ FunkcjaCzwarta
Przyjmijmy, że wywołanie każdej z tych funkcji zawarte jest w bloku rozpoczynającym się od słowa kluczowego try, czyli jest realizowane w ramach sygnalizacji wywołania funkcji, która może zgłosić wyjatek. Gdy FunkcjaCzwarta zgłosi wyjątek pewnego typu, to może on zostać obsłużony w FunkcjiTrzeciej, o ile zai
~~" wiera ona sekcję obsługi wyjątków odpowiedniego typu. Jeżeli takiej sekcji nie ma w FunkcjiTrzeciej, to wyjątek jest przekazywany do obsługi w FunkcjiDrugiej. W ten sposób wyjątek zgłoszony na pewnym poziomie zagnieżdżenia wywołań funkcji jest przekazywany kolejno aż do pierwotnej funkcji wywołującej. Gdy i ta funkcja nie zawiera sekcji obsługi wyjątku danego typu, to najczęściej wykonanie programu jest przerywane i wyświetlany jest stosowny komunikat systemowy.
enum RodzajeWyjątków { PIERWSZY, DRUGI, TRZECI } ; ', void FunkcjaCzwarta ( )
i
throw 1 ; // typ int
throw DRUGI ; // typ RodzajeWyjątków
(, void FunkcjaTrzecia ( )
'' try FunkcjaCzwarta ( ) ; } i
12.3. Kierarchia
catch ( int ) // obsługa wyjątku typu int
throw 'A' ; // typ char
}
void FunkcjaDruga ( )
try {
FunkcjaTrzecia ( ) ;
} catch ( RodzajeWyjątków ) // obsługa wyjątku typu RodzajeWyjątków
} void FunkcjaPierwsza ( ) {
try {
FunkcjaDruga ( ) ; }
catch (...) // obsługa wszystkich dotąd nieobsłużonych wyjątków
251