Память — storage и lifetime не одно и то же
C++ — почти единственный современный язык, где разработчик сам решает, где живёт объект, кто за него отвечает и когда он умирает. Никакого GC, никакого "освободится потом". Ошибка владения или lifetime в C++ не ловится исключением — она оборачивается Undefined Behavior: повреждённые данные, случайные падения или баг, который воспроизводится только на проде.
Главная идея, которую C++ настойчиво отделяет от других языков: storage (сырые байты, где может лежать объект) и lifetime (период, когда там реально существует объект данного типа) — это разные сущности. Байты могут оставаться на месте после того, как объект уничтожен; виртуальный адрес может быть зарезервирован до того, как ОС подмапит физическую страницу. Поэтому «у меня не упало» — не аргумент о корректности.
Стандарт даёт четыре storage duration (automatic / static / thread / dynamic), две формы передачи без владения (T*, T&) и явные способы выразить владение (unique_ptr, shared_ptr, RAII-объекты). Платформа добавляет реальную карту процесса (стек, куча, секции, виртуальные страницы) и правила выравнивания. Полная карта — в слоях ниже.
Карта темы
- Модель памяти C++ — storage / lifetime / scope / ownership, четыре storage duration, разница со stack/heap.
- Виртуальная память и страницы ОС — карта процесса (stack / heap / .bss / .data / .rodata / .text), ASLR, mmap, страницы и page faults.
- Указатели и ссылки —
T*vsT&, чем они отличаются и почему ни один сам по себе не означает владения. - Динамическая память —
new/delete,malloc/free, RAII, умные указатели; почемуnew[]↔delete[]. - Выравнивание —
alignof,alignas, padding в структурах, влияние порядка полей наsizeof. - Хранение std::string и SSO — где живёт сам объект, где буфер, что делает Small String Optimization.
- volatile и const volatile — что
volatileНЕ делает; почему это не заменаstd::atomic. - Оптимизация памяти — арены, пулы, выравнивание под кэш-линии, false sharing, SoA/AoS.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Смешать new/delete с malloc/free | UB — разные семейства API, разные runtime-структуры |
delete массива, созданного через new[] | UB; деструкторы остальных элементов не вызовутся |
Создать два shared_ptr из одного raw pointer | Double free — оба control block-а считают, что владеют |
Считать volatile потокобезопасным | Гонки сохраняются; нужен std::atomic или мьютекс |
| Вернуть указатель/ссылку на локальную переменную | Dangling — UB при использовании, иногда "работает" случайно |
| Полагаться на падение при use-after-free | Аллокатор оставил блок в free-list — UB читается тихо |
Зависеть от точного размера SSO или layout .data/.bss | Платформенно-зависимо; ломается при апгрейде stdlib или компилятора |
Передавать T* без проговаривания владения в типе | Интерфейс не виден на review: непонятно, кто delete-нет |
new T[n] без std::vector/unique_ptr | Ручной delete[] обязателен, и любое исключение по пути — утечка |
| Захватить ссылку на временный объект | Dangling после конца выражения; продление жизни только для именованных const& |
Значение для собеседований
Память — обязательная тема почти на каждом C++-интервью. Интервьюер проверяет не «знаешь ли ты слово heap», а умеешь ли ты рассуждать о времени жизни и владении.
Что обычно проверяют:
- Storage duration, lifetime и scope — что это разные вещи, не синонимы.
new/deletevsmalloc/free— конструктор/деструктор vs сырые байты, нельзя смешивать.- Почему RAII и умные указатели решают проблему владения, а raw pointer — нет.
- Use-after-free как UB, даже если код «работает».
- Почему
volatileне заменяетstd::atomicдля синхронизации потоков. - Padding,
alignof, влияние порядка полей наsizeof. - Где живёт
std::stringи что делает SSO; гдеconst char*литералы. - Что произойдёт, если создать два
shared_ptrиз одного raw pointer.
Типичный неверный ответ: «volatile int — это потокобезопасный счётчик, как Atomic в Java/C#». Это запускает обсуждение того, что volatile в C++ нужен для memory-mapped IO и signal handlers, и не даёт никаких гарантий синхронизации между потоками.