Menu

malloc в C: динамическое выделение памяти, free и sizeof

Зачем нужна куча, как malloc выдаёт память, размер которой определяется во время выполнения, идиома p = malloc(n * sizeof *p), проверка на NULL, освобождение ровно один раз и динамический массив от начала до конца.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

У каждого массива, который вы писали до сих пор, размер был зашит на этапе компиляции: int scores[10]; резервирует десять ячеек, нужны вам три или три сотни. Это работает ровно до тех пор, пока размер не становится известен только во время выполнения — сколько строк в файле, сколько записей введёт пользователь. Динамическая память решает эту задачу: вы запрашиваете нужные байты тогда, когда они нужны, и возвращаете их, когда закончили.

Инструменты лежат в <stdlib.h>:

#include <stdlib.h>

Зачем нужна куча

C даёт программе две основные области памяти. Локальные переменные живут в автоматической памяти (в стеке): их размер определяет компилятор, и они исчезают в момент выхода из функции. Динамические выделения живут в куче: размер вы задаёте во время выполнения, и блок остаётся жив, пока вы явно его не освободите.

Именно второе свойство и есть настоящая причина использовать malloc. Функция может выделить блок, вернуть указатель, и память останется корректной у вызывающего — локальный массив так никогда не сможет. Подробное сравнение двух областей — в стек против кучи.

malloc: запрос байтов

malloc принимает количество байт и возвращает void *, указывающий на такое количество неинициализированной памяти, либо NULL, если запрос не удался.

Четыре момента в этой короткой программе и составляют всю дисциплину:

  1. Выражение размера — n * sizeof *scores. Читайте его как «n штук того, на что указывает scores».
  2. Результат проверяется на NULL до любого использования.
  3. С блоком работают ровно как с массивомscores[i] работает, потому что индексация есть арифметика указателей.
  4. Он освобождается один раз, и указателю затем присваивается NULL.

Обратите внимание: приведения типа возвращаемого значения нет. В C void * автоматически преобразуется в любой указатель на объект, а запись (int *)malloc(...) добавляет шум и заодно маскирует отсутствующий #include <stdlib.h>. (C++ требует приведения; C — нет.)

Идиома с sizeof

Почему sizeof *scores, а не sizeof(int)? Потому что так размер не может устареть.

int  *a = malloc(n * sizeof *a);     /* n int-ов, каким бы int здесь ни был */
long *b = malloc(n * sizeof *b);     /* та же форма строки, верный размер */

/* хрупкая форма */
long *c = malloc(n * sizeof(int));   /* компилируется, выделяет слишком мало */

Последняя строка — настоящий баг, о котором ни один компилятор не предупредит: c имеет тип long *, размер выделения посчитан для int, и любая запись за первой половиной уходит за границу. С sizeof *c размер следует за объявлением автоматически.

sizeof *p не разыменовывает psizeof вычисляется на этапе компиляции по одному лишь типу, поэтому это безопасно, даже когда p не инициализирован или равен NULL.

Неинициализированная память

malloc не очищает то, что вам выдаёт. В байтах лежит всё, что было там раньше.

Когда нужно, чтобы блок сразу был обнулён, это делает calloc за один шаг — см. calloc и realloc.

free: вернуть память обратно

free(p) возвращает блок аллокатору. Три правила:

  • Передавайте ровно тот указатель, который вернул malloc, а не сдвинутый. free(p + 1) — неопределённое поведение.
  • Освобождайте каждый блок ровно один раз. Дважды — это двойное освобождение, оно портит внутренний учёт аллокатора.
  • После освобождения указатель становится «протухшим». Использовать его — это обращение после освобождения (use-after-free).
int *p = malloc(sizeof *p);
free(p);
*p = 5;        /* use-after-free - неопределённое поведение */
free(p);       /* двойное освобождение - неопределённое поведение */

Ни одна из этих строк не обязана уронить программу немедленно, и именно этим они опасны: программа может проработать несколько минут и сломаться в совершенно другом месте. Дешёвая защита — обнулить указатель:

free(p);
p = NULL;

free(NULL) по стандарту явно ничего не делает, поэтому повторный free(p) после этого безвреден, а *p превращается в мгновенное падение в месте настоящей ошибки вместо тихого повреждения памяти.

Динамический массив от начала до конца

Вот как выглядит реальное выделение: прочитать количество во время выполнения, взять под него блок, использовать, освободить.

Приведение количества к size_t начинает играть роль, когда размеры становятся большими: n * sizeof *values с обычным int n может переполниться ещё до того, как результат умножения дойдёт до malloc, и выдаст блок заметно меньше задуманного. Умножение в size_t этого избегает.

Выделение внутри функции

Именно правило времени жизни в куче делает такой код законным — и делает незаконным возврат локального массива:

len + 1 оставляет место под нулевой терминатор — то же правило расчёта размера, что и везде в строках. И обратите внимание на комментарий над функцией: когда функция возвращает выделенную память, вопрос «кто её освобождает» — часть её контракта. Записать это не бюрократия; это единственное, что предотвращает утечку.

Структуры в куче

Та же идиома работает для любого типа:

Обратите внимание: вторая ветка обработки ошибки освобождает p перед возвратом. Как только функция держит больше одного выделения, каждый путь с ошибкой обязан отпустить уже занятое — это самый частый источник утечек, описанных в утечках памяти.

Чек-лист

  • Всегда проверяйте результат malloc на NULL до первого использования.
  • Считайте размер как n * sizeof *p, умножая в size_t.
  • В C приведение возвращаемого значения не нужно.
  • Память из malloc не инициализирована; пишите до того, как читать.
  • Освобождайте ровно один раз, исходным указателем, затем присваивайте ему NULL.
  • Каждый путь с ошибкой после выделения обязан освободить то, что уже занято.

Часто задаваемые вопросы

Что делает malloc в C?

malloc(n) запрашивает n байт памяти из кучи и возвращает указатель на начало этого блока либо NULL, если запрос не удалось удовлетворить. Блок живёт, пока вы не передадите тот же самый указатель в free — в отличие от локальной переменной он не освобождается при выходе из функции.

Как пользоваться malloc и free в C?

Выделите память через int *p = malloc(n * sizeof *p);, проверьте if (p == NULL) до первого обращения, работайте с блоком как с массивом, а затем ровно один раз вызовите free(p);, когда закончите. Присваивание p = NULL после этого превращает случайное позднее обращение в честное падение вместо тихого повреждения памяти.

Почему malloc(n * sizeof *p) лучше, чем malloc(n * sizeof(int))?

Потому что sizeof *p автоматически следует за типом указателя. Если p позже станет long * или struct Point *, размер выделения обновится сам; а записанный вручную sizeof(int) оставит размер, который стал неверным и о котором компилятор не предупредит.

Что будет, если не освобождать память в C?

Блок останется выделенным до конца жизни процесса — это утечка памяти. Короткой программе это сходит с рук, потому что операционная система всё возвращает при выходе, но долгоживущая программа теряет понемногу на каждом проходе и в конце концов исчерпает память. См. утечки памяти.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