Правильное использование стека и кучи в C ++?


122

Я занимаюсь программированием некоторое время, но в основном это были Java и C #. На самом деле мне никогда не приходилось управлять памятью самостоятельно. Я недавно начал программировать на C ++, и я немного не понимаю, когда мне следует хранить вещи в стеке, а когда - в куче.

Я понимаю, что переменные, к которым осуществляется очень частый доступ, должны храниться в стеке, а объекты, редко используемые переменные и большие структуры данных должны храниться в куче. Это правильно или я не прав?


Ответы:


242

Нет, разница между стеком и кучей не в производительности. Это срок службы: любая локальная переменная внутри функции (все, что вы не используете malloc () или new) живет в стеке. Он уходит, когда вы возвращаетесь из функции. Если вы хотите, чтобы что-то проживало дольше, чем функция, которая его объявила, вы должны разместить это в куче.

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

Для более четкого понимания того, что такое стек, подойдите к нему с другого конца - вместо того, чтобы пытаться понять, что делает стек с точки зрения языка высокого уровня, найдите «стек вызовов» и «соглашение о вызовах» и посмотрите, что машина действительно делает это, когда вы вызываете функцию. Компьютерная память - это просто набор адресов; «куча» и «стек» - изобретения компилятора.


7
Можно с уверенностью добавить, что информация переменного размера обычно хранится в куче. Единственные исключения, о которых я знаю, - это VLA в C99 (которая имеет ограниченную поддержку) и функция alloca (), которую часто неправильно понимают даже программисты на C.
Дэн Олсон

10
Хорошее объяснение, хотя в многопоточном сценарии с частым распределением и / или освобождением кучи является предметом спора, что влияет на производительность. Тем не менее, объем почти всегда является решающим фактором.
peterchen 01

18
Конечно, и new / malloc () сам по себе является медленной операцией, и стек с большей вероятностью находится в dcache, чем в произвольной строке кучи. Это реальные соображения, но обычно второстепенные по отношению к вопросу о продолжительности жизни.
Crashworks 01

1
Верно ли, что «компьютерная память - это просто набор адресов;« куча »и« стек »- изобретения компилятора» ?? Я читал во многих местах, что стек - это особая область памяти нашего компьютера.
Vineeth Chitteti

2
@kai Это способ визуализировать это, но с физической точки зрения это не обязательно верно. ОС отвечает за выделение стека и кучи приложения. Компилятор также несет ответственность, но в первую очередь он полагается на ОС. Стек ограничен, а куча - нет. Это связано с тем, как ОС обрабатывает сортировку этих адресов памяти во что-то более структурированное, чтобы несколько приложений могли работать в одной системе. Heap и stack - не единственные, но обычно они единственные, которые беспокоят большинство разработчиков.
tsturzl

42

Я бы сказал:

Сохраните его в стеке, если МОЖЕТЕ.

Если НЕОБХОДИМО, храните его в куче.

Поэтому предпочитайте стек куче. Вот несколько возможных причин, по которым вы не можете хранить что-либо в стеке:

  • Он слишком велик - в многопоточных программах в 32-битной ОС стек имеет небольшой и фиксированный (по крайней мере, во время создания потока) размер (обычно всего несколько мегабайт). Это позволяет создавать множество потоков, не исчерпывая адреса. пространство. Для 64-битных программ или однопоточных программ (в любом случае Linux) это не является серьезной проблемой. В 32-битном Linux однопоточные программы обычно используют динамические стеки, которые могут расти, пока не достигнут вершины кучи.
  • Вам нужно получить к нему доступ за пределами исходного кадра стека - это действительно основная причина.

С помощью разумных компиляторов можно размещать объекты нефиксированного размера в куче (обычно это массивы, размер которых неизвестен во время компиляции).


1
Все, что превышает пару КБ, обычно лучше помещать в кучу. Я не знаю подробностей, но не припомню, чтобы когда-либо работал со стеком размером «несколько мегабайт».
Дэн Олсон

2
Это то, чем я бы не стал беспокоить пользователя вначале. Для пользователя кажется, что векторы и списки размещены в стеке, даже если STL хранит содержимое в куче. Похоже, что вопрос больше касался решения, когда явно вызывать new / delete.
Дэвид Родригес - дрибес, 01

1
Дэн: Я положил 2 гига (да, G как в GIGS) в стек под 32-битным Linux. Ограничения стека зависят от ОС.
Мистер Ри, 01

6
mrree: Стек Nintendo DS составляет 16 килобайт. Некоторые ограничения стека зависят от оборудования.
Ant

Ant: Все стеки зависят от оборудования, ОС, а также от компилятора.
Viliami

24

Это более тонко, чем предполагают другие ответы. Нет абсолютного разделения между данными в стеке и данными в куче в зависимости от того, как вы их объявляете. Например:

std::vector<int> v(10);

В теле функции, которая объявляет vector(динамический массив) из десяти целых чисел в стеке. Но хранилище, которым управляет, vectorотсутствует в стеке.

Ах, но (другие ответы предполагают) время жизни этого хранилища ограничено временем жизни самого vectorсебя, которое здесь основано на стеке, поэтому не имеет значения, как оно реализовано - мы можем рассматривать его только как объект на основе стека с семантикой значений.

