Каждая переменная в программе на C где-то живёт, и где именно определяет две вещи, которые потом не изменить: сколько она проживёт и сколько её может быть. C даёт вам три области памяти, и неверный выбор приводит либо к падению, либо к утечке. Эта страница раскладывает их по полочкам и показывает классическую ошибку, возникающую из-за путаницы со временем жизни.
Три области
старшие адреса
+---------------------------+
| стек | локальные, параметры, адреса возврата
| растёт вниз | | освобождается автоматически при возврате
| v |
+---------------------------+
| (неиспользуемый зазор)|
+---------------------------+
| ^ |
| растёт вверх | |
| куча | блоки malloc / calloc / realloc
+---------------------------+ освобождается только через free()
| статические / глобальные | глобальные и static, весь запуск
+---------------------------+
| код (text) | машинный код, только для чтения
+---------------------------+
младшие адреса
- Автоматическая память (стек) хранит параметры функций и нестатические локальные переменные. Блок стека занимается при входе в функцию и освобождается при возврате. Размер фиксируется на этапе компиляции.
- Динамическая память (куча) хранит всё, что пришло от
malloc,callocиrealloc. Размер решается во время выполнения; время жизни заканчивается только приfree. - Статическая память хранит глобальные переменные и всё, объявленное как
static. Она существует весь запуск программы и обнуляется до стартаmain.
Расположение адресов на схеме — обычная практика, а не гарантия: стандарт описывает времена жизни, а не разметку памяти.
Автоматическая память в действии
Каждый вызов demo получает свежие local и table. Ничего не освобождается вручную, ничто не может утечь, а выделение стоит одной инструкции, сдвигающей указатель стека. Именно поэтому обычные локальные переменные должны быть вашим выбором по умолчанию: это самая быстрая и самая безопасная память в C.
Загвоздка — в закрывающей фигурной скобке. Как только она отработала, этой памяти больше нет.
Висячий указатель
Вот ошибка, которую каждый программист на C пишет однажды:
/* СЛОМАНО: возвращает адрес памяти, которой больше не существует */
int *make_counter(void) {
int count = 0;
return &count; /* count умирает на этой скобке */
}
int main(void) {
int *p = make_counter();
*p = 5; /* запись в мёртвый кадр стека */
return 0;
}
&count был совершенно корректным адресом, пока make_counter работала. При возврате это место на стеке отдаётся той функции, которая будет вызвана следующей, так что p теперь указывает на чужие локальные переменные. Чтение даёт мусор; запись их портит. GCC и Clang предупреждают ровно об этой конструкции (-Wreturn-local-addr), так что компилируйте с включёнными предупреждениями.
Та же ошибка маскируется в случае с массивами, и там предупреждение часто не срабатывает:
Сломанная версия этой функции собирала бы текст в локальном char buf[64] и делала return buf; — возвращая адрес буфера, который перестаёт существовать в тот же самый миг.
Три способа это исправить
1. Буфер предоставляет вызывающий (показано выше). Никакого выделения памяти, никаких вопросов о владении и самый распространённый стиль в библиотеках C. Функция принимает размер, чтобы не выйти за его пределы.
2. Возвращать память из кучи и явно говорить, кто её освобождает.
Блок в куче переживает функцию по замыслу — в этом весь смысл динамической памяти. Цена — комментарий о владении и free на стороне вызывающего.
3. Использовать статическую память, когда единственный общий буфер приемлем:
static внутри функции оставляет область видимости переменной локальной, давая ей при этом время жизни программы, поэтому возвращать её адрес законно. Расплата в том, что она всегда одна: её разделяют все вызывающие, из-за чего этот приём непригоден в многопоточном коде и удивителен даже в однопоточном, когда два вызывающих держат указатель одновременно.
Размер: где заканчивается стек
Место на стеке мало и фиксировано. Главный поток обычно получает 1 МБ в Windows и 8 МБ в Linux; порождённый поток часто получает намного меньше. Куча ограничена доступной памятью системы.
void bad(void) {
int huge[1000000]; /* ~4 МБ стека — скорее всего падение на входе */
huge[0] = 1;
}
Здесь нет ни диагностики, ни NULL, который можно проверить: программа просто умирает, обычно с ошибкой сегментации, ещё до первой строки тела функции. Вариант с кучей сообщает о неудаче как положено:
Глубокая рекурсия исчерпывает стек точно так же, по одному кадру за раз: неуправляемая рекурсивная функция — самая частая причина переполнения стека на практике.
Цена и локальность
Выделение на стеке — это одна арифметическая операция над регистром. Выделение в куче — это вызов библиотечной функции, которая ищет подходящий блок, может взять блокировку и время от времени просит у операционной системы ещё памяти. В горячем цикле эта разница измерима.
Данные на стеке к тому же компактны и недавно использовались, поэтому они обычно лежат в кэше. Блоки кучи могут быть разбросаны. Ни один из этих фактов не должен сам по себе определять архитектуру — корректность времени жизни важнее, — но из двух одинаково корректных решений вариант со стеком обычно быстрее.
Как увидеть области
Печать адресов делает разметку наглядной. Точные значения различаются от запуска к запуску (современные системы их рандомизируют), но группировка видна:
Глобальная и статическая переменные лежат рядом; блок кучи — в другом месте; локальная переменная обычно далеко от обеих. Приводите к void * для %p — именно этого требует спецификатор формата.
Выбор
Используйте стек, когда:
- размер известен на этапе компиляции,
- данные нужны только внутри этой функции и тех, которые она вызывает,
- и они невелики — несколько килобайт, а не мегабайт.
Используйте кучу, когда:
- размер зависит от ввода, файла или вычисления,
- данные должны пережить создавшую их функцию,
- или они достаточно велики, чтобы угрожать пределу стека.
Используйте статическую память, когда:
- ровно один экземпляр должен существовать всю программу,
- и совместное использование его всеми вызывающими действительно корректно.
По умолчанию — стек. Тянитесь к куче, когда применима одна из трёх её причин, а когда тянетесь, соблюдайте правила владения из утечек памяти, чтобы блок, ради времени жизни которого всё затевалось, всё-таки был освобождён.
Две зеркальные ошибки
Их стоит назвать вместе, потому что это один и тот же вопрос о времени жизни с двумя разными ответами:
- Висячий указатель — память умерла раньше указателя. Возврат
&localили использование указателя послеfree. Программа читает или пишет память, которая теперь принадлежит чему-то другому. - Утечка памяти — указатель умер раньше памяти. Потеря последней ссылки на блок
malloc. Сразу ничего не ломается; процесс просто растёт.
Обе происходят из несоответствия между тем, сколько данные должны жить, и тем, в какую область вы их положили. Сначала решите вопрос времени жизни, и область определится сама.
Часто задаваемые вопросы
В чём разница между стеком и кучей в C?
Стек хранит локальные переменные: их размер определяет компилятор, они создаются при входе в функцию и уничтожаются при возврате, а выделение не стоит ничего. Куча хранит блоки malloc: размер вы выбираете во время выполнения, блок живёт до вызова free, и выделение имеет реальную цену.
Почему нельзя возвращать указатель на локальную переменную в C?
Потому что память локальной переменной освобождается в тот момент, когда функция возвращает управление. Указатель по-прежнему хранит этот адрес, но память теперь принадлежит следующему вызову функции — чтение даёт мусор, запись портит посторонние данные. Это и есть висячий указатель. Возвращайте блок malloc или пусть буфер предоставляет вызывающий.
Какого размера стек в C?
Обычно от 1 до 8 МБ для главного потока и гораздо меньше для дополнительных потоков — настолько мало, что локальный int big[1000000]; обычно роняет программу прямо на входе в функцию. Куча ограничена доступной памятью системы, поэтому большие данные или данные неизвестного размера место именно там.
Когда в C стоит использовать кучу вместо стека?
В трёх случаях: размер неизвестен до времени выполнения, данные должны пережить создавшую их функцию или блок слишком велик для стека (грубо говоря, всё, что больше нескольких сотен килобайт). Всё остальное должно быть обычной локальной переменной — это быстрее и не может утечь.