контейнеры и память — устройство и практикаcontainers and memory — internals and practice

Стек vs КучаStack vs Heap

Разница не в «где переменная объявлена», а в том, что стек — это просто сдвиг одного указателя, а куча — вызов аллокатора, который должен найти подходящий свободный блок, возможно среди тысяч других. Это объясняет и разницу в скорости на порядки, и то, почему куча (а не стек) фрагментируется.The difference isn't "where the variable is declared" — it's that the stack is just moving one pointer, while the heap is a call into an allocator that has to find a suitable free block, possibly among thousands of others. That explains both the order-of-magnitude speed gap and why the heap (not the stack) fragments.

фундаментfoundational частый вопросcommon interview question

ОпределениеDefinition

Стек — область памяти для локальных переменных и служебной информации вызовов функций (адрес возврата, сохранённые регистры), растущая и сжимающаяся строго по дисциплине LIFO вместе со стеком вызовов. Куча — область памяти для объектов с временем жизни, не привязанным к области видимости, управляемая явно через new/delete (или malloc/free) и аллокатором времени выполнения.The stack is the memory region for local variables and call bookkeeping (return address, saved registers), growing and shrinking strictly LIFO in lockstep with the call stack. The heap is the memory region for objects whose lifetime isn't tied to scope, managed explicitly via new/delete (or malloc/free) and a runtime allocator.

void f() {
    int local = 42;             // стек: живёт до конца f()// stack: lives until f() returns
    int* heap = new int(42);   // куча: живёт до delete// heap: lives until delete
    delete heap;
}                                // local уничтожается автоматически здесь// local is destroyed automatically here

Скорость выделенияAllocation speed

Выделение на стеке — это сдвиг одного регистра (stack pointer) на этапе компиляции известным смещением; часто это вообще не отдельная инструкция, а часть пролога функции. Стоимость не зависит от размера объекта и не требует обращения к какой-либо структуре данных.Stack allocation is moving one register (the stack pointer) by a compile-time-known offset; often it isn't even a separate instruction, just part of the function prologue. The cost doesn't depend on the object's size and requires no lookup into any data structure.

// упрощённо — то, что делает компилятор для локальных переменных// simplified — what the compiler does for locals
sub rsp, 16      ; "выделить" 16 байт на стеке; "allocate" 16 bytes on the stack
; ... использование ...; ... usage ...
add rsp, 16      ; "освободить" — тоже одна инструкция; "free" — also one instruction

Выделение на куче — вызов аллокатора (`operator new` → обычно `malloc`), который должен: найти свободный блок подходящего размера (пройдя по free-list или дереву блоков), возможно разбить больший блок на части, обновить внутренние метаданные аллокатора, и в общем случае — потенциально сделать системный вызов (`sbrk`/`mmap`), если внутренний пул исчерпан. Это на 1-2 порядка медленнее, и время выделения не гарантированно постоянно — зависит от текущего состояния кучи.Heap allocation is a call into the allocator (`operator new` → typically `malloc`), which must: find a free block of a suitable size (walking a free list or block tree), possibly split a larger block, update the allocator's internal bookkeeping, and in general potentially make a system call (`sbrk`/`mmap`) if the internal pool is exhausted. This is 1-2 orders of magnitude slower, and the allocation time isn't guaranteed constant — it depends on the heap's current state.

Время жизни и инвалидацияLifetime and invalidation

Стековый объект уничтожается автоматически и детерминированно при выходе из области видимости — компилятор точно знает точку уничтожения на этапе компиляции. Куча даёт время жизни, независимое от области видимости, но платой становится ответственность программиста (или smart pointer-а) за то, чтобы delete случился ровно один раз и не раньше последнего использования.A stack object is destroyed automatically and deterministically at scope exit — the compiler knows the destruction point exactly at compile time. The heap gives lifetime independent of scope, but the cost is that the programmer (or a smart pointer) is responsible for making sure delete happens exactly once and no earlier than the last use.

