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

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

150 голосов
/ 03 января 2009

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

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

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

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

int * asafe=new int;

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

asafe=new int;

Ответы [ 9 ]

206 голосов
/ 03 января 2009

Аналогичный вопрос был задан, но он не спрашивал о статике.

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

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

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

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

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

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

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

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

Сборка мусора

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

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

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

52 голосов
/ 03 января 2009

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

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


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

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

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 привязан не к конкретным объектам своего класса, а к самому классу. Адрес всех трех имен выше одинаков, и все они обозначают один и тот же объект. Существует специальное правило о том, когда и как инициализируются статические объекты, но сейчас не будем об этом беспокоиться. Это означает термин статический порядок инициализации фиаско .


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

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

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();
}

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

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

37 голосов
/ 06 января 2009

Это было сказано обстоятельно, просто как «короткий ответ»:

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

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

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

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


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

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

5 голосов
/ 03 января 2009

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

Stack

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

Heap

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

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

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

3 голосов
/ 03 января 2009

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1 голос
/ 03 января 2009

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

1 голос
/ 03 января 2009

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

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

0 голосов
/ 06 ноября 2013

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

...