Почему программисты C++ должны минимизировать использование "нового"?

я наткнулся на вопрос переполнения стека утечка памяти с помощью std:: string при использовании std:: list и один из комментариев говорит:

перестать использовать new Так уж и много. Я не вижу причин, по которым ты использовал что-то новое. это ты. Вы можете создавать объекты по значению в C++ и это одна из огромные преимущества использования языка. Вам не нужно выделять все в куче. Перестаньте думать как Java программист.

17 ответов


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

стек

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

в C++ это называется автоматическое хранение потому что хранилище утверждается автоматически в конце области. Как только выполняется текущий блок кода (разделенный с помощью {}) завершено, память для всех переменных в этом блоке автоматически собрана. Это также момент, когда деструкторы применяются для очистки ресурсы.

кучу

куча позволяет более гибкий режим выделения памяти. Бухгалтерия сложнее, а распределение медленнее. Поскольку нет неявной точки выпуска, вы должны освободить память вручную, используя delete или delete[] (free в C). Однако отсутствие неявной точки выпуска является ключом к гибкости кучи.

причины использования динамического распределения

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

две основные причины использования динамического распределения:

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

  • вы хотите выделить память, которая сохранится после выхода из текущего блока. Например, вы можете написать функцию string readfile(string path) возвращает содержимое файла. В этом случае, даже если стек может содержать все содержимое файла, вы не можете вернуться из функции и сохранить выделенный блок памяти.

почему динамическое распределение часто не нужно

в C++ есть аккуратная конструкция называется деструктор. Этот механизм позволяет управлять ресурсами путем согласования времени жизни ресурса с временем жизни переменной. Эта техника называется РАИИ и является отличительной чертой C++. Он" обертывает " ресурсы в объекты. std::string является прекрасным примером. Этот фрагмент:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

фактически выделяет переменный объем памяти. The std::string объект выделяет память с помощью кучи и освобождает ее в своем деструкторе. В этом случае вы сделали не нужно вручную управлять любыми ресурсами и по-прежнему получать преимущества динамического выделения памяти.

в частности, это означает, что в этот фрагмент:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

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

почему вы должны использовать автоматическое хранение так часто, как возможно

в принципе, последний абзац подводит итог. Использование автоматического хранения как можно чаще делает ваши программы:

  • быстрее печатать;
  • быстрее при запуске;
  • менее подвержены утечкам памяти / ресурсов.

бонусные баллы

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

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

на самом деле гораздо более рискованным, чем следующий:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

причина в том, что std::string правильно определяет конструктор копирования. Рассмотрим следующую программу:

int main ()
{
    Line l1;
    Line l2 = l1;
}

используя исходную версию, эта программа, скорее всего, сбой, так как она использует delete на одной и той же строке дважды. Используя измененную версию, each Line экземпляр будет иметь свою собственную строку экземпляр, каждый со своей памятью, и оба будут выпущены в конце программы.

другой Примечания

широкое использование РАИИ считается лучшей практикой в C++ по всем вышеперечисленным причинам. Однако есть и дополнительная выгода, которая не сразу бросается в глаза. В принципе, это лучше, чем сумма его частей. Весь механизм составляет. Он масштабируется.

если вы используете Line класс как строительный блок:

 class Table
 {
      Line borders[4];
 };

затем

 int main ()
 {
     Table table;
 }

выделяет четыре std::string экземпляров, четыре Line экземплярах, один Table экземпляр и все содержимое строки и все освобождается автоматически.


потому что стек быстрый и надежный

В C++ требуется только одна инструкция для выделения пространства -- в стеке -- для каждого локального объекта области в данной функции, и невозможно утечка любой из этой памяти. Этот комментарий предназначался (или должен был предназначаться), чтобы сказать что-то вроде "используйте стек, а не кучу".


это сложно.

во-первых, C++ не собирает мусор. Поэтому для каждого нового должно быть соответствующее удаление. Если вы не можете поместить это удаление, то у вас есть утечка памяти. Теперь, для такого простого случая:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

это просто. Но что произойдет, если "Do stuff" выдает исключение? Упс: утечка памяти. Что произойдет, если" делать вещи " проблемы return рано? Упс: утечка памяти.

и это простой дело. Если вам случится вернуть эту строку кому-то, теперь они должны удалить ее. И если они передают его как аргумент, должен ли человек, получающий его, удалить его? Когда они должны удалить его?

или, вы можете просто сделать это:

