Обработка исключений в C++

C++Beginner
Практиковаться сейчас

Введение

В этом лабораторном занятии (Lab) вы научитесь основным концепциям обработки исключений в C++. Вы начнете с понимания того, как бросать (throw) и перехватывать (catch) базовые исключения, которые представляют собой способ обработки ошибок времени выполнения или непредвиденных ситуаций в вашей программе. Затем вы изучите использование ключевых слов try, catch и throw, а также стандартных классов исключений, таких как std::exception. Кроме того, вы научитесь определять пользовательские классы исключений и реализовывать несколько блоков catch для различных типов исключений. Наконец, вы изучите использование вложенных блоков try-catch для обработки исключений на разных уровнях вашей программы.

Бросание и перехват базовых исключений

На этом этапе вы научитесь основным концепциям обработки исключений в C++, сосредоточившись на том, как бросать (throw) и перехватывать (catch) базовые исключения. Исключения - это способ обработки ошибок времени выполнения или непредвиденных ситуаций в вашей программе.

Откройте WebIDE и создайте новый файл с именем basic_exceptions.cpp в директории ~/project:

touch ~/project/basic_exceptions.cpp

Добавьте следующий код в файл basic_exceptions.cpp:

#include <iostream>
#include <stdexcept>

int divide(int numerator, int denominator) {
    // Throw an exception if denominator is zero
    if (denominator == 0) {
        throw std::runtime_error("Division by zero is not allowed!");
    }
    return numerator / denominator;
}

int main() {
    try {
        // Attempt a normal division
        int result1 = divide(10, 2);
        std::cout << "10 / 2 = " << result1 << std::endl;

        // Attempt division by zero
        int result2 = divide(10, 0);
        std::cout << "This line will not be executed" << std::endl;
    }
    catch (const std::exception& e) {
        // Catch and handle the exception
        std::cout << "Error: " << e.what() << std::endl;
    }

    return 0;
}

Разберем основные компоненты:

  1. Ключевое слово throw:

    • Используется для генерации исключения при возникновении ошибки
    • В этом примере мы бросаем исключение std::runtime_error, когда пытаемся выполнить деление на ноль
  2. Блок try:

    • Содержит код, который может сгенерировать исключение
    • Позволяет выполнять рискованные операции
    • Если возникает исключение, управление программой передается в блок catch
  3. Блок catch:

    • Обрабатывает исключение, брошенное в блоке try
    • Перехватывает исключения определенного типа (в данном случае std::exception)
    • Использует e.what() для получения сообщения об ошибке

Скомпилируйте и запустите программу:

g++ basic_exceptions.cpp -o basic_exceptions
./basic_exceptions

Пример вывода:

10 / 2 = 5
Error: Division by zero is not allowed!

Основные моменты по базовой обработке исключений:

  • Исключения предоставляют способ элегантно обрабатывать ошибки времени выполнения
  • throw генерирует исключение
  • try и catch работают вместе для управления исключительными ситуациями
  • Предотвращает аварийное завершение программы, обеспечивая контролируемую обработку ошибок

Понимание ключевых слов try, catch и throw

На этом этапе вы более детально познакомитесь с тремя ключевыми словами обработки исключений в C++: try, catch и throw. Эти ключевые слова работают вместе, чтобы создать надежный механизм обработки ошибок в ваших программах.

Откройте WebIDE и создайте новый файл с именем exception_keywords.cpp в директории ~/project:

touch ~/project/exception_keywords.cpp

Добавьте следующий код в файл exception_keywords.cpp:

#include <iostream>
#include <string>

// Function that might throw an exception
int processAge(int age) {
    // Throw an exception for invalid age
    if (age < 0) {
        throw std::string("Age cannot be negative");
    }
    if (age > 120) {
        throw std::string("Age is unrealistically high");
    }
    return age;
}

int main() {
    // First try block: handling age validation
    try {
        // Successful case
        int validAge = processAge(25);
        std::cout << "Valid age: " << validAge << std::endl;

        // This will throw an exception
        int invalidAge1 = processAge(-5);
        std::cout << "This line will not be executed" << std::endl;
    }
    catch (const std::string& errorMessage) {
        // Catch block to handle the thrown exception
        std::cout << "Error: " << errorMessage << std::endl;
    }

    // Second try block: another example
    try {
        // This will throw another exception
        int invalidAge2 = processAge(150);
        std::cout << "This line will also not be executed" << std::endl;
    }
    catch (const std::string& errorMessage) {
        std::cout << "Error: " << errorMessage << std::endl;
    }

    return 0;
}

