Перейти к содержимому
Теория · Память и умные указатели · урок 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 его забудут.

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

Надёжнее поручить удаление деструктору: он выполняется, когда заканчивается время жизни объекта. Для локального объекта это любой выход из блока, в том числе через return. Идиому, в которой объект получает ресурс в конструкторе и освобождает его в деструкторе, называют 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 — неопределённое поведение.

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

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

operator= — это метод, который выполняет присваивание b = a: его имя — слово operator и знак =. = delete вместо тела говорит компилятору, что метода нет и вызывать его нельзя.

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

TEXT
error: call to deleted constructor of 'IntBox'

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

Главное

  • RAII: объект получает ресурс в конструкторе и освобождает его в деструкторе.
  • Деструктор локального объекта срабатывает на любом выходе из блока, поэтому ранний return не приводит к утечке.
  • Скопированный владелец удаляет ресурс дважды, поэтому копию запрещают через = delete.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом