RAII: ресурс = время жизни объекта
Вопрос с собеседования: что выведет этот код?
$$ $$
#include <iostream>
class Logger {
public:
Logger(const std::string& name) : name_(name) {
std::cout << "Открыл: " << name_ << "\n";
}
~Logger() {
std::cout << "Закрыл: " << name_ << "\n";
}
private:
std::string name_;
};
void doWork() {
Logger log("файл.txt");
std::cout << "Работаем...\n";
throw std::runtime_error("что-то сломалось");
}
int main() {
try {
doWork();
} catch (...) {
std::cout << "Поймали исключение\n";
}
}Вывод:
Открыл: файл.txt
Работаем...
Закрыл: файл.txt
Поймали исключениеМногие ожидают, что строка «Закрыл: файл.txt» не появится — ведь функция вылетела с исключением, а не дошла до конца нормально. Но она появляется. И это не случайность, а суть идиомы, которая держит на себе весь современный C++.
RAII расшифровывается как Resource Acquisition Is Initialization — «получение ресурса есть инициализация». Звучит громоздко, а идея простая: ты привязываешь время жизни ресурса (память, файл, сетевое соединение, мьютекс — что угодно, что нужно потом «освободить») к времени жизни обычного объекта на стеке. Ресурс захватывается в конструкторе. Ресурс освобождается в деструкторе. А деструктор объекта на стеке C++ вызывает всегда, когда объект выходит из области видимости — неважно, дошла функция до конца, вернулась через return раньше, или её прервало исключение.
В примере выше объект log создаётся на стеке функции doWork. Когда внутри функции бросается исключение, начинается процесс, который называется stack unwinding — «раскрутка стека». Программа не просто прыгает к обработчику catch, она аккуратно проходит назад по всем объектам, которые успели создаться на стеке, и вызывает их деструкторы. Поэтому Logger корректно печатает «Закрыл» ещё до того, как управление доходит до catch.
Почему это так важно именно для C++? Потому что в этом языке нет сборщика мусора. В Java или Python за тебя решает GC: когда-то, не факт что сразу, он найдёт неиспользуемый объект и освободит память. C++ на такую роскошь не рассчитывает — здесь ты обязан сам гарантировать, что каждый malloc получит free, каждый new — delete, каждый open() — close(). Раньше это делали руками, и постоянно забывали закрыть файл в одной из веток кода или пропускали delete в obscure edge case с исключением. RAII убирает эту забывчивость из уравнения: если ресурс завёрнут в объект, то освобождение происходит автоматически и его физически невозможно забыть, потому что оно не завязано на то, что программист не забудет дописать строчку — оно завязано на языковую гарантию вызова деструктора.
Именно на этой идее построены все умные указатели, RAII-обёртки для мьютексов (std::lock_guard), файловые потоки (std::ifstream сам закрывает файл в деструкторе) и вообще большая часть стандартной библиотеки C++. Когда на собеседовании спрашивают «почему в C++ не нужен try/finally, как в Java», правильный ответ — RAII: конструктор/деструктор объекта на стеке уже даёт эту гарантию без явного finally.
Важный нюанс, который любят проверять: RAII работает только для объектов, у которых есть время жизни, управляемое языком — то есть объектов на стеке (автоматическое хранение) или полей внутри других объектов. Если ты создал объект через new и никуда его не завернул, RAII тебе не поможет — сырой указатель сам по себе не имеет деструктора, который бы что-то освобождал. Именно поэтому следующий шаг — обернуть такой указатель в умный указатель, который сам является RAII-объектом на стеке.