Obsługa wyjątków w OOP
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 81 z 104.
Obsługa wyjątków w C++ zapewnia uporządkowany sposób wykrywania błędów czasu wykonania i reagowania na nie. W programowaniu obiektowym wyjątki naturalnie integrują się z hierarchiami klas, umożliwiając tworzenie znaczących typów błędów, które zawierają kontekst dotyczący tego, co poszło nie tak.
Podstawowy mechanizm wykorzystuje trzy słowa kluczowe: try, throw i catch. Kod, który może zawieść, umieszcza się w bloku try, błędy sygnalizuje się za pomocą throw, a bloki catch obsługują określone typy wyjątków:
#include <iostream>
#include <stdexcept>
class BankAccount {
double balance;
public:
BankAccount(double b) : balance(b) {}
void withdraw(double amount) {
if (amount > balance) {
throw std::runtime_error("Insufficient funds");
}
balance -= amount;
}
double getBalance() const { return balance; }
};
int main() {
BankAccount account(100.0);
try {
account.withdraw(150.0);
} catch (const std::runtime_error& e) {
std::cout << "Error: " << e.what() << "\n";
}
std::cout << "Balance: " << account.getBalance() << "\n";
}Biblioteka standardowa C++ udostępnia hierarchię klas wyjątków, której podstawą jest std::exception. Do często używanych klas pochodnych należą std::runtime_error, służąca do obsługi problemów występujących w czasie działania, oraz std::logic_error, służąca do obsługi błędów programistycznych. Wszystkie standardowe wyjątki udostępniają metodę what(), która zwraca komunikat o błędzie.
Gdy zostaje zgłoszony wyjątek, stos jest rozwijany — destruktory są wywoływane dla wszystkich obiektów lokalnych, gdy program szuka pasującego bloku catch. Zapewnia to prawidłowe sprzątanie nawet w przypadku wystąpienia błędów, dlatego RAII i obsługa wyjątków tak dobrze ze sobą współpracują.
Wyzwanie
ŁatwyZbudujmy bezpieczny kalkulator dzielenia, który demonstruje obsługę wyjątków w kontekście obiektowym. Utworzysz klasę Calculator, która wykonuje operacje dzielenia i zgłasza odpowiednie wyjątki, gdy coś pójdzie nie tak, a następnie prawidłowo obsłużysz te wyjątki w programie głównym.
Podzielisz kod na trzy pliki:
Calculator.h: Zdefiniuj klasęCalculator, która wykonuje operacje matematyczne z odpowiednią obsługą błędów.Twoja klasa
Calculatorpowinna mieć metodędivide(double numerator, double denominator), która zwraca wynik dzielenia. Jeśli jednak mianownik wynosi zero, metoda powinna zgłosić wyjątekstd::runtime_errorz komunikatemDivision by zero. Jeśli licznik lub mianownik jest ujemny, powinna zgłosić wyjątekstd::invalid_argumentz komunikatemNegative values not allowed.Dodaj też metodę
safeSqrt(double value), która zwraca pierwiastek kwadratowy z wartości. Jeśli wartość jest ujemna, zgłoś wyjątekstd::runtime_errorz komunikatemCannot compute square root of negative number. Musisz dołączyć<cmath>, aby użyć funkcjisqrt.Calculator.cpp: Zaimplementuj metody zadeklarowane w pliku nagłówkowym. Pamiętaj, aby dołączyć wymagane nagłówki (<stdexcept>i<cmath>) oraz sprawdzić warunki przed wykonaniem operacji.main.cpp: Wczytaj trzy dane wejściowe (każdą w osobnym wierszu):- Typ operacji:
dividelubsqrt - Pierwsza liczba (double)
- Druga liczba (double): używana tylko przy dzieleniu, ignorowana przy obliczaniu pierwiastka
Utwórz obiekt
Calculatori wykonaj żądaną operację wewnątrz bloku try. Jeśli operacja się powiedzie, wypiszResult: [value]. Jeśli zostanie przechwycony wyjątek, wypiszError: [message], gdzie komunikat pochodzi z metody wyjątkuwhat().Po bloku try-catch (niezależnie od tego, czy wystąpił wyjątek) wypisz
Calculation complete, aby pokazać, że program kontynuuje działanie po obsłużeniu wyjątków.- Typ operacji:
Na przykład, dla danych wejściowych divide, 10 i 2:
Result: 5
Calculation completeDla danych wejściowych divide, 10 i 0:
Error: Division by zero
Calculation completeDla danych wejściowych divide, -5 i 2:
Error: Negative values not allowed
Calculation completeDla danych wejściowych sqrt, 16 i 0:
Result: 4
Calculation completeDla danych wejściowych sqrt, -9 i 0:
Error: Cannot compute square root of negative number
Calculation completeZwróć uwagę, jak program prawidłowo obsługuje błędy, przechwytując wyjątki i wyświetlając zrozumiałe komunikaty, a następnie kontynuuje działanie. Ten wzorzec pozwala zachować przejrzystą obsługę błędów i skupić główną logikę programu na typowym przebiegu działania.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Calculator.h"
using namespace std;
int main() {
// Wczytaj dane wejściowe
string operation;
double num1, num2;
cin >> operation;
cin >> num1;
cin >> num2;
// Utwórz obiekt Calculator
Calculator calc;
// TODO: Użyj bloku try-catch, aby wykonać operację
// - Jeśli operation to "divide", wywołaj calc.divide(num1, num2)
// - Jeśli operation to "sqrt", wywołaj calc.safeSqrt(num1)
// - Jeśli operacja się powiedzie, wypisz "Result: ", a następnie wynik
// - Przechwyć wyjątki i wypisz "Error: ", a następnie komunikat wyjątku (użyj what())
// - Po bloku try-catch zawsze wypisz "Calculation complete"
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów11Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieMixiny z użyciem CRTPIdiom PimplWymazywanie typówKlasy enum i silne typowanieObsługa wyjątków w OOPWłasne hierarchie wyjątków3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online