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 — поэтому в реальном коде вместо ручного управления обычно используют умные указатели.