Разберем основные компоненты:

  1. Ключевое слово throw:

    • Используется для генерации исключения, когда выполняется определенное условие
    • Может бросать различные типы объектов (строки, целые числа, пользовательские объекты)
    • Незамедлительно останавливает выполнение текущей функции
  2. Блок try:

    • Содержит код, который может сгенерировать исключение
    • Позволяет выполнять рискованные операции
    • Если возникает исключение, управление программой передается в соответствующий блок catch
  3. Блок catch:

    • Перехватывает и обрабатывает определенные типы исключений
    • Может быть несколько блоков catch для различных типов исключений
    • Предотвращает аварийное завершение программы, элегантно обрабатывая ошибки

Скомпилируйте и запустите программу:

g++ exception_keywords.cpp -o exception_keywords
./exception_keywords

Пример вывода:

Valid age: 25
Error: Age cannot be negative
Error: Age is unrealistically high

Основные моменты о ключевых словах обработки исключений:

  • throw сигнализирует о возникновении ошибки
  • try определяет блок кода, который может сгенерировать исключение
  • catch обрабатывает исключение и предотвращает завершение программы
  • Исключения предоставляют структурированный способ управления ошибками времени выполнения

Использование стандартных классов исключений (std::exception)

На этом этапе вы узнаете о стандартных классах исключений в C++ и о том, как использовать иерархию std::exception для обработки различных типов ошибок времени выполнения. Стандартная библиотека C++ предоставляет набор предопределенных классов исключений, которые охватывают различные сценарии ошибок.

Откройте WebIDE и создайте новый файл с именем standard_exceptions.cpp в директории ~/project:

touch ~/project/standard_exceptions.cpp

Добавьте следующий код в файл standard_exceptions.cpp:

#include <iostream>
#include <stdexcept>
#include <limits>

double divideNumbers(double numerator, double denominator) {
    // Check for division by zero using std::runtime_error
    if (denominator == 0) {
        throw std::runtime_error("Division by zero is not allowed!");
    }
    return numerator / denominator;
}

void checkArrayIndex(int* arr, int size, int index) {
    // Check for out-of-range access using std::out_of_range
    if (index < 0 || index >= size) {
        throw std::out_of_range("Array index is out of bounds!");
    }
    std::cout << "Value at index " << index << ": " << arr[index] << std::endl;
}

int main() {
    try {
        // Demonstrate division by zero exception
        std::cout << "Attempting division:" << std::endl;
        double result = divideNumbers(10, 0);
    }
    catch (const std::runtime_error& e) {
        std::cout << "Runtime Error: " << e.what() << std::endl;
    }

    try {
        // Demonstrate array index out of range exception
        int numbers[] = {1, 2, 3, 4, 5};
        int arraySize = 5;

        std::cout << "\nAccessing array elements:" << std::endl;
        checkArrayIndex(numbers, arraySize, 2);  // Valid index
        checkArrayIndex(numbers, arraySize, 10); // Invalid index
    }
    catch (const std::out_of_range& e) {
        std::cout << "Out of Range Error: " << e.what() << std::endl;
    }

    return 0;
}

Рассмотрим стандартные классы исключений:

  1. std::exception:

    • Базовый класс для всех стандартных исключений
    • Предоставляет виртуальный метод what(), чтобы получить описание ошибки
  2. Общие производные классы исключений:

    • std::runtime_error: Для ошибок времени выполнения, которые могут быть обнаружены только во время выполнения программы
    • std::out_of_range: Когда индекс или итератор находятся вне допустимого диапазона
    • Другие общие классы включают std::logic_error, std::invalid_argument и т.д.

Скомпилируйте и запустите программу:

g++ standard_exceptions.cpp -o standard_exceptions
./standard_exceptions

Пример вывода:

Attempting division:
Runtime Error: Division by zero is not allowed!

Accessing array elements:
Value at index 2: 3
Out of Range Error: Array index is out of bounds!

Основные моменты о стандартных классах исключений:

  • Предоставляют структурированный способ обработки различных типов ошибок
  • Каждый класс исключений служит определенной цели
  • Метод what() возвращает описательное сообщение об ошибке
  • Помогает создавать более надежную и информативную обработку ошибок

Определение пользовательских классов исключений

На этом этапе вы узнаете, как создавать свои собственные пользовательские классы исключений в C++. Пользовательские исключения позволяют определить конкретные типы ошибок, адаптированные к уникальным требованиям вашего приложения.

Откройте WebIDE и создайте новый файл с именем custom_exceptions.cpp в директории ~/project:

touch ~/project/custom_exceptions.cpp

Добавьте следующий код в файл custom_exceptions.cpp:

#include <iostream>
#include <string>
#include <stdexcept>

// Custom exception class for bank account errors
class InsufficientFundsException : public std::runtime_error {
public:
    // Constructor that takes account balance and withdrawal amount
    InsufficientFundsException(double balance, double amount)
        : std::runtime_error("Insufficient funds"),
          currentBalance(balance),
          withdrawalAmount(amount) {}

