Утечка памяти - это не память, которая исчезла. Это память, которая всё ещё ваша, всё ещё занята и которую вы больше не в состоянии вернуть, потому что последний указатель на неё пропал. Ничего не падает. Программа продолжает работать, с каждым разом чуть тяжелее, пока где-нибудь в совершенно постороннем месте что-нибудь не откажет.
Эта страница о том, как возникают утечки, какая дисциплина предотвращает большинство из них и какие два инструмента находят остальные.
Как выглядит утечка
Каждая итерация перезаписывает block свежим указателем. Предыдущий блок по-прежнему выделен; ни одна переменная не хранит его адрес; освободить его уже невозможно. Три итерации теряют двенадцать килобайт. Сервер, делающий так на каждый запрос, теряет их навсегда, с той скоростью, с какой приходят запросы.
Исправление - одна строка, free(block); в конце тела цикла, но настоящий навык - понять, где ей место.
Как возникают утечки
1. Потерянный указатель
Любое присваивание указателю, который всё ещё хранит единственную ссылку на живой блок, приводит к утечке этого блока.
char *name = malloc(32);
name = malloc(64); /* первые 32 байта теперь недостижимы */
Цикл выше - тот же самый баг, только в обёртке цикла. Как и переприсваивание поля структуры, и сокращённая запись realloc из calloc и realloc:
p = realloc(p, n); /* при неудаче: p становится NULL, старый блок осиротел */
2. Ранний выход
Каждый путь выхода из функции обязан вернуть то, что функция уже взяла. Забывают всегда о том пути, который обрабатывает ошибку.
Успешный путь верен, а путь ошибки протекает - именно поэтому такое переживает тестирование: сбойная ветка при разработке почти никогда не выполняется. Лечится одной секцией очистки, на которую переходят все пути:
Это тот единственный случай использования goto, который опытные си-программисты сами рекомендуют. Работает он потому, что каждый указатель начинает с NULL, а free(NULL) ничего не делает, так что единственный блок выхода верен независимо от того, как далеко успела зайти функция.
3. Неясное владение
Самые коварные утечки вообще не являются ошибками в коде - это две функции, не сошедшиеся во мнении, чья это была задача.
char *build_message(void); /* вызывающий должен это освободить? */
void store(char *text); /* store забирает владение? */
Если build_message возвращает выделенную память, а store её копирует, освобождать должен вызывающий. Если store сохраняет указатель, вызывающему освобождать нельзя. В коде ничего не сказано о том, как оно на самом деле, поэтому одно из двух предположений будет сделано дважды - и вы получите либо утечку, либо двойное освобождение.
Лекарство - соглашение, записанное в комментарии рядом с каждой функцией, которая что-то выделяет:
/* Возвращает вновь выделенную строку; вызывающий обязан её освободить. */
char *build_message(void);
/* Забирает владение 'text'; освобождать её будет store_free(). */
void store(char *text);
Пишите правило у функции, а не в проектном документе. Это самая полезная привычка в управлении памятью на C.
Дисциплина владения
Четыре правила покрывают почти всё:
- У каждого выделения ровно один владелец - один участок кода, отвечающий за освобождение.
- Каждой выделяющей функции - парная освобождающая.
vec_init/vec_free,config_load/config_free. Симметрия делает пропущенный вызов заметным. - Освобождайте на том же уровне, где выделяли, если только комментарий к функции явно не передаёт владение.
- После освобождения присваивайте указателю
NULL, чтобы случайное последующее использование упало на месте ошибки, а не потихоньку испортило кучу.
Поиск утечек: valgrind
В Linux valgrind не требует перекомпиляции, хотя отладочные символы делают отчёт читаемым:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
Для протекающего цикла в начале страницы отчёт закончится примерно так:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
Читайте снизу вверх. «definitely lost» означает, что на момент выхода указателя на блок не существовало - настоящая утечка. Трассировка стека называет строку того malloc, который создал блок, а не строку, где он был потерян, и обычно этого достаточно, чтобы найти недостающий free.
Встречаются ещё две категории:
- indirectly lost - блоки, достижимые только через блок, который сам был потерян, например элементы утёкшего связного списка. Исправьте «definitely lost», и эти исчезнут.
- still reachable - выделены на момент выхода, но на них есть живой указатель; обычно это глобальный кэш. Не утечка в опасном смысле, но освободить стоит, чтобы отчёт оставался пустым.
Valgrind заодно ловит чтение неинициализированной памяти и запись за конец блока, а это часто и есть способ обнаружить баг, стоящий за утечкой.
Поиск утечек: AddressSanitizer
AddressSanitizer встроен в GCC и Clang, работает намного быстрее valgrind и там, где valgrind не работает (включая современные macOS):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
Отчёт об утечках печатается при выходе автоматически:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
ASan также превращает использование после освобождения и переполнения буфера в куче в немедленные, чётко подписанные аварийные остановки вместо загадочной порчи памяти где-то потом. Собирайте с ним тестовые прогоны, а релизы - без него, поскольку он стоит памяти и скорости.
Если на вашей платформе обнаружение утечек не срабатывает, задайте перед запуском переменную окружения ASAN_OPTIONS=detect_leaks=1.
Чиним протекающую программу по шагам
Вот небольшая программа с тремя разными утечками:
Valgrind сообщит о трёх записях «definitely lost» с тремя разными номерами строк. Исправляем по одной:
Починка 1 - это воплощённый комментарий о владении: shout выделяет, main освобождает. Починка 2 вообще убирает лишнее выделение, а не освобождает первый блок, - более простой код здесь же и более правильный. Починка 3 добавляет недостающий free на раннем выходе; когда выделений больше, единственная метка cleanup:, показанная выше, масштабируется лучше, чем повторение free в каждой ветке.
Привычки, предотвращающие утечки
- Пишите
freeсразу после того, как написалиmalloc, а код между ними дописывайте потом. - Каждой выделяющей функции давайте парную освобождающую.
- Указывайте владение в комментарии к любой функции, которая возвращает или принимает выделенный ею указатель.
- В функциях с несколькими выделениями используйте один блок выхода
cleanup:. - Гоняйте тесты под
-fsanitize=addressкак само собой разумеющееся, а не только когда что-то выглядит подозрительно. - Считайте «definitely lost: 0 bytes» частью успешного тестового прогона.
Часто задаваемые вопросы
Что такое утечка памяти в C?
Память, выделенная через malloc, которую вы больше не можете освободить, потому что на неё ничего в программе не указывает. Блок остаётся занятым до конца жизни процесса. Это не падение - программа продолжает работать, просто с каждым проходом занимает всё больше памяти, пока та в итоге не кончится.
Как найти утечки памяти в C?
Запустите программу под valgrind: valgrind --leak-check=full ./program. Он сообщит о каждом блоке, который остался выделенным на момент выхода, вместе со стеком вызова malloc, создавшего его. В macOS или там, где valgrind недоступен, компилируйте с -fsanitize=address - тот же отчёт появится при выходе.
Из-за чего возникают утечки памяти в C?
Почти все они укладываются в три схемы: перезапись единственного указателя на блок (включая p = realloc(p, n) при неудаче), ранний выход из функции, которая уже что-то выделила, и неясное владение - две функции, каждая из которых считает, что освобождает другая, так что не освобождает никто.
Важны ли утечки, если программа всё равно завершается?
Для программы, которая один раз отработала и вышла, операционная система вернёт себе всё, так что практических последствий нет. Это важно для всего долгоживущего - сервера, игрового цикла, демона, - где утечка на каждый запрос растёт без предела. Освобождайте память всё равно: отчёт с утечками - это шум, который прячет те, что действительно важны.