Стек, статика и куча в C ++


160

Я искал, но я не очень хорошо понял эти три понятия. Когда мне нужно использовать динамическое распределение (в куче) и каково его реальное преимущество? В чем проблемы статики и стека? Могу ли я написать целое приложение без размещения переменных в куче?

Я слышал, что в других языках есть «сборщик мусора», поэтому вам не нужно беспокоиться о памяти. Что делает сборщик мусора?

Что вы могли бы сделать, управляя памятью самостоятельно, что вы не могли бы сделать с помощью этого сборщика мусора?

Однажды кто-то сказал мне, что с этим заявлением:

int * asafe=new int;

У меня есть «указатель на указатель». Что это означает? Это отличается от:

asafe=new int;

?


Когда-то был задан очень похожий вопрос: что и где находится стек и куча? Есть несколько действительно хороших ответов на этот вопрос, которые должны пролить свет на ваш.
Скотт Саад

Ответы:


223

Был задан похожий вопрос , но он не задал вопрос о статике.

Сводка о том, что такое статическая, куча и память стека:

  • Статическая переменная - это в основном глобальная переменная, даже если вы не можете получить к ней глобальный доступ. Обычно для него есть адрес, который находится в самом исполняемом файле. Существует только одна копия для всей программы. Независимо от того, сколько раз вы входите в вызов функции (или класс) (и сколько потоков!) Переменная ссылается на одну и ту же область памяти.

  • Куча - это куча памяти, которая может использоваться динамически. Если вы хотите 4 КБ для объекта, то динамический распределитель просмотрит список свободного пространства в куче, выберет кусок 4 КБ и выдаст его вам. Обычно динамический распределитель памяти (malloc, new и т. Д.) Запускается в конце памяти и работает в обратном направлении.

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

Когда вы хотите использовать каждый из них:

  • Статика / глобальные переменные полезны для памяти, которая, как вы знаете, вам всегда будет нужна, и вы знаете, что никогда не захотите ее освобождать. (Кстати, встроенные среды могут рассматриваться как имеющие только статическую память ... стек и куча являются частью известного адресного пространства, совместно используемого третьим типом памяти: программным кодом. Программы часто выполняют динамическое распределение из своих статическая память, когда им нужны такие вещи, как связанные списки. Но независимо от этого сама статическая память (буфер) сама по себе не «выделяется», а, скорее, другие объекты выделяются из памяти, удерживаемой буфером для этой цели. и в не встроенных, и в консольных играх часто отказываются от встроенных механизмов динамической памяти в пользу жесткого контроля процесса выделения с использованием буферов заданных размеров для всех распределений.)

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

  • Распределение кучи (динамически выделяемая память) полезно, когда вы хотите быть более гибкими, чем указано выше. Часто вызывается функция для ответа на событие (пользователь нажимает кнопку «Создать ящик»). Для правильного ответа может потребоваться выделение нового объекта (нового объекта Box), который должен сохраняться долго после выхода из функции, поэтому он не может находиться в стеке. Но вы не знаете, сколько ящиков вам понадобится в начале программы, поэтому оно не может быть статичным.

Вывоз мусора

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

Сборка мусора является прекрасным механизмом, когда производительность не является большой проблемой. Я слышал, что GC становятся все лучше и сложнее, но на самом деле вы можете быть вынуждены согласиться на снижение производительности (в зависимости от варианта использования). И если вы ленивы, это все еще может не работать должным образом. В лучшие времена сборщики мусора понимают, что ваша память уходит, когда она понимает, что на нее больше нет ссылок (см. Подсчет ссылок). Но если у вас есть объект, который ссылается на себя (возможно, ссылаясь на другой объект, который ссылается назад), то только подсчет ссылок не будет указывать, что память может быть удалена. В этом случае GC необходимо просмотреть весь эталонный суп и выяснить, существуют ли какие-либо острова, на которые ссылаются только они сами. Случайно, я бы предположил, что это операция O (n ^ 2), но как бы то ни было, она может испортиться, если вы вообще обеспокоены производительностью. (Правка: Мартин Б. указывает, что это O (n) для достаточно эффективных алгоритмов. Это все равно O (n) слишком много, если вы обеспокоены производительностью и можете освободить ресурсы в постоянное время без сбора мусора.)

