Обработка ошибок в C++: try/catch от А до Я – полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему обработка ошибок — это фундамент надежного кода на C++

Любая программа, работающая с внешним миром (файлы, сеть, ввод пользователя), неизбежно сталкивается с исключительными ситуациями. В C++ механизм try/catch является основным инструментом для элегантного и контролируемого управления такими ситуациями. В этой статье мы разберем обработку ошибок в C++ от базовых конструкций до продвинутых техник, включая best practices и подводные камни.

1. Основы: try, catch и throw

Механизм исключений в C++ строится на трех ключевых словах: try, catch и throw.

1.1. Блок try и catch

Блок try содержит код, который может выбросить исключение. Блок catch ловит это исключение и обрабатывает его.

#include <iostream>
#include <stdexcept>

int main() {
    try {
        // Код, который может вызвать ошибку
        int age = -5;
        if (age < 0) {
            throw std::invalid_argument("Возраст не может быть отрицательным");
        }
        std::cout << "Возраст: " << age << std::endl;
    }
    catch (const std::invalid_argument& e) {
        std::cerr << "Ошибка: " << e.what() << std::endl;
    }
    return 0;
}

1.2. Типы исключений

В C++ можно выбрасывать исключения любого типа: числа, строки, объекты классов. Однако рекомендуется использовать стандартные классы исключений из <stdexcept> или создавать свои собственные классы, унаследованные от std::exception.

  • Стандартные исключения: std::runtime_error, std::logic_error, std::out_of_range и др.
  • Пользовательские исключения: создаются через наследование от std::exception.
class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Мое собственное исключение";
    }
};

1.3. Множественные catch-блоки

Вы можете перехватывать разные типы исключений в отдельных блоках catch. Порядок важен: более конкретные исключения должны идти первыми.

try {
    // код
} catch (const std::invalid_argument& e) {
    // обработка invalid_argument
} catch (const std::exception& e) {
    // обработка всех остальных исключений из std
} catch (...) {
    // catch-all: ловит все, что не поймали выше
}

2. Продвинутые техники: стек вызовов и RAII

2.1. Раскрутка стека (Stack Unwinding)

Когда выбрасывается исключение, C++ автоматически уничтожает все локальные объекты на пути от точки throw до соответствующего catch. Этот процесс называется раскруткой стека. Он гарантирует вызов деструкторов для всех объектов, созданных в блоках try.

class Resource {
public:
    Resource() { std::cout << "Ресурс создан\n"; }
    ~Resource() { std::cout << "Ресурс уничтожен\n"; }
};

void riskyFunction() {
    Resource res;
    throw std::runtime_error("Что-то пошло не так");
}

int main() {
    try {
        riskyFunction();
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    // Вывод: Ресурс создан -> Ресурс уничтожен -> Что-то пошло не так
}

2.2. RAII (Resource Acquisition Is Initialization)

RAII — это ключевая идиома C++, которая тесно связана с обработкой ошибок. Идея: управление ресурсами (память, файлы, мьютексы) привязывается к времени жизни объекта. Деструктор автоматически освобождает ресурс, даже при исключении.

  • Умные указатели: std::unique_ptr, std::shared_ptr автоматически удаляют память.
  • Файлы: std::ifstream / std::ofstream закрывают файл в деструкторе.
  • Блокировки: std::lock_guard освобождает мьютекс.
#include <memory>
#include <fstream>

void processFile() {
    std::ifstream file("data.txt");  // RAII: файл закроется автоматически
    auto ptr = std::make_unique<int>(42); // RAII: память освободится
    
    if (!file.is_open()) {
        throw std::runtime_error("Не удалось открыть файл");
    }
    // Деструкторы умных указателей и файлов вызовутся при раскрутке стека
}

3. Best Practices: как писать безопасный и читаемый код

3.1. Исключения — только для исключительных ситуаций

Не используйте исключения для управления обычным потоком программы (например, для выхода из цикла). Это дорого и усложняет чтение кода. Для обычных проверок используйте if/else или std::optional.

3.2. Спецификация noexcept

Ключевое слово noexcept указывает, что функция не выбрасывает исключений. Это позволяет компилятору генерировать более эффективный код и служит документацией.

void safeFunction() noexcept {
    // Этот код гарантированно не выбросит исключение
}

void riskyFunction() noexcept(false) {
    // Может выбросить
}

3.3. Перехват по константной ссылке

Всегда перехватывайте исключения по константной ссылке (const std::exception&), чтобы избежать срезки объекта и лишнего копирования.

3.4. Не глотайте исключения

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

Блоги

Рекомендаций по книгам