Не так. Предположим, функция была:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

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

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

Если вы это сделаете, вы можете думать обо всех переменных в своей программе, как если бы они были простыми типами значений, и вообще забыть о куче (кроме случаев написания нового класса-оболочки, подобного значению, для некоторых данных кучи, что должно быть необычным) ,

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

a = b;

поменяйте их местами так:

a.swap(b);

потому что это намного быстрее и не вызывает исключений. Единственное требование состоит в том, что вам не нужно bпродолжать хранить то же самое значение ( aвместо этого оно получит значение, которое будет потеряно a = b).

Обратной стороной является то, что этот подход заставляет вас возвращать значения из функций через выходные параметры вместо фактического возвращаемого значения. Но они исправляют это в C ++ 0x с помощью ссылок rvalue .

В самых сложных ситуациях вы довели бы эту идею до крайности и использовали бы класс интеллектуального указателя, такой как тот, shared_ptrкоторый уже находится в tr1. (Хотя я бы сказал, что если вам это нужно, вы, возможно, вышли за пределы сладкого места применимости Стандартного C ++.)


6

Вы также должны сохранить элемент в куче, если его нужно использовать за пределами функции, в которой он создан. Одна идиома, используемая с объектами стека, называется RAII - это включает использование объекта на основе стека в качестве оболочки для ресурса, когда объект уничтожается, ресурс будет очищен. Объекты на основе стека легче отслеживать, когда вы можете генерировать исключения - вам не нужно беспокоиться об удалении объекта на основе кучи в обработчике исключений. Вот почему необработанные указатели обычно не используются в современном C ++, вы должны использовать интеллектуальный указатель, который может быть оболочкой на основе стека для необработанного указателя на объект на основе кучи.


5

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

Выделение в куче требует отслеживания блока памяти, что не является операцией с постоянным временем (и требует некоторых циклов и накладных расходов). Это может замедлиться, поскольку память становится фрагментированной и / или вы приближаетесь к использованию 100% вашего адресного пространства. С другой стороны, выделение стека - это постоянное время, в основном «свободные» операции.

Еще одна вещь, которую следует учитывать (опять же, действительно важно, только если это становится проблемой), заключается в том, что обычно размер стека является фиксированным и может быть намного меньше размера кучи. Поэтому, если вы выделяете большие объекты или много мелких объектов, вы, вероятно, захотите использовать кучу; если у вас закончится место в стеке, среда выполнения выдаст исключительное исключение сайта. Обычно это не имеет большого значения, но стоит подумать о другом.


И куча, и стек представляют собой выгружаемую виртуальную память. Время поиска в куче невероятно быстро по сравнению с тем, что требуется для отображения в новой памяти. Под 32-битным Linux я могу разместить в стеке> 2 гига. Под Mac, я думаю, стек жестко ограничен 65Meg.
Мистер Ри, 01

3

Стек более эффективен и проще в управлении данными с заданной областью.

Но кучу следует использовать для всего, что превышает несколько КБ (в C ++ это просто, просто создайте boost::scoped_ptrв стеке указатель на выделенную память).

Рассмотрим рекурсивный алгоритм, который постоянно обращается к себе. Очень сложно ограничить или угадать общее использование стека! В то время как в куче распределитель ( malloc()или new) может указывать на нехватку памяти, возвращая NULLили throwing.

Источник : ядро ​​Linux, размер стека которого не превышает 8 КБ!


Для справки для других читателей: (A) «Следует» здесь является чисто личным мнением пользователя, составленным в лучшем случае из 1 цитирования и 1 сценария, с которым многие пользователи вряд ли столкнутся (рекурсия). Кроме того, (B) стандартная библиотека предоставляет std::unique_ptr, что должно быть предпочтительнее любой внешней библиотеки, такой как Boost (хотя со временем это все-таки приводит к стандарту).
underscore_d


1

Выбор того, следует ли выделить в куче или в стеке, делается за вас, в зависимости от того, как выделяется ваша переменная. Если вы выделяете что-то динамически, используя «новый» вызов, вы распределяете его из кучи. Если вы выделяете что-то как глобальную переменную или как параметр в функции, оно размещается в стеке.


4
Я подозреваю, что он спрашивал, когда складывать вещи в кучу, а не как.
Стив Роу,

0

На мой взгляд, есть два решающих фактора

1) Scope of variable
2) Performance.

Я бы предпочел использовать стек в большинстве случаев, но если вам нужен доступ к переменной вне области видимости, вы можете использовать кучу.

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


0

вероятно, на это был дан довольно хороший ответ. Я хотел бы указать вам на приведенную ниже серию статей, чтобы лучше понять низкоуровневые детали. У Алекса Дарби есть серия статей, в которых он знакомит вас с отладчиком. Вот часть 3 о стеке. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/


Ссылка кажется мертвой, но проверка Internet Archive Wayback Machine показывает, что она говорит только о стеке и, следовательно, ничего не делает, чтобы ответить на конкретный вопрос здесь о стеке по сравнению с кучей . -1
underscore_d
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.