std::string someString(...);
//Do stuff

нет delete. Объект был создан в "стеке", и он будет уничтожен, как только он выйдет из области видимости. Вы даже можете вернуть объект, передав его содержимое вызывающей функции. Вы можете передать объект функции (обычно как ссылка или const-ссылка:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). И так далее.

без new и delete. Вопрос не в том, кому принадлежит память или кто отвечает за ее удаление. Если у вас:

std::string someString(...);
std::string otherString;
otherString = someString;

понятно, что otherString и копия сведения of someString. Это не указатель, это отдельный объект. Они могут иметь одинаковое содержимое, но вы можете изменить его, не затрагивая другое:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

понимаете мысль?


объекты, созданные new должно быть в конце концов deleted чтобы они не протекали. Деструктор не будет вызван, память не будет освобождена, весь бит. Поскольку в C++ нет сборки мусора, это проблема.

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

умные указатели, такие как auto_ptr, shared_ptr решить подвешенные проблема, но они требуют дисциплины кодирования и имеют другие проблемы (копируемость, циклы ссылок и т. д.).

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

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


  • C++ не использует собственный менеджер памяти. Другие языки, такие как C#, Java имеет сборщик мусора для обработки памяти
  • C++ использование процедур операционной системы для выделения памяти и слишком много нового / удаления может фрагментировать доступную память
  • С любым приложением, если память часто используется, рекомендуется предварительно выделить ее и освободить, когда это не требуется.
  • неправильное управление памятью может привести к утечкам памяти и это очень трудно отследить. Таким образом, использование объектов стека в рамках функции является проверенным методом
  • недостатком использования объектов стека является то, что он создает несколько копий объектов при возврате, передаче в функции и т. д. Однако умные компиляторы хорошо осведомлены об этих ситуациях, и они были хорошо оптимизированы для производительности
  • Это действительно утомительно в C++, если память выделяется и освобождается в двух разных местах. Ответственность за освобождение всегда вопрос и в основном мы полагаемся на некоторые общедоступные указатели, объекты стека (максимально возможные) и методы, такие как auto_ptr (объекты RAII)
  • самое лучшее, что у вас есть контроль над памятью, и самое худшее, что у вас не будет никакого контроля над памятью, если мы используем неправильное управление памятью для приложения. Сбои, вызванные повреждениями памяти, являются самыми неприятными и трудными для отслеживания.

в значительной степени это кто-то, кто поднимает свои собственные слабости до общего правила. Все в порядке per se С созданием объектов с помощью new оператора. Есть аргумент в пользу того, что вы должны делать это с некоторой дисциплиной: если вы создаете объект, вам нужно убедиться, что он будет уничтожен.

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

 {
    File foo = File("foo.dat");

    // do things

 }

теперь обратите внимание, что, когда вы падаете с этого блока после конца скобки,foo выходит за рамки. C++ автоматически вызовет свой dtor для вас. В отличие от Java, вам не нужно ждать, пока GC найдет его.

если бы вы написали

 {
     File * foo = new File("foo.dat");

вы хотели бы явно сопоставить его с

     delete foo;
  }

или даже лучше, выделите свой File * как "умный указатель". Если вы не будете осторожны, это может привести к подтеки.

ответ сам делает ошибочное предположение, что если вы не используете new вы не выделяете в куче; на самом деле, в C++ вы этого не знаете. Самое большее, вы знаете, что небольшой объем памяти, скажем, один указатель, определенно выделяется в стеке. Однако рассмотрим, является ли реализация файла чем-то вроде

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

затем FileImpl будет еще выделяться в стеке.

и да, вы должны быть уверены, что есть

     ~File(){ delete fd ; }

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


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

оператор new имеет недетерминированное время выполнения

вызов new может или не может заставить ОС выделить новую физическую страницу для вашего процесса это может быть довольно медленно, если вы делаете это часто. Или у него уже может быть подходящее место для памяти, мы не знаем. Если ваша программа должна иметь согласованное и предсказуемое время выполнения (например, в системе реального времени или игра / моделирование физики) вам нужно избегать new в критических циклах времени.

оператор new является неявной синхронизацией потоков

Да, вы слышали меня, ваша ОС должна убедиться, что ваши таблицы страниц согласованы и как таковые вызывают new вызовет ваш поток получить неявное мьютекс замок. Если вы последовательно вызываете new из многих потоков вы фактически сериализуете свои потоки (я сделал это с 32 процессорами, каждый нажимая на new получить несколько сотен байт каждый, ой! это было королевское "п".Я. т. a. для отладки)

