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

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

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

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

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

int * asafe=new int;

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

asafe=new int;

?

9 ответов

Решение

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

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

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

  • Куча - это куча памяти, которая может использоваться динамически. Если вы хотите 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 и др. Хороши для написания скриптов, но не для основного игрового движка.

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

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


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

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

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

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

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

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

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

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


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

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

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

стек

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

отвал

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам