Вопрос

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

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

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

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

int * asafe=new int;

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

asafe=new int;

?

Это было полезно?

Решение

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

Краткое описание того, что такое статическая, куча и стековая память:

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

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

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

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

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

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

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

Вывоз мусора

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

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

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

Другие советы

Дальше, конечно, все не совсем точно.Отнеситесь к этому с недоверием, когда прочтете :)

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Куча

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

Куча

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

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

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

Каковы проблемы статики и стека?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top