Проблема: ресурсы нужно освобождать вручную
Программа работает с ресурсами — памятью, файлами, сетевыми соединениями, мьютексами. Каждый ресурс нужно явно освободить, когда он больше не нужен. Забыть — легко.
void processFile(const string& path) {
FILE* f = fopen(path.c_str(), "r");
if (!f) return;
char* buffer = new char[1024];
// ... читаем из файла, парсим ...
if (some_error_condition) {
return; // ← забыли освободить f и buffer!
}
delete[] buffer;
fclose(f);
}
- Забыли освободить. Утечка ресурса: файл остаётся открытым, память занята.
- Исключение. Если между получением и освобождением летит исключение — управление не дойдёт до
fclose. - Двойное освобождение. Если освободили в двух ветках кода — программа может упасть.
Идея RAII в одной фразе
void processFile(const string& path) {
ifstream f(path);
if (!f) return;
vector<char> buffer(1024);
// ... читаем из файла, парсим ...
if (some_error_condition) {
return; // ← f и buffer освободятся автоматически
}
// Никаких fclose, delete[] — всё сделает деструктор
}
Ключевые моменты:
ifstream— объект RAII: при создании открывает файл, при разрушении закрывает.vector<char>— объект RAII: владеет динамическим массивом и освобождает его автоматически.- Никаких явных
fcloseиdelete[]— компилятор гарантирует вызов деструкторов. - Даже если летит исключение — деструкторы будут вызваны.
Демонстрация: две стратегии и одно исключение
Один и тот же сценарий: получаем два ресурса, потом что-то идёт не так и летит исключение. Смотрите, что происходит с ресурсами.
Примеры RAII из стандартной библиотеки
Вы уже используете RAII, возможно, сами того не замечая.
Файлы: ifstream / ofstream
{
ifstream f("data.txt");
string line;
while (getline(f, line)) {
cout << line << endl;
}
} // ← f.close() вызывается автоматически
Память: unique_ptr / shared_ptr
{
auto p = make_unique<int[]>(100);
p[0] = 42;
// ... работаем ...
} // ← delete[] вызывается автоматически
Мьютексы: lock_guard
mutex mtx;
void safeFunction() {
lock_guard<mutex> lock(mtx);
// ... критическая секция ...
} // ← mtx.unlock() вызывается автоматически,
// даже если внутри было исключение
Динамические массивы: vector / string
{
vector<int> v = {1, 2, 3, 4, 5};
string s = "hello";
// ... работаем ...
} // ← вся память освободится автоматически
Пишем свой RAII-класс
Обёртка вокруг C-функции fopen / fclose. При создании объекта
файл открывается, при разрушении — закрывается. Никаких утечек.
class FileHandle {
FILE* fp; // владеем этим ресурсом
public:
// Конструктор ЗАХВАТЫВАЕТ ресурс
FileHandle(const char* path, const char* mode)
: fp(fopen(path, mode)) {}
// Деструктор ОСВОБОЖДАЕТ ресурс
~FileHandle() {
if (fp) fclose(fp);
}
// Копирование ЗАПРЕЩЕНО: иначе два объекта будут закрывать один файл
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Перемещение РАЗРЕШЕНО: владение переходит
FileHandle(FileHandle&& other) noexcept
: fp(other.fp) {
other.fp = nullptr;
}
FILE* get() const { return fp; }
operator bool() const { return fp != nullptr; }
};
- Конструктор захватывает ресурс (или явно ничего не делает, если ресурса нет).
- Деструктор гарантированно освобождает ресурс.
- Копирование запрещено (или реализовано через подсчёт ссылок). Иначе два объекта станут «владельцами» одного ресурса — двойное освобождение.
void readConfig() {
FileHandle f("config.txt", "r");
if (!f) {
cerr << "Не удалось открыть файл" << endl;
return;
}
char line[256];
while (fgets(line, sizeof(line), f.get())) {
// обрабатываем строку
}
} // ← f.~FileHandle() вызывается автоматически, fclose тоже
Правила использования RAII
1. Каждому ресурсу — свой RAII-класс
Память — unique_ptr. Файлы — fstream. Мьютексы —
lock_guard. Соединения с БД — своя обёртка. Не смешивайте ресурсы
в одном классе.
2. Правило пяти / правило нуля
Если ваш класс владеет ресурсом, продумайте все пять операций:
деструктор, конструктор копирования, оператор копирования, конструктор перемещения,
оператор перемещения. Лучший вариант — правило нуля: не владеть
ресурсом напрямую, а использовать уже готовые RAII-обёртки (unique_ptr,
vector), и тогда ни один из пяти специальных методов писать не нужно.
3. Исключения в деструкторах
Деструктор не должен выбрасывать исключений. Если во время разворачивания стека
(в ответ на другое исключение) деструктор тоже бросит исключение — программа
завершится аварийно. Все деструкторы RAII-классов делайте noexcept
(явно или неявно).
4. Время жизни объекта = время жизни ресурса
Создавайте RAII-объект как можно позже — прямо перед использованием ресурса. Разрушайте
как можно раньше — оборачивайте в блоки { }, если ресурс нужен только
в этом участке.
Антипаттерн: владеть ресурсом через сырой указатель
class Widget {
int* data; // сырой указатель
public:
Widget(int n) : data(new int[n]) {}
~Widget() { delete[] data; } // придётся не забыть
};
Хуже того: этот класс упадёт при копировании — два объекта будут
удалять один и тот же массив. Правильно — использовать vector<int>
или unique_ptr<int[]>.
Антипаттерн: ручное освобождение в нескольких точках выхода
int process() {
FILE* f = fopen("data.txt", "r");
if (!f) return -1;
if (checkSomething() == false) {
fclose(f); // ← не забудь!
return -2;
}
if (parse() == false) {
fclose(f); // ← не забудь!
return -3;
}
fclose(f); // ← и здесь не забудь!
return 0;
}
Каждая новая ветка — новый шанс забыть fclose. С RAII достаточно объявить
ifstream f("data.txt"); — и всё закроется само при любом выходе из функции.
Шпаргалка
| Ресурс | RAII-обёртка в стандартной библиотеке |
|---|---|
| Динамическая память (один объект) | unique_ptr<T>, shared_ptr<T> |
| Динамический массив | vector<T>, unique_ptr<T[]> |
| Файл для чтения | ifstream |
| Файл для записи | ofstream |
| Файл для чтения и записи | fstream |
| Мьютекс | lock_guard<mutex>, unique_lock<mutex> |
| Строка | string |
| Сокет / соединение с БД | своя RAII-обёртка (в стандартной библиотеке нет) |
Задание
- Возьмите функцию с ручным
fopen/fcloseи перепишите её с использованиемifstream. - Напишите RAII-класс
Timer, который в конструкторе запоминает время, а в деструкторе выводит, сколько прошло. - Оберните
mutexвlock_guardи объясните, что произойдёт, если внутри критической секции вылетит исключение. - Что такое «правило нуля»? Приведите пример класса, для которого не нужно писать ни деструктор, ни конструктор копирования, ни оператор присваивания.
- Почему деструктор не должен выбрасывать исключений? Что произойдёт, если
во время разворачивания стека деструктор бросит
std::exception? - Напишите RAII-класс
DbConnection, который в конструкторе открывает соединение, а в деструкторе закрывает его. Запретите копирование, разрешите перемещение.