Перейти к содержимому
Теория · Память и умные указатели · урок 4 из 12

RAII

Объект из new нужно удалить на каждом пути выхода из функции. Ранний return может обойти delete:

C++
#include <iostream>

void print_square(int x) {
    int* square = new int{x * x};
    if (*square > 100) {
        return;  // ✗
    }
    std::cout << *square << '\n';
    delete square;
}

int main() {
    print_square(12);
    print_square(5);  // 25
}

При x = 12 функция выходит через return, не дойдя до delete. Указатель square уничтожен, адрес потерян, и число остаётся в куче до конца программы — это утечка. При запуске её не видно: программа печатает 25 и завершается как обычно. Можно поставить delete перед каждым return, но рано или поздно один из них забудут.

Владелец ресурса

Надёжнее поручить удаление деструктору: он выполняется, когда заканчивается время жизни объекта. Для локального объекта это любой выход из блока, в том числе через return. Идиому, в которой объект получает ресурс (resource) в конструкторе и освобождает его в деструкторе, называют RAII. Ресурс — всё, что нужно вернуть после работы: память из кучи, открытый файл.

Название расшифровывается как Resource Acquisition Is Initialization — «получение ресурса есть инициализация». Ресурс получают, когда создают объект-владелец, а отдают, когда этот объект уничтожается.

C++
class IntBox {
public:
    IntBox(int value) : ptr_(new int{value}) {}
    ~IntBox() { delete ptr_; }
    int get() const { return *ptr_; }
private:
    int* ptr_;
};
void print_square(int x) {
    IntBox square(x * x);
    if (square.get() > 100) {
        return;
    }
    std::cout << square.get() << '\n';
}

IntBox владеет числом: создаёт его в конструкторе и удаляет в деструкторе. В новой print_square нет ни одного delete, и утечки тоже нет. На каждом выходе из функции компилятор сам вставляет вызов ~IntBox.

По тому же правилу устроены классы стандартной библиотеки. std::vector и std::string сами выделяют память под элементы и сами освобождают её в деструкторе, поэтому delete для них не пишут.

Частая ошибка

Владельца ресурса копируют:

C++
int main() {
    IntBox a(5);
    IntBox b = a;  // ✗
}

Компилятор собирает эту программу без предупреждений. IntBox b = a; копирует объект поле за полем, и b.ptr_ получает тот же адрес, что и a.ptr_. В конце main оба деструктора удаляют одно и то же число, а повторный delete — неопределённое поведение.

Копию создаёт конструктор копирования (copy constructor), и владелец ресурса либо запрещает его, либо копирует сам ресурс, как std::vector. Проще всего запретить копию — добавить в public-часть класса две строки с = delete:

C++
IntBox(const IntBox&) = delete;
IntBox& operator=(const IntBox&) = delete;

Первая строка запрещает копию при создании, IntBox b = a;, а вторая — присваивание b = a;. Теперь ошибку находит компилятор:

TEXT
error: call to deleted constructor of 'IntBox'

Передавать такой объект в функцию по-прежнему можно — по ссылке (reference), которая его не копирует.