Динамическая память: 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.

Проверьте себя
1. Что произойдёт, если выделить память через malloc, но забыть вызвать free?
AПрограмма не скомпилируется
BУтечка памяти: блок остаётся занятым до конца программы и недоступен
CПамять освободится автоматически при выходе из функции
Dmalloc вернёт NULL
2. Зачем после free(arr) присваивать arr = NULL?
AЧтобы ускорить программу
BЧтобы повторно выделить ту же память
CЧтобы обезвредить указатель: обращение по NULL вызовет явный сбой вместо коварного use-after-free
DЭто обязательное требование компилятора