Динамическая память

Зачем нужна динамическая память

Статический массив требует, чтобы размер был известен при компиляции:

int a[100];     /* максимум 100 элементов, больше не поместится */

Но часто размер становится известен только во время работы программы: пользователь вводит N, файл содержит неизвестное число записей, список растёт по мере добавления элементов. Если зарезервировать «массив побольше», память может закончиться или её будет жалко.

Динамическая память (куча, heap) — область, в которой программа сама запрашивает и освобождает память во время выполнения. Размер задаётся в байтах, а не в элементах.

Аналогия. Статический массив — заранее купленный шкаф фиксированного размера. Динамическая память — склад, где можно попросить ровно столько полок, сколько нужно сейчас, а потом вернуть их.

malloc: выделить память

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n <= 0)
    {
        printf("Ошибка: нужно положительное N\n");
        return 1;
    }

    int *a = malloc((size_t)n * sizeof(*a));
    if (a == NULL)
    {
        printf("Не хватило памяти\n");
        return 1;
    }

    for (int i = 0; i < n; i++)
    {
        scanf("%d", &a[i]);
    }

    for (int i = 0; i < n; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");

    free(a);
    a = NULL;
    return 0;
}

Разберём строку выделения:

  • sizeof(*a) — размер одного элемента, на который смотрит a. Если тип элемента когда-нибудь изменится, код останется правильным;
  • n * sizeof(*a) — общий размер в байтах;
  • (size_t) — приведение к беззнаковому типу размера, чтобы произведение не переполнилось при больших n;
  • malloc возвращает void *; в C приводить результат к int * не нужно;
  • результат обязательно проверяют на NULL.

malloc не заполняет память: там могут быть любые байты. Читать элемент до записи нельзя.

calloc: выделить и обнулить

calloc принимает количество элементов и размер одного элемента, а память заполняет нулями:

int *a = calloc((size_t)n, sizeof(*a));
if (a == NULL)
{
    return 1;
}

Это удобно для массивов счётчиков, матриц и структур, которые должны начинаться с нулей. Для обычного массива, который сразу заполняется, разницы с malloc почти нет.

realloc: изменить размер

realloc меняет размер ранее выделенного блока. Старое содержимое сохраняется в пределах нового размера:

int *bigger = realloc(a, (size_t)(n + 1) * sizeof(*a));
if (bigger == NULL)
{
    /* Старый блок a остался жив — его нужно освободить. */
    free(a);
    return 1;
}
a = bigger;
a[n] = 42;
n++;

Результат всегда сохраняют во временную переменную. Если записать a = realloc(a, ...) и realloc вернёт NULL, старый указатель будет потерян — получится утечка.

realloc(NULL, size) работает как malloc(size). realloc(ptr, 0) — особый случай; в учебных задачах его лучше не использовать, а просто вызвать free.

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

free(a);
a = NULL;
  • free принимает только указатель, который вернул malloc, calloc или realloc;
  • free(NULL) безопасен и ничего не делает;
  • повторный free того же блока — ошибка (double free);
  • после free память больше не принадлежит программе: обращаться к ней нельзя;
  • присваивание a = NULL после free — полезная привычка, но она не заменяет аккуратность: если на блок есть другие указатели, они останутся «висячими».

Утечки и висячие указатели

Утечка памяти (memory leak) — блок выделен, но указатель на него потерян, и free вызвать уже невозможно:

int *p = malloc(sizeof(*p));
*p = 12;

p = malloc(sizeof(*p));   /* старый блок потерян — утечка */
*p = 0;
free(p);

Висячий указатель (dangling pointer) — указатель на уже освобождённую память:

int *p = malloc(sizeof(*p));
free(p);
*p = 10;        /* ошибка: память уже вернули */

В маленькой учебной программе утечка может быть незаметна, потому что память освобождается при завершении процесса. Но рассчитывать на это нельзя: в большой программе или в цикле утечки быстро съедают память. В лабораторной работе память нужно освобождать явно.

Динамический массив: типовой каркас

#include <stdio.h>
#include <stdlib.h>

int *input_array(int n);
void print_array(const int *a, int n);
void free_array(int *a);

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n <= 0)
    {
        printf("Ошибка ввода\n");
        return 1;
    }

    int *a = input_array(n);
    if (a == NULL)
    {
        printf("Не хватило памяти\n");
        return 1;
    }

    print_array(a, n);
    free_array(a);
    return 0;
}

int *input_array(int n)
{
    int *a = malloc((size_t)n * sizeof(*a));
    if (a == NULL)
    {
        return NULL;
    }

    for (int i = 0; i < n; i++)
    {
        if (scanf("%d", &a[i]) != 1)
        {
            free(a);
            return NULL;
        }
    }
    return a;
}

void print_array(const int *a, int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
}

void free_array(int *a)
{
    free(a);
}

Функция input_array возвращает указатель на новый блок или NULL при ошибке. Владелец блока — main: именно он вызывает free_array. Такой подход помогает не запутаться, кто должен освобождать память.

Сколько памяти нужно

Размер вычисляется как количество элементов * sizeof(элемент). Для массива структур:

struct Student *group = malloc((size_t)n * sizeof(*group));

Для матрицы n × m из int:

int *matrix = malloc((size_t)n * m * sizeof(*matrix));

Важно использовать sizeof(*указатель), а не sizeof(указатель): во втором случае получится размер самого указателя (обычно 8 байт), а не элемента.

Частые ошибки

ОшибкаЧто не так
не проверили malloc на NULLобращение по пустому адресу, падение
malloc(sizeof(int *)) вместо sizeof(int)выделен размер указателя, а не числа
malloc(n) вместо malloc(n * sizeof(*a))выделено n байт вместо n элементов
a = realloc(a, ...) без временной переменнойпри ошибке старый блок потерян
забыли freeутечка памяти
free дваждыповреждение кучи, неопределённое поведение
обратились к памяти после freeвисячий указатель, непредсказуемый результат
читают элемент до записи после mallocв памяти лежит «мусор»

Документация

Что запомнить

  1. Динамическая память выделяется во время работы программы и освобождается вручную.
  2. malloc выделяет блок в байтах, calloc — и обнуляет его, realloc — меняет размер.
  3. Результат выделения всегда проверяют на NULL.
  4. free вызывают ровно один раз для каждого выделенного блока.
  5. После free памятью пользоваться нельзя.
  6. Размер считают как количество * sizeof(*указатель).

Подтверждение

—