Правильное использование стека и кучи в С++?
Я программировал некоторое время, но в основном это Java и С#. Мне никогда не приходилось управлять памятью самостоятельно. Я недавно начал программирование на С++, и я немного смущен, когда должен хранить вещи в стеке и когда их хранить в куче.
Мое понимание заключается в том, что переменные, к которым обращаются очень часто, должны храниться в стеке и объектах, редко используемые переменные, а большие структуры данных должны храниться в куче. Правильно ли это или я неверен?
Ответы
Ответ 1
Нет, разница между стеком и кучей не является производительностью. Продолжительность жизни: любая локальная переменная внутри функции (все, что вы не malloc() или новое) живет в стеке. Он исчезает, когда вы возвращаетесь из функции. Если вы хотите, чтобы что-то жило дольше, чем функция, объявившая его, вы должны выделить его в куче.
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;
}
Для более четкого понимания того, что такое стек, приходите к нему с другого конца - вместо того, чтобы пытаться понять, что делает стек с точки зрения языка высокого уровня, найдите "стек вызовов" и "вызов", и посмотреть, что машина действительно делает, когда вы вызываете функцию. Компьютерная память - это всего лишь серия адресов; "куча" и "стопка" - это изобретения компилятора.
Ответ 2
Я бы сказал:
Сохраните его в стеке, если вы МОЖЕТЕ.
Храните его в куче, если вам НУЖНО.
Поэтому предпочитайте стек кучи. Некоторые возможные причины, по которым вы не можете хранить что-то в стеке, следующие:
- Он слишком велик - для многопоточных программ на 32-битной ОС стек имеет небольшой и фиксированный (по крайней мере, время создания потока) размер (как правило, всего несколько мегабайт. Это значит, что вы можете создавать множество потоков без исчерпания адресного пространства. Для 64-битных программ или однопоточных (Linux в любом случае) программ это не является серьезной проблемой. В 32-разрядной Linux однопоточные программы обычно используют динамические стеки, которые могут продолжать расти, пока они не достигнут вершины куча.
- Вам нужно получить доступ к нему за пределами рамки исходного стека - это действительно главная причина.
Возможно, с разумными компиляторами выделять объекты нефиксированного размера в куче (обычно массивы, размер которых неизвестен во время компиляции).
Ответ 3
Это более тонкий, чем другие ответы. Абсолютного разрыва между данными в стеке и данными в куче нет, основываясь на том, как вы его объявляете. Например:
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
(и любой тип сложного значения должно иметь одно) может служить своего рода перезаписываемой ссылкой на некоторые данные кучи в рамках системы, которая гарантирует единственного владельца этих данных.
Поэтому современный подход на С++ никогда не хранит адрес данных кучи в голых переменных локального указателя. Все распределения кучи должны быть скрыты внутри классов.
Если вы это сделаете, вы можете думать обо всех переменных в своей программе, как если бы они были простыми типами значений, и вообще забыть о куче (кроме того, когда вы пишете новый класс-класс-оболочку для некоторых данных кучи, который должен быть необычным).
Вам просто нужно сохранить один специальный бит знаний, который поможет вам оптимизировать: где это возможно, вместо назначения одной переменной другому:
a = b;
замените их следующим образом:
a.swap(b);
потому что он намного быстрее и не генерирует исключений. Единственное требование состоит в том, что вам не нужно b
продолжать удерживать одно и то же значение (вместо этого оно получит значение a
, которое будет разбито на a = b
).
Недостатком является то, что этот подход заставляет вас возвращать значения из функций через выходные параметры вместо фактического возвращаемого значения. Но они фиксируют это в С++ 0x с ссылки rvalue.
В самых сложных ситуациях вы бы взяли эту идею в общую крайность и использовали класс интеллектуальных указателей, такой как shared_ptr
, который уже находится в tr1. (Хотя я бы сказал, что, если вам это кажется, вы, возможно, перешли за пределы стандартного С++ сладкого пятна применимости.)
Ответ 4
Вы также сохранили бы элемент в куче, если он должен использоваться вне области действия функции, в которой он создан. Одна идиома, используемая с объектами стека, называется RAII - это включает использование объекта на основе стека в качестве оболочки для ресурса, когда объект уничтожается, ресурс будет очищен. Объекты на основе стека легче отслеживать, когда вы можете бросать исключения - вам не нужно заботиться о том, чтобы удалить объект с кучей в обработчике исключений. Вот почему необработанные указатели обычно не используются в современном С++, вы бы использовали интеллектуальный указатель, который может быть оболочкой на основе стека для необработанного указателя на объект с кучей.
Ответ 5
Чтобы добавить к другим ответам, это также может быть связано с производительностью, по крайней мере, немного. Не то, чтобы вы беспокоились об этом, если это не относится к вам, но:
Выделение в куче требует поиска отслеживания блока памяти, который не является операцией постоянного времени (и занимает несколько циклов и накладных расходов). Это может замедляться по мере фрагментации памяти и/или вы приближаетесь к использованию 100% вашего адресного пространства. С другой стороны, распределения стека являются постоянными, в основном "свободными" операциями.
Еще одна вещь, которую следует учитывать (опять же, действительно важно только в том случае, если она становится проблемой) заключается в том, что обычно размер стека фиксирован и может быть намного меньше размера кучи. Поэтому, если вы выделяете большие объекты или много мелких объектов, вы, вероятно, захотите использовать кучу; если у вас закончится пространство стека, среда выполнения будет вызывать исключение для сайта. Обычно это не большая проблема, но еще одна вещь, которую нужно учитывать.
Ответ 6
Стек более эффективен и легче управляется областью данных.
Но куча должна использоваться для чего-либо большего, чем несколько килобайт (это легко на С++, просто создайте boost::scoped_ptr
в стеке, чтобы удерживать указатель на выделенную память).
Рассмотрим рекурсивный алгоритм, который продолжает называть себя. Очень сложно ограничить и угадать общее использование стека! Если в куче распределитель (malloc()
или new
) может указывать на неактивную память, возвращая NULL
или throw
ing.
Источник: Ядро Linux, стек которого не превышает 8 КБ!
Ответ 7
Для полноты вы можете прочитать статью Miro Samek о проблемах использования кучи в контексте встроенного программного обеспечения.
Куча проблем
Ответ 8
Выбор того, следует ли выделять кучу или в стеке, - это тот, который сделан для вас, в зависимости от того, как распределяется ваша переменная. Если вы выделяете что-то динамически, используя "новый" вызов, вы выделяете из кучи. Если вы выделяете что-то как глобальную переменную или как параметр в функции, она выделяется в стеке.
Ответ 9
По моему мнению, существуют два решающих фактора
1) Scope of variable
2) Performance.
Я бы предпочел использовать стек в большинстве случаев, но если вам нужен доступ к переменной внешней видимости, вы можете использовать кучу.
Чтобы повысить производительность при использовании кучи, вы также можете использовать функциональные возможности для создания блока кучи, что может помочь в получении производительности, а не в распределении каждой переменной в разных ячейках памяти.
Ответ 10
Вероятно, этому ответили довольно хорошо. Я хотел бы указать вам на следующую серию статей, чтобы получить более глубокое понимание деталей низкого уровня. У Alex Darby есть серия статей, где он проводит вас с помощью отладчика. Вот часть 3 о стеке.
http://www.altdevblogaday.com/2011/12/14/c-c-low-level-curriculum-part-3-the-stack/