RAII
Объект из new нужно удалить на каждом пути выхода из функции. Ранний return может обойти delete:
#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 — «получение ресурса есть инициализация». Ресурс получают, когда создают объект-владелец, а отдают, когда этот объект уничтожается.
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 для них не пишут.
Частая ошибка
Владельца ресурса копируют:
int main() {
IntBox a(5);
IntBox b = a; // ✗
}Компилятор собирает эту программу без предупреждений. IntBox b = a; копирует объект поле за полем, и b.ptr_ получает тот же адрес, что и a.ptr_. В конце main оба деструктора удаляют одно и то же число, а повторный delete — неопределённое поведение.
Копию создаёт конструктор копирования (copy constructor), и владелец ресурса либо запрещает его, либо копирует сам ресурс, как std::vector. Проще всего запретить копию — добавить в public-часть класса две строки с = delete:
IntBox(const IntBox&) = delete;
IntBox& operator=(const IntBox&) = delete;Первая строка запрещает копию при создании, IntBox b = a;, а вторая — присваивание b = a;. Теперь ошибку находит компилятор:
error: call to deleted constructor of 'IntBox'Передавать такой объект в функцию по-прежнему можно — по ссылке (reference), которая его не копирует.