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

Я программировал некоторое время, но в основном это 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/