Динамическая память: malloc и free
Урок учит выделять память вручную: функции malloc, calloc, realloc и free, разницу между стеком и кучей, и как избежать утечек и опасного использования освобождённой памяти.
В C память на куче вы выделяете и освобождаете сами. На каждый malloc обязан приходиться free. Забудете освободить — утечка; используете после освобождения — use-after-free, одна из опаснейших уязвимостей.
До сих пор все переменные жили на стеке: их размер был известен заранее, и они исчезали при выходе из функции. Но иногда размер данных известен только во время работы (например, пользователь вводит, сколько элементов нужно). Тогда память берут из кучи функцией malloc:
#include <stdlib.h>
int n = 5;
int *arr = malloc(n * sizeof(int)); // память под 5 int
if (arr == NULL) { // malloc может вернуть NULL!
return 1; // памяти не хватило
}
for (int i = 0; i < n; i++) {
arr[i] = i * 10; // используем как обычный массив
}
free(arr); // ОБЯЗАТЕЛЬНО освобождаем
arr = NULL; // защита от повторного доступа
malloc возвращает адрес выделенного блока (или NULL при неудаче). После использования память возвращают системе через free. Родственники: calloc выделяет и обнуляет память, realloc изменяет размер уже выделенного блока.
Как работает под капотом
Стек и куча — две разные области памяти с разным поведением. Стеком управляет компилятор автоматически; кучей — вы вручную.
СТЕК (stack) КУЧА (heap)
- управляет компилятор - управляете ВЫ (malloc/free)
- авто-очистка при выходе - живёт, пока не вызовете free
- размер известен заранее - размер можно задать в рантайме
- быстрый - чуть медленнее, гибкий
malloc(20):
[ запрос 20 байт ] --> куча выделяет блок --> возвращает адрес
|
arr ----> [ ? ? ? ? ? ] (20 байт на куче)
|
free(arr): блок возвращён системе
arr = NULL: указатель обезврежен (больше не "висячий")
Память на куче не исчезает сама при выходе из функции — она живёт, пока вы явно не освободите её через free. Это даёт гибкость, но и накладывает ответственность.
Частые ошибки
- Утечка памяти (memory leak). Выделили через
malloc, но забылиfree— память занята до конца программы. В долгоживущих программах это критично. - Use-after-free. Обращение к памяти после
free— чтение или запись по уже освобождённому адресу. Классическая уязвимость. - Двойное освобождение.
freeодного блока дважды повреждает структуры кучи и роняет программу. - Не проверили результат malloc. При нехватке памяти
mallocвернётNULL; разыменование без проверки — крах.
Best practices
- Правило: на каждый
malloc— ровно одинfree. Планируйте освобождение сразу при выделении. - После
freeприсваивайте указателюNULL— это превращает опасный use-after-free в безопасный сбой при разыменовании. - Всегда проверяйте результат
mallocнаNULLперед использованием. - Проверяйте программу инструментами:
valgrind ./progили компиляцией с-fsanitize=address— они ловят утечки и use-after-free автоматически.
В Python память управляется автоматически сборщиком мусора, но логику «выделить блок, поработать, освободить и больше не трогать» можно смоделировать явно — чтобы прочувствовать дисциплину C.
# Эмулируем ручное управление памятью кучи
heap = {}
next_addr = [1000]
def malloc(size):
addr = next_addr[0]
heap[addr] = [0] * size # выделяем блок
next_addr[0] += size
return addr
def free(addr):
if addr not in heap:
raise Exception("double free или неверный адрес!")
del heap[addr] # освобождаем
p = malloc(5)
heap[p][0] = 42
print("Записали в блок:", heap[p])
free(p)
print("После free блок", p, "существует:", p in heap) # False
# heap[p] теперь вызовет ошибку — это и есть use-after-free
Та же логика на Python ▶ — обращение к освобождённому блоку вызывает ошибку. В C такое обращение часто «срабатывает» и читает мусор, что делает баг куда коварнее.
Фрагментация и владение памятью
Ручное управление памятью порождает два понятия, важных в больших программах. Первое — фрагментация кучи: после множества выделений и освобождений блоков разного размера свободная память дробится на мелкие куски, и крупный запрос может не пройти, хотя суммарно места достаточно. Второе и более практичное — владение (ownership): для каждого выделенного блока должно быть чётко понятно, какая часть кода обязана его освободить. Размытое владение — главный источник и утечек (никто не освободил), и double free (освободили дважды). Зрелые C-проекты вводят соглашения: например, «функция, которая выделила, та и освобождает» или «вызывающий владеет возвращённым указателем». Эти правила — не язык, а дисциплина команды, и именно она отличает надёжный C-код от ненадёжного.
Итоги
Динамическая память берётся из кучи функцией malloc (и родственными calloc, realloc) и обязательно освобождается через free. Стек управляется автоматически, куча — вручную. Главные опасности — утечки, use-after-free и двойное освобождение. Защита — парность malloc/free, обнуление указателя после free, проверка на NULL и инструменты вроде valgrind и AddressSanitizer.