Типы памяти в C++

Переменные и константы могут хранится в 3 местах: stack, heap (куча), data/bss segments.

Stack

Что хранится

  • Локальные переменные (включая аргументы функции)
  • Память, выделенная alloca()

Как работает

Концептуально так же как и одноименная структура данных. Есть последовательный блок памяти и указатель на конец уже используемой области.

При выделении памяти для новой переменной происходит увеличение этого указателя на размер переменной. Сама переменная использует память между старым и новым значением указателя.

При освобождении памяти этой переменной указатель возвращается на свое исходное значение.

Из-за того что выделение/освобождение памяти происходит путем изменения одной переменной, создание переменных на стеке крайне производительно.

Layout

Стек находится в одном последовательном блоке виртуальной памяти. У каждого потока свой стек.

Размер

В мейнстримных ОС размер фиксированный и определяется настройками системы. Типичный размер 1-10МБ.

Из-за такого скромного размера стек не подходит для хранения больших объектов.

Примеры

void function(float x, float y) {
    // аргументы x, y будут созданы на стеке при вызове этой функции
    int data = 1;
    // локальная переменная data тоже попадает на стек
}

Что может пойти не так

Переполнение стека (stack overflow) - происходит при попытке выделить количество памяти, превыщающее фиксированный размер стека. Рантайм C++ не контролирует переполнение стека, соответственно средствами языка поймать и обработать такую ошибку нельзя. Правильный подход это избегать кода, который использует неконтролируемо много памяти на стеке. Сам по себе вызов функции (по крайней мере не заинлайненной) тоже увеличивает использование стека. Рекурсивный вызов функции может привести к переполнению стека, если никак не ограничивать глубину рекурсии.

Использование после конца жизни переменной - может произойти в следующих ситуациях:

  • Сохранение ссылки на эту переменную в объекте живущем дольше этой переменной.
  • Использование ссылки на эту переменную в другом потоке.

Heap (куча)

Что хранится

  • Переменные созданные в динамической памяти (malloc, new, make_{shared,unique})

Как работает

При запросе на выделение памяти ОС ищет участок в адресном пространстве процесса, который с одной стороны достаточно большой, а с другой еще не занят другим объектом.

При освобождении памяти выделенный участок помечается как свободный, и может быть переиспользован для других объектов.

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

Производительность выделения/освобождения памяти в куче на порядки (это не преувеличение!) хуже стека.

Layout

Выделенная память может быть разбросана по адресному пространству в любом порядке.

Размер

Нету фиксированного размера. Процесс может запрашивать больше памяти в процессе выполнения. Есть мягкий лимит в виде размера свободной ОЗУ, однако по факту достижение его не всегда приводит к ошибке, потому что ОС может выгружать часть памяти, используемой процессом, на жесткий диск. Жесткий лимит - размер адресного пространства процесса, обычно это 2^{32,64}-1 байта, в зависимости от битности системы.

Примеры

// выделение памяти под 1 флоат на куче
float* x = (float*)malloc(sizeof(float));
// освобождение выделенной памяти
free(x);

// выделение памяти под 1 инт на куче - new вызовет malloc и проинициализирует единицей
int* y = new int(1);
// освобождение выделенной памяти - delete вызовет free
delete y;

//
{
    // double создается на куче, указатель ptr создается на стеке
    std::shared_ptr<double> ptr = std::make_shared<double>(1.0);
}   // по выходу исполнения из области { } переменная ptr будет удалена, а деструктор shared_ptr автоматически освободит память выделенную под double

Что может пойти не так

  • Утечка памяти - выделенная память не удаляется, или удаляется не полностью. Со временем может привести к повышенному потреблению памяти.
  • Использование после деаллокации - попытка использовать ранее выделенную память после того, как она освобождена. Приводит к крашу.
  • Фрагментация памяти - по скольку аллокации и деаллокации происходят в неупорядоченном порядке, в отличии от стека, то со временем в долго работающих процессах адресное пространство будет фрагментироваться. Это может привести к потреблению большего количества памяти, чем вы выделили, а так же к невозможности выделить большой блок памяти даже когда он меньше свободной в данной момент памяти.
  • Попытка выделить слишком много памяти - обрабатываемая ошибка, в зависимости от используемого API. malloc вернет nullptr, new выкинет std::bad_alloc.

Data/bss segments

Что хранится

  • Глобальные переменные: инициализированные в data, неинициализированные в bss.

Как работает

Количество глобальных переменных известно заранее, поэтому память под них выделяется в специальных областях программы, отдельными и от стека и от кучи.

Выделять или освобождать память из этой области не надо, да и невозможно.

Отличие data от bss в том, что data занимает место на диске в составе бинарника (до запуска программы), а bss нет.

Примеры

// глобальная инициализированная переменная x будет помещена в data сегмент
int x = 1;

int main() {
    // статическая инициализированная константа x будет помещена в rodata (read only data) сегмент
    static const int y = 2;
}

Еще контент

Подписывайтесь на мой телеграм канал, я выкладываю новости из мира C++ и авторские статьи по типу этой: t.me/krestovii_podhod