    // Method to get detailed error information
    double getCurrentBalance() const { return currentBalance; }
    double getWithdrawalAmount() const { return withdrawalAmount; }

private:
    double currentBalance;
    double withdrawalAmount;
};

class BankAccount {
private:
    double balance;

public:
    BankAccount(double initialBalance) : balance(initialBalance) {}

    void withdraw(double amount) {
        // Check if withdrawal amount exceeds current balance
        if (amount > balance) {
            throw InsufficientFundsException(balance, amount);
        }
        balance -= amount;
        std::cout << "Withdrawal successful. Remaining balance: $"
                  << balance << std::endl;
    }

    double getBalance() const { return balance; }
};

int main() {
    try {
        // Create a bank account with initial balance
        BankAccount account(100.0);

        // Attempt a valid withdrawal
        account.withdraw(50.0);

        // Attempt an invalid withdrawal
        account.withdraw(75.0);
    }
    catch (const InsufficientFundsException& e) {
        std::cout << "Error: " << e.what() << std::endl;
        std::cout << "Current Balance: $" << e.getCurrentBalance() << std::endl;
        std::cout << "Attempted Withdrawal: $" << e.getWithdrawalAmount() << std::endl;
    }

    return 0;
}

Разберем пользовательский класс исключений:

  1. Наследование от std::runtime_error:

    • Предоставляет базу для пользовательских классов исключений
    • Позволяет использовать метод what() для получения описания ошибки
  2. Особенности пользовательского исключения:

    • Конструктор с дополнительным контекстом ошибки
    • Методы для получения конкретных деталей ошибки
    • Предоставляет более информативную обработку ошибок

Скомпилируйте и запустите программу:

g++ custom_exceptions.cpp -o custom_exceptions
./custom_exceptions

Пример вывода:

Withdrawal successful. Remaining balance: $50
Error: Insufficient funds
Current Balance: $50
Attempted Withdrawal: $75

Основные моменты о пользовательских классах исключений:

  • Наследуются от стандартных классов исключений
  • Добавляют конкретный контекст ошибки и методы
  • Предоставляют более детальную информацию об ошибке
  • Улучшают обработку ошибок и отладку

Реализация нескольких блоков catch для различных типов исключений

На этом этапе вы узнаете, как обрабатывать несколько типов исключений с помощью разных блоков catch. Этот подход позволяет вам обеспечить конкретную обработку ошибок для различных типов исключений, которые могут возникнуть в вашей программе.

Откройте WebIDE и создайте новый файл с именем multiple_catch.cpp в директории ~/project:

touch ~/project/multiple_catch.cpp

Добавьте следующий код в файл multiple_catch.cpp:

#include <iostream>
#include <stdexcept>
#include <string>

class InvalidAgeException : public std::runtime_error {
public:
    InvalidAgeException(int age)
        : std::runtime_error("Invalid age"), invalidAge(age) {}

    int getInvalidAge() const { return invalidAge; }

private:
    int invalidAge;
};

class Student {
private:
    std::string name;
    int age;

public:
    void setStudent(const std::string& studentName, int studentAge) {
        // Validate name length
        if (studentName.length() < 2) {
            throw std::length_error("Name is too short");
        }

        // Validate age range
        if (studentAge < 0 || studentAge > 120) {
            throw InvalidAgeException(studentAge);
        }

        name = studentName;
        age = studentAge;
    }

    void displayInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Student student;

    // First attempt: Short name
    try {
        student.setStudent("A", 25);
    }
    catch (const std::length_error& e) {
        std::cout << "Length Error: " << e.what() << std::endl;
    }
    catch (const InvalidAgeException& e) {
        std::cout << "Age Error: " << e.what()
                  << " (" << e.getInvalidAge() << ")" << std::endl;
    }

    // Second attempt: Invalid age
    try {
        student.setStudent("John Doe", 150);
    }
    catch (const std::length_error& e) {
        std::cout << "Length Error: " << e.what() << std::endl;
    }
    catch (const InvalidAgeException& e) {
        std::cout << "Age Error: " << e.what()
                  << " (" << e.getInvalidAge() << ")" << std::endl;
    }

    // Successful attempt
    try {
        student.setStudent("Alice", 20);
        student.displayInfo();
    }
    catch (const std::length_error& e) {
        std::cout << "Length Error: " << e.what() << std::endl;
    }
    catch (const InvalidAgeException& e) {
        std::cout << "Age Error: " << e.what()
                  << " (" << e.getInvalidAge() << ")" << std::endl;
    }

    return 0;
}

Разберем несколько блоков catch:

  1. Несколько блоков catch:

    • Позволяют обрабатывать различные типы исключений
    • Выполняются в порядке от наиболее конкретного к наиболее общему
    • Каждый блок может обрабатывать определенный тип исключения
  2. Стратегия обработки исключений:

    • Первый блок catch обрабатывает std::length_error
    • Второй блок catch обрабатывает InvalidAgeException
    • Предоставляет конкретные сообщения об ошибке для разных сценариев