Лично, когда я слышу, как люди говорят, что в C ++ нет сборки мусора, я вспоминаю это как особенность C ++, но я, вероятно, в меньшинстве. Наверное, труднее всего людям узнать о программировании на C и C ++ - указатели и то, как правильно обрабатывать их динамическое распределение памяти. Некоторые другие языки, такие как Python, были бы ужасны без GC, поэтому я думаю, что все сводится к тому, что вы хотите от языка. Если вам нужна надежная производительность, то C ++ без сборки мусора - это единственное, что я могу подумать об этой стороне Fortran. Если вы хотите простоты использования и тренировочных колес (чтобы избавить вас от сбоев, не требуя обучения «правильному» управлению памятью), выберите что-то с помощью ГХ. Даже если вы знаете, как правильно управлять памятью, это сэкономит вам время, которое вы можете потратить на оптимизацию другого кода. На самом деле потери производительности не так уж и велики, но если вам действительно нужна надежная производительность (и способность точно знать, что происходит, когда под прикрытием), я бы остановился на C ++. Есть причина, по которой все основные игровые движки, о которых я когда-либо слышал, находятся на C ++ (если не на C или сборке). Python и др. Хороши для написания скриптов, но не для основного игрового движка.


На самом деле это не относится к исходному вопросу (или вообще к большому количеству), но вы получили расположение стека и кучу назад. Как правило , стек уменьшается, а куча растет (хотя куча на самом деле не «растет», так что это огромное упрощение) ...
P Daddy

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

7
Ваше уничижительное отношение к сбору мусора было немного менее чем полезным.
P Daddy

9
Часто сборка мусора в настоящее время лучше, чем ручное освобождение памяти, потому что это происходит, когда мало работы, в отличие от освобождения памяти, которое может произойти именно тогда, когда производительность может быть использована иначе.
Георг Шолли

3
Просто небольшой комментарий - сборка мусора не имеет сложности O (n ^ 2) (что, действительно, пагубно для производительности). Время, затрачиваемое на один цикл сборки мусора, пропорционально размеру кучи - см. Hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Мартин Б

54

Следующее, конечно, все не совсем точно. Возьми это с крошкой соли, когда читаешь это :)

Итак, три вещи, на которые вы ссылаетесь, - это автоматическое, статическое и динамическое время хранения , которое имеет отношение к тому, как долго живут объекты и когда они начинают жить.


Автоматическая продолжительность хранения

Вы используете автоматическую длительность хранения для коротких и небольших данных, которые необходимы только локально в некотором блоке:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

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


Статическая продолжительность хранения

Вы используете статическую длительность хранения для свободных переменных, к которым может обращаться любой код в любое время, если их область допускает такое использование (область пространства имен), и для локальных переменных, которым необходимо продлить срок их жизни при выходе из своей области (локальная область), и для переменных-членов, которые должны быть общими для всех объектов их класса (класс classs). Их срок службы зависит от объема они находятся. Они могут иметь область пространства имен и локальную область и область видимость класса . Что верно для них обоих, так это то, что как только начинается их жизнь, заканчивается жизнь в конце программы . Вот два примера:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

Программа печатает ababab, потому что localAне уничтожается при выходе из своего блока. Вы можете сказать, что объекты, имеющие локальную область видимости, начинают время жизни, когда управление достигает своего определения . Ведь localAэто происходит, когда вводится тело функции. Для объектов в области пространства имен время жизни начинается при запуске программы . То же самое верно для статических объектов класса видимости:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Как видите, classScopeAпривязан не к конкретным объектам своего класса, а к самому классу. Адрес всех трех имен выше одинаков, и все они обозначают один и тот же объект. Существует специальное правило о том, когда и как инициализируются статические объекты, но сейчас не будем об этом беспокоиться. Это подразумевается под термином статический порядок инициализации фиаско .


Динамическая продолжительность хранения

Последняя продолжительность хранения является динамической. Вы используете его, если хотите, чтобы объекты жили на другом острове, и вы хотите поместить указатели вокруг них. Вы также можете использовать их, если ваши объекты большие , и если вы хотите создать массивы размером, известным только во время выполнения . Из-за этой гибкости объекты, имеющие динамическую продолжительность хранения, являются сложными и медленными в управлении. Объекты с такой динамической продолжительностью начинают время жизни, когда происходит соответствующий вызов нового оператора:

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

Его время жизни заканчивается только когда вы вызываете delete для них. Если вы забудете об этом, эти объекты никогда не заканчивают жизнь. И объектам классов, которые определяют объявленный пользователем конструктор, не будут вызываться их деструкторы. Объекты, имеющие динамическую продолжительность хранения, требуют ручной обработки их времени жизни и связанного ресурса памяти. Библиотеки существуют, чтобы облегчить их использование. Явная сборка мусора для определенных объектов может быть установлена ​​с помощью умного указателя:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

Вам не нужно заботиться о вызове delete: общий ptr сделает это за вас, если последний указатель, который ссылается на объект, выходит из области видимости. Сам общий ресурс имеет автоматическую продолжительность хранения. Таким образом, его время жизни автоматически управляется, что позволяет ему проверять, следует ли удалять указанный динамический объект в его деструкторе. Для ссылки на shared_ptr см. Дополнительные документы: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


39

Это было сказано продуманно, так же как «короткий ответ»:

  • время жизни статической переменной (класса)
    = время выполнения программы (1)
    видимость = определяется модификаторами доступа (private / protected / public)

  • статическая переменная (глобальная область)
    время жизни = время выполнения программы (1)
    видимость = единица компиляции, в которой она создается в (2)


  • время жизни переменной кучи = определено вами (новое для удаления)
    видимость = определено вами (независимо от того, на что вы назначаете указатель)

  • переменная
    видимости стека = от объявления до выхода из области действия
    время жизни = от объявления до выхода из области объявления


(1) точнее: от инициализации до деинициализации модуля компиляции (то есть файла C / C ++). Порядок инициализации блоков компиляции не определяется стандартом.

(2) Осторожно: если вы создаете статическую переменную в заголовке, каждый модуль компиляции получает свою собственную копию.


5

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

стек

Значительно быстрее выделить. Это делается в O (1), поскольку оно выделяется при настройке фрейма стека, поэтому оно по существу свободно. Недостатком является то, что, если у вас заканчивается свободное пространство в стеке, у вас кость. Вы можете настроить размер стека, но у IIRC у вас есть ~ 2MB для игры. Также, как только вы выходите из функции, все в стеке очищается. Поэтому может быть проблематично сослаться на это позже. (Указатели на размещение выделенных объектов приводят к ошибкам.)

отвал

Значительно медленнее выделять. Но у вас есть ГБ, чтобы поиграть, и укажите на.

Уборщик мусора

Сборщик мусора - это некоторый код, который работает в фоновом режиме и освобождает память. Когда вы выделяете память в куче, очень легко забыть освободить ее, что называется утечкой памяти. Со временем память, используемая вашим приложением, растет и растет, пока не выйдет из строя. Сборщик мусора периодически освобождает память, которая вам больше не нужна, помогает устранить этот класс ошибок. Конечно, это дорого, поскольку сборщик мусора замедляет работу.


3

В чем проблемы статики и стека?

Проблема со «статическим» распределением заключается в том, что выделение выполняется во время компиляции: вы не можете использовать его для выделения некоторого переменного числа данных, число которых неизвестно до времени выполнения.

Проблема с размещением в «стеке» заключается в том, что распределение уничтожается, как только возвращается подпрограмма, которая выполняет распределение.

Я мог бы написать целое приложение без выделения переменных в куче?

Возможно, но не нетривиальное, нормальное, большое приложение (но так называемые «встроенные» программы могут быть написаны без кучи, используя подмножество C ++).

Что делает сборщик мусора?

Он продолжает следить за вашими данными («пометить и развернуть»), чтобы определить, когда ваше приложение больше не ссылается на них. Это удобно для приложения, потому что приложение не должно освобождать данные ... но сборщик мусора может быть вычислительно дорогим.

Сборщики мусора не являются обычной особенностью программирования на C ++.

Что вы могли бы сделать, управляя памятью самостоятельно, что вы не могли бы сделать с помощью этого сборщика мусора?

Изучите механизмы C ++ для детерминированного освобождения памяти:

  • «статический»: никогда не освобождается
  • «стек»: как только переменная выходит из области видимости
  • «куча»: когда указатель удален (явно удален приложением или неявно удален в той или иной подпрограмме)

