Память, которую программа берёт и возвращает сама: массив любого размера, список, стек и очередь
malloc, calloc, realloc, free и проверять результатДо сих пор размер каждого массива был известен при компиляции: int a[1000]. Это неудобно: сколько строк в файле или чисел во вводе, становится ясно только при работе программы. Массив «с запасом» тратит память впустую и всё равно может оказаться мал. У программы на C три области для данных:
| Область | Что там | Живёт | Размер |
|---|---|---|---|
| статическая | глобальные и static-переменные | всё время работы программы | задан при компиляции |
| стек | локальные переменные, параметры | до выхода из функции | задан при компиляции; весь стек — 1 МБ в Windows, 8 МБ в Linux |
| куча (heap) | блоки от malloc | пока программа не вызовет free | любой, выбирается при работе; ограничен памятью компьютера |
Блок в куче не привязан к функции: его можно создать в одной функции, вернуть указатель и освободить в другой. Это и даёт свободу, и создаёт ответственность: у каждого блока должен быть владелец — код, который его освободит ровно один раз.
Функции объявлены в stdlib.h и работают с размером в байтах:
malloc(size) — блок из size байтов, содержимое не определено. Размер массива — n * sizeof *p: если поменять тип p, размер поменяется сам.calloc(n, size) — блок под n элементов, заполненный нулями.realloc(p, size) — изменить размер блока; старое содержимое сохраняется, но блок может переехать по другому адресу.free(p) — вернуть блок. free(NULL) ничего не делает.Если памяти нет, первые три возвращают NULL — его проверяют каждый раз.
int *squares = malloc((size_t)n * sizeof *squares);
if (squares == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
…
/* realloc: результат — во временный указатель */
int *bigger = realloc(squares, 2 * (size_t)n * sizeof *squares);
if (bigger == NULL) {
free(squares); /* старый блок цел — освободить его */
…
}
squares = bigger;
…
free(squares);
squares = NULL; /* висячий указатель больше не опасен */
malloc 5 ints (20 bytes): last = 16
realloc to 10 ints: first = 0, last = 81
calloc digit counts of pi: 0 2 1 3 1 3 1 1 1 3
copy_string: Nevabit (8 bytes)
malloc(SIZE_MAX / 2) = NULL
p = realloc(p, …) — ошибка: при неудаче realloc вернёт NULL, старый блок останется, но указатель на него будет потерян — утечка.copy_string из примера (strdup в C11 нет) делает malloc(strlen(s) + 1) и копирует строку. В комментарии к такой функции пишут, кто освобождает результат.malloc честно возвращает NULL. Программа без проверки упадёт позже, на первом обращении по нулевому указателю.Пример целиком — examples/17/memory.c.
Компилятор следит за памятью стека, но не кучи. Ошибки с ней не видны сразу: программа часто выводит верный ответ, а падает потом или на другом компьютере. Программа examples/17/bugs.c делает четыре типичные ошибки:
| Ошибка | Код | Без проверки | С ASan |
|---|---|---|---|
| Утечка: блок не освобождён, указатель потерян | int *a = malloc(…); в цикле без free | работает; долгоживущая программа (сервер) съедает память | detected memory leaks, 1200 байтов в 3 блоках, строка malloc |
Висячий указатель: обращение после free | free(a); printf("%d", a[0]); | печатает мусор: 1519377410 вместо 42 | heap-use-after-free: где читали, где освободили, где выделили |
| Двойное освобождение | free(s); free(s); | Linux: double free detected, аварийно (код 134); бывает и тихая порча кучи | attempting double-free, обе строки free |
| Выход за границу блока | malloc(strlen(s)) — забыт байт под '\0' | работает: copy = Nevabit | heap-buffer-overflow в strcpy |
AddressSanitizer (ASan) встроен в gcc и clang: программа собирается с проверками каждого обращения к памяти и при выходе сообщает об утечках. Отладочная сборка:
gcc -std=c11 -Wall -Wextra -g -fsanitize=address,undefined bugs.c -o bugs
./bugs dangling
==1435616==ERROR: AddressSanitizer: heap-use-after-free on address 0x504000000010 …
#0 … in dangling bugs.c:28 ← где прочитали
…
#1 … in dangling bugs.c:27 ← где освободили
…
#1 … in dangling bugs.c:22 ← где выделили
apt install gcc, ничего не нужно доставлять. В MSYS2 gcc под Windows ASan не поддерживается — есть clang в окружении CLANG64 (pacman -S mingw-w64-clang-x86_64-clang, та же строка с clang вместо gcc). Программа с ASan работает в 2–3 раза медленнее — это сборка для проверки, не для сдачи. Кроме того, gcc с -Wall замечает часть ошибок сам: pointer 'a' used after 'free', writing 8 bytes into a region of size 7 (последнее — только с -O2).Правила, которые снимают большую часть ошибок:
malloc/calloc/realloc — проверка на NULL; realloc — во временный указатель;free(p) — p = NULL: повторный free(NULL) безопасен, а обращение по NULL сразу упадёт, а не прочитает мусор;init / free, и free вызывается на каждом пути выхода, в том числе при ошибке;Динамический массив хранит три поля: указатель на блок, сколько элементов занято (size) и под сколько выделено место (capacity). Пока место есть, добавление — просто запись в data[size++]. Когда место кончилось, блок увеличивается realloc-ом — вдвое:
typedef struct {
int *data;
size_t size; /* занято элементов */
size_t capacity; /* выделено элементов */
} IntArray;
void array_init(IntArray *a)
{
a->data = NULL;
a->size = 0;
a->capacity = 0;
}
/* Добавить в конец; при нехватке места — увеличить ёмкость вдвое. Нет памяти — завершить программу */
void array_push(IntArray *a, int value)
{
if (a->size == a->capacity) {
size_t new_capacity = a->capacity == 0 ? 4 : a->capacity * 2;
int *p = realloc(a->data, new_capacity * sizeof *p);
if (p == NULL) {
fprintf(stderr, "out of memory\n");
array_free(a);
exit(1);
}
a->data = p;
a->capacity = new_capacity;
}
a->data[a->size++] = value;
}
void array_free(IntArray *a)
{
free(a->data);
array_init(a);
}
/* Чтение заранее неизвестного числа целых */
IntArray a;
array_init(&a);
int x;
while (scanf("%d", &x) == 1) {
array_push(&a, x);
}
Функции получают указатель на структуру: они меняют её поля, а копия структуры (раздел 15) изменилась бы и пропала. realloc(NULL, n) работает как malloc(n) — поэтому первый рост не нужно выделять отдельно. Удаление из середины сдвигает хвост влево — memmove, за O(n).
Почему вдвое. realloc может переместить блок и скопировать всё занятое. Пример examples/17/dynarray.c считает перевыделения и элементы, которые при этом пришлось бы скопировать, при чтении чисел 1…n для трёх правил роста:
| n | ×2: перевыделений / копий | +1 | +1000 |
|---|---|---|---|
| 10 | 3 / 12 | 10 / 45 | 1 / 0 |
| 1 000 | 9 / 1 020 | 1 000 / 499 500 | 1 / 0 |
| 1 000 000 | 19 / 1 048 572 | 10⁶ / ≈ 5·10¹¹ (расчёт) | 1 000 / 499 500 000 |
Массив держит элементы подряд. Связный список — цепочка отдельных блоков-узлов: в каждом значение и указатель на следующий узел. У последнего next == NULL; весь список — указатель на первый узел (голову), пустой список — NULL.
typedef struct Node {
int value;
struct Node *next; /* тип Node ещё не объявлен — пишем struct Node */
} Node;
/* В начало за O(1): голова меняется — передаём указатель на неё */
bool push_front(Node **head, int value)
{
Node *n = malloc(sizeof *n);
if (n == NULL) {
return false;
}
n->value = value;
n->next = *head;
*head = n;
return true;
}
/* Удалить первый узел со значением; указатель на ссылку убирает особый случай головы */
bool remove_value(Node **head, int value)
{
Node **link = head;
while (*link != NULL && (*link)->value != value) {
link = &(*link)->next;
}
if (*link == NULL) {
return false;
}
Node *victim = *link;
*link = victim->next; /* обойти узел: перецепить ссылку */
free(victim);
return true;
}
/* Освободить все узлы: next запомнить до free */
void free_list(Node **head)
{
Node *p = *head;
while (p != NULL) {
Node *next = p->next;
free(p);
p = next;
}
*head = NULL;
}
push_front: 30 -> 20 -> 10 -> NULL
push_back: 30 -> 20 -> 10 -> 5 -> 7 -> NULL
find 10: yes, find 99: no
remove 30 5 7: 20 -> 10 -> NULL
reverse: 2 -> 1 -> 10 -> 20 -> NULL
backward: 20 10 1 2
free_list: NULL
Node **. Функция, которая может изменить голову (вставка в начало, удаление первого узла), получает адрес переменной head. А указатель link на поле next (или на саму head) позволяет удалять и вставлять одним кодом и в начале, и в середине.for (p = head; p != NULL; p = p->next). Цикл free_list сохраняет next до free(p): после free поле читать нельзя — это висячий указатель.prev, cur, next: каждый узел перецепляется на предыдущий. Новых узлов нет, O(n).print_backward для миллиона узлов переполнит стек (раздел 16).tail): тогда O(1).| Операция | Динамический массив | Односвязный список |
|---|---|---|
| доступ к i-му | O(1) | O(i) — идти от головы |
| в конец | O(1) в среднем | O(1) с tail, иначе O(n) |
| в начало, удалить первый | O(n) — сдвиг | O(1) |
| вставить / удалить в известном месте | O(n) — сдвиг | O(1) — перецепить ссылки |
| поиск, двоичный поиск | O(n); в отсортированном O(log n) | O(n); двоичного нет |
| память на элемент | элемент (+ запас ёмкости) | элемент + указатель + служебные байты блока |
На практике массив обычно быстрее даже там, где у списка лучше оценка: элементы лежат подряд, и процессор читает их из кэша, а узлы списка разбросаны по памяти. Список выбирают, когда важны вставки и удаления в середине при уже найденной позиции и неподвижность узлов: указатель на узел не портится, когда список растёт.
Это не способы хранения, а правила доступа:
push кладёт наверх, pop снимает сверху. Стопка тарелок, кнопка «Назад» в браузере, отмена действий, стек вызовов функций.enqueue ставит в конец, dequeue берёт из начала. Очередь печати, обработка запросов по порядку, обход в ширину (раздел 24).Стек удобно делать на динамическом массиве: вершина — data[size − 1], обе операции O(1). Очередь на массиве со сдвигом после каждого dequeue стоила бы O(n); вместо сдвига используют кольцевой буфер: начало head движется вперёд, а индексы берутся по модулю ёмкости.
typedef enum { OK, ERR_MEMORY, ERR_EMPTY, ERR_FULL } Status;
typedef struct {
int *data;
size_t head; /* индекс первого элемента */
size_t count;
size_t capacity;
} Queue;
Status enqueue(Queue *q, int value)
{
if (q->count == q->capacity) {
return ERR_FULL;
}
q->data[(q->head + q->count) % q->capacity] = value; /* конец — за последним, по кругу */
q->count++;
return OK;
}
Status dequeue(Queue *q, int *value)
{
if (q->count == 0) {
return ERR_EMPTY;
}
*value = q->data[q->head];
q->head = (q->head + 1) % q->capacity;
q->count--;
return OK;
}
queue of 4: in 1 in 2 in 3 in 4 FULL(5), out 1, out 2, in 6 7 (head = 2)
rest: 3 4 6 7
После двух dequeue начало сдвинулось на индекс 2, а 6 и 7 легли в освободившиеся ячейки 0 и 1 — по кругу. Очередь можно сделать и на списке: head для выдачи, tail для добавления, обе операции O(1), ограничения ёмкости нет.
Как оформлять функции. У pop и dequeue два результата: значение и признак успеха (пустая очередь — не значение). Принятый в C способ (раздел 15): функция возвращает код ошибки, значение кладёт по указателю. Перечисление Status делает коды читаемыми, и вызывающий решает, что делать при ошибке. exit внутри функции структуры данных — крайняя мера (как в array_push выше, где на нехватку памяти учебной программе нечего ответить): библиотечная функция не должна решать за программу, завершаться ли ей.
Проверка скобок. Открывающую скобку кладём в стек; закрывающая должна совпасть по виду со снятой сверху. Ошибка — если стек пуст при закрывающей, вид не совпал или в конце в стеке что-то осталось. Позиции считаются с 0:
a[i] = (b + c) * {d} ok
f(a[i)] error at 5 ← ')' закрывает '['
((x) error at 4 ← конец строки, '(' не закрыта
x) error at 1 ← закрывать нечего
Обратная польская запись (ОПН, постфиксная): знак операции ставится после операндов — 3 4 + 2 * вместо (3 + 4) * 2. Скобки и приоритеты не нужны, а вычисление — один проход со стеком: число кладётся в стек, операция снимает два верхних (сначала правый операнд, затем левый) и кладёт результат. В конце в стеке ровно одно число — ответ.
Status eval_rpn(const char *expr, long *result); /* код ошибки + результат через указатель */
3 4 + 2 * = 14
5 1 2 + 4 * + 3 - = 14
2 3 error: too many operands
1 + error: too few operands
4 0 / error: division by zero
2 x * error: bad token
Обычную (инфиксную) запись в ОПН переводит алгоритм сортировочной станции Дейкстры — тоже стеком, но для операций: число сразу уходит в выход, операция выталкивает из стека операции не ниже своего приоритета, скобка «(» кладётся в стек, «)» выталкивает всё до «(». Так работают калькуляторы и компиляторы. Код примеров — examples/17/stack_queue.c.
malloc/calloc/realloc выделяют, free возвращает; размер — n * sizeof *pNULL; realloc — во временный указательfree, выход за блок часто не видны в выводе — их ловит ASan (-fsanitize=address); после free — p = NULLdata, size, capacity; рост вдвое даёт добавление за амортизированное O(1), рост на константу — O(n²) копийnext; вставка и удаление в известном месте O(1), доступ по номеру O(n); Node ** — когда меняется головаnext до freestatic; раздел 16 — стек вызовов и рекурсия. Нужен для: раздела 18 — модуль «стек» в отдельных файлах; раздела 19 — хеш-таблица на списках и дерево поиска из узлов (ПЗ №18, 19); раздела 20 — new/delete и std::vector вместо ручного realloc; раздела 21 — динамический массив как класс с деструктором; раздела 24 — очередь для обхода графа в ширину.