Скомпилируйте и запустите программу:

g++ multiple_catch.cpp -o multiple_catch
./multiple_catch

Пример вывода:

Length Error: Name is too short
Age Error: Invalid age (150)
Name: Alice, Age: 20

Основные моменты о нескольких блоках catch:

  • Обрабатывают разные типы исключений отдельно
  • Предоставляют конкретную обработку ошибок для каждого исключения
  • Порядок имеет значение при перехвате исключений
  • Позволяет более детально управлять ошибками

Использование вложенных блоков try-catch

На этом этапе вы узнаете, как использовать вложенные блоки try-catch для обработки сложных сценариев ошибок и обеспечения более детальной обработки исключений. Вложенные блоки try-catch позволяют обрабатывать исключения на разных уровнях вашего кода.

Откройте WebIDE и создайте новый файл с именем nested_exceptions.cpp в директории ~/project:

touch ~/project/nested_exceptions.cpp

Добавьте следующий код в файл nested_exceptions.cpp:

#include <iostream>
#include <stdexcept>
#include <string>

class FileReadError : public std::runtime_error {
public:
    FileReadError(const std::string& filename)
        : std::runtime_error("Error reading file"), fileName(filename) {}

    std::string getFileName() const { return fileName; }

private:
    std::string fileName;
};

class DataProcessor {
public:
    void processFile(const std::string& filename) {
        try {
            // Simulate file reading
            if (filename.empty()) {
                throw FileReadError("Empty filename");
            }

            std::cout << "Reading file: " << filename << std::endl;

            try {
                // Simulate data processing
                validateData(filename);
            }
            catch (const std::runtime_error& e) {
                std::cout << "Inner try-catch: Data validation error" << std::endl;
                std::cout << "Error details: " << e.what() << std::endl;

                // Rethrow the exception to outer catch block
                throw;
            }
        }
        catch (const FileReadError& e) {
            std::cout << "Outer try-catch: File read error" << std::endl;
            std::cout << "Filename: " << e.getFileName() << std::endl;
        }
        catch (...) {
            std::cout << "Caught unknown exception" << std::endl;
        }
    }

private:
    void validateData(const std::string& filename) {
        // Simulate data validation
        if (filename == "corrupt.txt") {
            throw std::runtime_error("Corrupt file data");
        }
        std::cout << "Data validation successful" << std::endl;
    }
};

int main() {
    DataProcessor processor;

    // Scenario 1: Empty filename
    std::cout << "Scenario 1: Empty Filename" << std::endl;
    processor.processFile("");

    // Scenario 2: Corrupt file
    std::cout << "\nScenario 2: Corrupt File" << std::endl;
    processor.processFile("corrupt.txt");

    // Scenario 3: Valid file
    std::cout << "\nScenario 3: Valid File" << std::endl;
    processor.processFile("data.txt");

    return 0;
}

Разберем вложенные блоки try-catch:

  1. Внешний блок try-catch:

    • Обрабатывает исключения на уровне файла
    • Перехватывает FileReadError и другие потенциальные ошибки
  2. Внутренний блок try-catch:

    • Обрабатывает исключения, специфичные для обработки данных
    • Может повторно бросать исключения в внешний блок catch
  3. Универсальный обработчик (catch (...)):

    • Перехватывает любые неожиданные исключения
    • Предоставляет последний уровень обработки ошибок

Скомпилируйте и запустите программу:

g++ nested_exceptions.cpp -o nested_exceptions
./nested_exceptions

Пример вывода:

Scenario 1: Empty Filename
Outer try-catch: File read error
Filename: Empty filename

Scenario 2: Corrupt File
Reading file: corrupt.txt
Inner try-catch: Data validation error
Error details: Corrupt file data

Scenario 3: Valid File
Reading file: data.txt
Data validation successful

Основные моменты о вложенных блоках try-catch:

  • Предоставляют несколько уровней обработки исключений
  • Позволяют более детально управлять ошибками
  • Можно повторно бросать исключения в внешние блоки catch
  • Полезны для сложных сценариев ошибок

Резюме

В этом практическом занятии вы изучили основные концепции обработки исключений в C++. Вы начали с понимания того, как бросать и перехватывать базовые исключения с использованием ключевых слов throw, try и catch. Вы рассмотрели применение класса исключений std::runtime_error для обработки ошибок деления на ноль. Кроме того, вы узнали, как получить доступ к сообщению об ошибке с помощью функции e.what() внутри блока catch. Эти техники позволяют элегантно обрабатывать ошибки времени выполнения и предотвращать аварийное завершение программы, что позволяет вам создавать более надежные и устойчивые приложения на C++.