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

Стек и куча; new и delete

Локальная переменная уничтожается в конце своего блока, поэтому указатель (pointer) на неё, возвращённый из функции, будет висячим (dangling). Объект, созданный выражением new, живёт дольше: пока программа не удалит его выражением delete.

C++
#include <iostream>

int* make_counter(int start) {
    int* counter = new int{start};
    return counter;
}

int main() {
    int* c = make_counter(5);
    *c = *c + 1;
    std::cout << *c << '\n';  // 6
    delete c;
}

new int{start} создаёт число типа int со значением start и возвращает его адрес. Указатель counter уничтожается при выходе из make_counter, а число остаётся, и main получает его адрес. Когда число больше не нужно, delete c его удаляет.

Стек и куча

Локальные переменные хранятся в стеке (stack). Место в нём занимается при входе в блок и освобождается при выходе, без единой строки кода. Объекты из new лежат в другой области памяти — в куче (heap). Объект в куче живёт до вызова delete с его адресом, сколько бы блоков и функций ни завершилось.

Сам по себе объект в куче не удаляется. Если адрес потерян, а delete не выполнен, память остаётся занятой до конца программы. Это утечка памяти (memory leak): программа, которая долго работает и теряет объекты, занимает всё больше памяти.

Массивы в куче

new int[n] создаёт n чисел подряд и возвращает адрес первого. В отличие от C-массива, размер n может стать известен только во время работы программы. Такой массив удаляют через delete[], с квадратными скобками:

C++
int n = 4;
int* squares = new int[n];
for (int i = 0; i < n; ++i) {
    squares[i] = i * i;
}
std::cout << squares[n - 1] << '\n';  // 9
delete[] squares;

Массив из new[] нельзя удалять обычным delete, а одиночный объект — через delete[]. Такая путаница — неопределённое поведение (undefined behavior), и компилятор (compiler) предупреждает о ней только в простых случаях.

Удаление через базовый класс

Объект производного класса можно создать через new и хранить его адрес в указателе на базовый класс. Тогда delete через этот указатель корректен, только если деструктор (destructor) базового класса виртуальный. Такой delete вызывает сначала деструктор производного класса, затем базового:

C++
#include <iostream>

struct Base {
    virtual ~Base() { std::cout << "~Base\n"; }
};

struct Derived : public Base {
    ~Derived() { std::cout << "~Derived "; }
};

int main() {
    Base* p = new Derived{};
    delete p;  // ~Derived ~Base
}

Без virtual у ~Base такой delete — неопределённое поведение: стандарт не гарантирует даже вызова ~Derived.

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

Объект используют после delete:

C++
int main() {
    int* p = new int{7};
    delete p;
    *p = 8;  // ✗
}

Программа собирается, но объекта по адресу из p уже нет, и запись *p = 8 — неопределённое поведение. То же касается любой копии этого адреса: после int* q = p; и delete p указатель q тоже ведёт к удалённому объекту. Если указатель ещё нужен, присвойте ему nullptr сразу после delete. Тогда проверка p != nullptr покажет, есть ли за ним объект.