1

Распределение стековой памяти (функциональные переменные, локальные переменные) может быть проблематичным, когда ваш стек слишком «глубокий» и вы переполняете память, доступную для выделения стека. Куча предназначена для объектов, к которым необходимо обращаться из нескольких потоков или на протяжении жизненного цикла программы. Вы можете написать целую программу без использования кучи.

Вы можете легко утечь память без сборщика мусора, но вы также можете определять, когда объекты и память освобождаются. Я столкнулся с проблемами с Java, когда он запускает GC, и у меня есть процесс в реальном времени, потому что GC является эксклюзивным потоком (больше ничего не может работать). Поэтому, если производительность критична, и вы можете гарантировать отсутствие утечек, использование GC очень полезно. В противном случае это просто заставляет вас ненавидеть жизнь, когда ваше приложение потребляет память, и вам нужно отследить источник утечки.


1

Что делать, если ваша программа не знает заранее, сколько памяти выделить (следовательно, вы не можете использовать переменные стека). Скажем, связанные списки, списки могут расти, не зная заранее, каков его размер. Поэтому распределение в куче имеет смысл для связанного списка, когда вы не знаете, сколько элементов будет в него вставлено.


0

Преимущество GC в одних ситуациях - раздражение в других; опора на GC побуждает не задумываться об этом. Теоретически, ожидает до периода «простоя» или до тех пор, пока это абсолютно не нужно, когда он украдет пропускную способность и вызовет задержку отклика в вашем приложении.

Но вам не нужно «не думать об этом». Как и все остальное в многопоточных приложениях, когда вы можете уступить, вы можете уступить. Так, например, в .Net можно запросить GC; Делая это, вместо менее частого запуска GC, вы можете чаще выполнять GC с более коротким временем работы и распределять задержку, связанную с этими издержками.

Но это побеждает первичную привлекательность GC, которая, кажется, «поощряется, чтобы не думать много об этом, потому что это автоматически».

Если вы впервые познакомились с программированием до того, как GC стал распространенным, и вас устраивали malloc / free и new / delete, то может даже оказаться, что GC вас немного раздражает и / или вызывает недоверие (так как можно не доверять ' Оптимизация », которая имеет изменчивую историю.) Многие приложения допускают случайную задержку. Но для приложений, которые этого не делают, где случайная задержка менее приемлема, обычной реакцией является отказ от среды GC и движение в направлении чисто неуправляемого кода (или не дай бог, давно умирающего искусства, языка ассемблера).

Некоторое время назад у меня здесь был летний студент, молодой, умный парень, которого отлучили от школы; он был настолько восхищен превосходством GC, что даже при программировании на неуправляемом C / C ++ он отказывался следовать модели malloc / free new / delete, потому что, цитируйте: «Вы не должны делать это на современном языке программирования». И ты знаешь? Для крошечных, коротко работающих приложений вы можете сойти с рук, но не для долго работающих приложений.


0

Стек - это память, выделяемая компилятором. Когда мы компилируем программу, по умолчанию компилятор выделяет некоторую память из ОС (мы можем изменить настройки из настроек компилятора в вашей IDE), а ОС - это та, которая дает вам память, это зависит во многих доступной памяти в системе и многих других вещах, и приход к памяти стека распределяется, когда мы объявляем переменную, которую они копируют (ref как формальные), эти переменные помещаются в стек, они следуют некоторым соглашениям об именах по умолчанию, его CDECL в визуальных студиях например: инфиксная запись: c = a + b; перемещение в стек выполняется справа налево PUSHING, b в стек, оператор, a в стек и результат тех i, ec в стек. В предварительной записи: = + cab Здесь все переменные помещаются в стек 1-го (справа налево), а затем выполняется операция. Эта память, выделенная компилятором, исправлена. Итак, давайте предположим, что 1 МБ памяти выделено нашему приложению, скажем, переменные использовали 700 КБ памяти (все локальные переменные помещаются в стек, если они не выделяются динамически), поэтому оставшаяся память 324 КБ выделяется для кучи. И у этого стека меньше времени жизни, когда область действия функции заканчивается, эти стеки очищаются.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.