new/delete и утечки памяти

Финальный и самый практичный вопрос раздела: «Что не так в этой функции?» — и почти всегда ответ связан с забытым delete.

Утечка памяти (memory leak) — ситуация, когда память, выделенная через new, больше никому не доступна (указатель на неё потерян), но и не освобождена через delete — она остаётся занятой до конца работы программы.

Что не так в этом коде?

#include <iostream>

void obrabotatZapros() {
    int* dannye = new int[1000]; // выделили память под 1000 чисел

    dannye[0] = 42;
    std::cout << dannye[0] << std::endl;

    // функция заканчивается без delete[]
}

int main() {
    for (int i = 0; i < 100000; i++) {
        obrabotatZapros();
    }
    return 0;
}

Вывод:

42
42
42
... (напечатается 100000 раз, а память будет расти всё это время)

Функция obrabotatZapros вызывается сто тысяч раз, и каждый раз выделяет память под 1000 чисел через new int[1000]. Но локальная переменная dannye — это просто указатель, а не сама память. Когда функция заканчивается, сам указатель dannye (как любая локальная переменная) исчезает из стека — а вот память в куче, на которую он указывал, никуда не девается: адрес до неё просто больше нигде не сохранён. Получить его обратно и освободить уже нельзя. Так после ста тысяч вызовов программа тихо «съедает» сотни мегабайт памяти, и это будет продолжаться, пока операционная система не остановит процесс или он не закончится сам.

Как это выглядит в реальности

Утечка памяти редко роняет программу мгновенно — в этом и коварство проблемы. Программа продолжает работать, но с каждым новым вызовом функции без освобождённого delete занимает всё больше оперативной памяти. Если это серверное приложение, которое работает неделями, утечка в несколько килобайт на запрос в итоге приведёт к тому, что сервер займёт всю доступную память и упадёт — а разработчик будет искать причину падения совсем не там, где реальная утечка.

Как выглядит исправленный код

#include <iostream>

void obrabotatZapros() {
    int* dannye = new int[1000];

    dannye[0] = 42;
    std::cout << dannye[0] << std::endl;

    delete[] dannye; // освобождаем ровно то, что выделили
}

int main() {
    for (int i = 0; i < 100000; i++) {
        obrabotatZapros();
    }
    return 0;
}

Правило простое: сколько раз вызвали new, столько раз должен быть вызван соответствующий delete. Важная деталь, которую любят проверять на собеседовании: для массива, выделенного через new int[1000] (со скобками), освобождать нужно строго через delete[] (тоже со скобками), а не просто delete — иначе компилятор корректно освободит память только под первый элемент, а остальные 999 всё равно утекут.

А что если между new и delete выбросится исключение?

void riskovannaya() {
    int* p = new int(5);
    mozhetBrositIskluchenie(); // а если тут выброшено исключение?
    delete p; // эта строка никогда не выполнится!
}

Если mozhetBrositIskluchenie() действительно выбросит исключение, выполнение функции прервётся немедленно, строка delete p; не выполнится, и снова утечка — даже несмотря на то, что delete в коде физически присутствует. Именно поэтому в современном C++ ручные new/delete стараются вообще не использовать напрямую, а заменяют «умными указателями» вроде std::unique_ptr, которые освобождают память автоматически в своём деструкторе — сработает он и при обычном завершении функции, и при исключении.

Частые ошибки на собеседовании

  • Не замечают несоответствие new[] / delete (без скобок) — частный, но популярный вопрос-ловушка именно про массивы.
  • Считают, что утечка памяти — это ошибка компиляции или падение программы. На самом деле код с утечкой компилируется и запускается совершенно нормально, просто медленно исчерпывает память.
  • Забывают про пути выхода через исключения — «delete в конце функции» не гарантирует освобождение, если раньше вылетит исключение.

Итог

  • Утечка — это память, выделенная через new, адрес которой потерян до вызова delete.
  • Каждому new должен соответствовать ровно один delete, а каждому new[] — ровно один delete[].
  • Исключения могут «перепрыгнуть» через delete — поэтому в реальном коде вместо ручного управления обычно используют умные указатели.
Проверьте себя
1. Почему код с утечкой памяти в примере не падает сразу, а печатает 42 сто тысяч раз без ошибок?
AПотому что утечка памяти — это ошибка компиляции, которая была бы видна сразу
BПотому что программа продолжает работать нормально, просто постепенно занимает всё больше памяти в куче
CПотому что new автоматически освобождает память сам, если забыт delete
DПотому что массив из 1000 чисел слишком маленький, чтобы вызвать утечку
2. Память была выделена как int* arr = new int[50]; Каким способом её нужно правильно освободить?
Adelete arr;
Bdelete[] arr;
Cfree(arr);
DОсвобождать не нужно, массивы освобождаются автоматически