остальные, такие как медленная, фрагментация, склонность к ошибкам и т. д., уже упоминались в других ответах.


new() Не следует использовать как мало как это возможно. Он должен использоваться как внимательно как это возможно. И его следует использовать так часто, как это необходимо, как диктуется прагматизмом.

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

однако, если время жизни объекта должна выходить за пределы текущей области, то new() Это правильный ответ. Просто убедитесь, что вы обращаете внимание на то, когда и как вы звоните delete() и возможности нулевых указателей, используя удаленные объекты и все другие gotchas, которые поставляются с использованием указателей.


при использовании new объекты выделяются в кучу. Он обычно используется, когда вы ожидаете расширения. Когда вы объявляете объект, такой как,

Class var;

он помещается в стек.

вам всегда придется вызывать destroy на объекте, который вы разместили в куче с новым. Это открывает возможность утечки памяти. Объекты, размещенные в стеке, не подвержены утечке памяти!


Pre-C++17:

потому что он склонен к тонким утечки даже если вы обернете результат в смарт-указатель.

рассмотрим "осторожного" пользователя, который помнит, чтобы обернуть объекты в смарт-указатели:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

этот код опасен, потому что есть никакой гарантии что-либо shared_ptr построен до или T1 или T2. Следовательно, если один из new T1() или new T2() сбой за другим успешно, тогда первый объект будет утечка, потому что нет чтобы уничтожить и освободить его.

решение: используйте make_shared.

Post-C++17:

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

более подробное объяснение нового порядка оценки, введенного C++17, было предоставлено Барри в другом ответе.


Я думаю, что постер хотел сказать You do not have to allocate everything on theheap а не stack.

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


Я склонен не соглашаться с идеей использования нового "слишком много". Хотя использование оригинального плаката new с системными классами немного смешно. (int *i; i = new int[9999];? неужели? int i[9999]; гораздо понятнее.) Я думаю это что стало комментатор козье.

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

когда вы работаете со своими собственными классами/объектами, однако (например, класс линии оригинального плаката), вам нужно начните думать о таких проблемах, как объем памяти, сохраняемость данных и т. д. себе. На данный момент разрешение нескольких ссылок на одно и то же значение неоценимо - оно позволяет создавать такие конструкции, как связанные списки, словари и графики, где несколько переменных должны не только иметь одно и то же значение, но и ссылаться на одно и то же объект в памяти. Однако класс Line не имеет ни одного из этих требований. Таким образом, исходный код плаката на самом деле не имеет абсолютно никаких потребностей new.


одной из заметных причин избежать чрезмерного использования кучи является производительность-в частности, производительность механизма управления памятью по умолчанию, используемого C++. Хотя выделение может быть довольно быстрым в тривиальном случае, делая много new и delete на объектах неравномерного размера без строгого порядка приводит не только к фрагментации памяти, но и усложняет алгоритм выделения и в некоторых случаях может полностью разрушить производительность.

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

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


двум причинам:

  1. в этом случае это не нужно. Вы напрасно усложняете свой код.
  2. он выделяет место в куче, и это означает, что вы должны помнить, чтобы delete Это позже, или это вызовет утечку памяти.

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

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

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

традиционные шаблоны дизайна, особенно те, которые упомянуты в г книги, используйте new много, так как они являются типичным кодом OO.


new новая goto.

напомнить, почему goto так ругается: хотя это мощный, низкоуровневый инструмент для управления потоком, люди часто использовали его неоправданно сложными способами, которые затрудняли следование коду. Кроме того, наиболее полезные и простые для чтения шаблоны были закодированы в инструкциях структурированного программирования (например,for или while); конечный эффект состоит в том, что код, где goto соответствующий способ довольно редок, если у вас есть соблазн пиши goto, вы, вероятно, делаете вещи плохо (если вы действительно знайте, что вы делаете).

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

I даже поспорил бы, что new is хуже чем goto, из-за необходимости пары new и delete заявления.

как goto, Если вы когда-нибудь думаете, что вам нужно использовать new, вы, вероятно, делаете что - то плохо-особенно если вы делаете это вне реализации класса, цель которого в жизни заключается в инкапсуляции любых динамических распределений, которые вам нужно сделать.


new выделение объектов в куче. В противном случае объекты выделяются в стеке. Искать разница между двумя.