возврат указателя на локальную переменнуюreturning a pointer to a local
int* bad() {
    int x = 10;
    return &x;           // x уничтожен на выходе из bad() — висячий указатель// x is destroyed on exit from bad() — dangling pointer
}
объект в куче переживает функциюa heap object outlives the function
int* ok() {
    int* x = new int(10);
    return x;            // x жив, пока кто-то не вызовет delete// x is alive until someone calls delete
}

ФрагментацияFragmentation

Стек не фрагментируется в принципе — LIFO-дисциплина гарантирует, что освобождённая память всегда находится на "верхушке" и немедленно доступна для следующего выделения без поиска. Куча фрагментируется, потому что выделения и освобождения происходят в произвольном порядке: после многих циклов alloc/free остаётся россыпь свободных блоков вперемешку с занятыми, и даже при достаточном суммарном свободном объёме может не найтись одного достаточно большого непрерывного блока под новый запрос.The stack cannot fragment by construction — the LIFO discipline guarantees freed memory is always at the "top" and immediately available for the next allocation with no search. The heap fragments because allocations and frees happen in arbitrary order: after many alloc/free cycles you're left with a scatter of free blocks interleaved with live ones, and even with enough total free space, there may be no single contiguous block big enough for a new request.

Различают внешнюю фрагментацию (свободной памяти достаточно суммарно, но она разбросана мелкими кусками) и внутреннюю (аллокатор выдаёт блок больше запрошенного из-за выравнивания или размерных классов — разница простаивает). Долгоживущие процессы с нерегулярными размерами выделений (например, серверы с объектами переменного размера) особенно подвержены внешней фрагментации.There's a distinction between external fragmentation (enough free memory in total, but scattered in small pieces) and internal fragmentation (the allocator hands out a block larger than requested due to alignment or size classes — the difference sits idle). Long-running processes with irregular allocation sizes (e.g., servers with variable-sized objects) are especially prone to external fragmentation.

ГраблиGotchas

Переполнение стека — тоже "память кончилась", просто в другом местеStack overflow is also "out of memory," just in a different place

Размер стека фиксирован при старте потока (обычно 1-8 МБ, задаётся ОС/линкером) и не растёт динамически. Глубокая рекурсия или большие локальные массивы (int buf[1000000]; на стеке) переполняют его — крашатся без исключения, часто без внятного сообщения об ошибке, в отличие от std::bad_alloc при нехватке кучи.Stack size is fixed when a thread starts (typically 1-8 MB, set by the OS/linker) and doesn't grow dynamically. Deep recursion or large local arrays (int buf[1000000]; on the stack) overflow it — crashing without an exception, often with no clear error message, unlike std::bad_alloc on heap exhaustion.

"Стек быстрее" не значит "куча не нужна""Stack is faster" doesn't mean "the heap is unnecessary"

Стек не годится, когда время жизни объекта должно пережить текущую область видимости, когда размер неизвестен на этапе компиляции, или когда объект слишком велик для типичного лимита стека. Выбор — не оптимизация, а требование корректности; премature оптимизация в духе "засунуть всё на стек" ведёт к переполнению стека и UB.The stack doesn't work when an object's lifetime must outlive the current scope, when the size isn't known at compile time, or when the object is too large for a typical stack limit. The choice is a correctness requirement, not an optimization; "just put everything on the stack" premature optimization leads to stack overflow and UB.

Аллокаторы кучи борются с фрагментацией классами размеровHeap allocators fight fragmentation with size classes

Современные аллокаторы (tcmalloc, jemalloc, glibc ptmalloc) группируют выделения по фиксированным классам размеров (например, 16, 32, 48, 64 байта...), чтобы освобождённый блок конкретного размера сразу переиспользовался под запрос того же класса — это снижает внешнюю фрагментацию ценой небольшой внутренней.Modern allocators (tcmalloc, jemalloc, glibc ptmalloc) group allocations into fixed size classes (e.g., 16, 32, 48, 64 bytes...) so a freed block of a given size is immediately reusable for a request in the same class — this reduces external fragmentation at the cost of a little internal fragmentation.