Почему программисты С++ минимизируют использование "новых"?
Я наткнулся на вопрос "Переполнение стека". Утечка памяти с помощью std :: string при использовании std :: list <std :: string>, и один из комментариев говорит об этом:
Прекратите использовать new
так много. Я не вижу причины, по которой вы использовали новое, где бы вы ни были. Вы можете создавать объекты по значению в C++ и это одно из огромных преимуществ использования языка. Вам не нужно выделять все в кучу. Прекратите думать, как программист на Java.
Я не совсем уверен, что он имеет в виду. Почему объекты должны быть созданы по значению в C++ настолько часто, насколько это возможно, и какая разница делает его внутренне? Я неправильно понял ответ?
Ответы
Ответ 1
Существует два широко используемых метода выделения памяти: автоматическое распределение и динамическое распределение. Обычно для каждой области памяти имеется соответствующая область памяти: стек и куча.
Stack
Стек всегда выделяет память последовательным образом. Он может сделать это, потому что он требует освобождения памяти в обратном порядке (First-In, Last-Out: FILO). Это метод распределения памяти для локальных переменных на многих языках программирования. Это очень, очень быстро, потому что он требует минимальной бухгалтерской отчетности, а следующий адрес для выделения является неявным.
В С++ это называется автоматическим хранилищем, поскольку хранилище автоматически заявляется в конце области. Как только выполняется выполнение текущего блока кода (с разделителем с помощью {}
), память для всех переменных в этом блоке будет автоматически собрана. Это также момент, когда деструкторы вызываются для очистки ресурсов.
Heap
Куча позволяет использовать более гибкий режим выделения памяти. Бухгалтерия является более сложной и распределение происходит медленнее. Поскольку не существует неявной точки выпуска, вы должны освободить память вручную, используя delete
или delete[]
(free
в C). Однако отсутствие неявной точки выпуска является ключом к гибкости кучи.
Причины использования динамического распределения
Даже если использование кучи происходит медленнее и потенциально приводит к утечкам памяти или фрагментации памяти, для динамического распределения есть очень хорошие варианты использования, поскольку они менее ограничены.
Две основные причины использования динамического распределения:
-
Вы не знаете, сколько памяти вам нужно во время компиляции. Например, при чтении текстового файла в строку вы обычно не знаете, какой размер имеет файл, поэтому вы не можете решить, сколько памяти будет выделено до запуска программы.
-
Вы хотите выделить память, которая будет сохраняться после выхода из текущего блока. Например, вы можете написать функцию string readfile(string path)
, которая возвращает содержимое файла. В этом случае, даже если стек может содержать все содержимое файла, вы не можете вернуться из функции и сохранить выделенный блок памяти.
Почему динамическое распределение часто не требуется
В С++ существует аккуратная конструкция, называемая деструктором. Этот механизм позволяет вам управлять ресурсами путем согласования ресурса ресурса со временем жизни переменной. Этот метод называется RAII и является отличительной чертой С++. Он "обертывает" ресурсы на объекты. std::string
- прекрасный пример. Этот фрагмент:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
фактически выделяет переменный объем памяти. Объект 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
в одной строке дважды. Используя модифицированную версию, каждый экземпляр Line
будет иметь свой собственный экземпляр строки, каждый со своей собственной памятью, и оба будут выпущены в конце программы.
Другие примечания
Широкое использование RAII считается лучшей практикой на С++ из-за всех вышеперечисленных причин. Однако есть дополнительное преимущество, которое не сразу становится очевидным. В принципе, это лучше, чем сумма его частей. Составлен весь механизм. Он масштабируется.
Если вы используете класс Line
как строительный блок:
class Table
{
Line borders[4];
};
Тогда
int main ()
{
Table table;
}
выделяет четыре экземпляра std::string
, четыре экземпляра Line
, один экземпляр Table
и все содержимое строки, и все автоматически освобождается.
Ответ 2
Потому что стек быстрее и герметичнее
В C++ требуется всего одна инструкция для выделения пространства - в стеке - для каждого локального объекта области в данной функции, и утечка этой памяти невозможна. Этот комментарий намеревался (или должен был) сказать что-то вроде "используйте стек, а не кучу".
Ответ 3
Причина сложна.
Во-первых, 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
имеет копию данных someString
. Это не указатель; это отдельный объект. Возможно, они имеют одинаковое содержимое, но вы можете изменить одно, не затрагивая другое:
someString += "More text.";
if(otherString == someString) { /*Will never get here */ }
Видишь идею?
Ответ 4
Объекты, созданные new
должны быть в конечном итоге delete
чтобы они не пропали Деструктор не будет вызван, память не будет освобождена, в целом. Поскольку C++ не имеет сборки мусора, это проблема.
Объекты, созданные по значению (т.е. В стеке), автоматически умирают, когда выходят из области видимости. Вызов деструктора вставляется компилятором, а память автоматически освобождается при возврате функции.
Интеллектуальные указатели, такие как unique_ptr
, shared_ptr
решают проблему свисающих ссылок, но они требуют дисциплины кодирования и имеют другие потенциальные проблемы (копируемость, циклы ссылок и т.д.).
Кроме того, в многопоточных сценариях new
является предметом спора между потоками; может быть влияние на производительность для чрезмерного использования new
. Создание объекта стека по определению является локальным для потока, поскольку каждый поток имеет свой собственный стек.
Недостатком объектов-значений является то, что они умирают после возврата из функции хоста - вы не можете передать ссылку на них обратно вызывающей стороне, только копируя, возвращая или перемещая по значению.
Ответ 5
- C++ не использует какой-либо менеджер памяти сам по себе. В других языках, таких как С#, в Java есть сборщик мусора для обработки памяти
- Реализации C++ обычно используют процедуры операционной системы для выделения памяти, и слишком много нового/удаления может фрагментировать доступную память
- В любом приложении, если память часто используется, рекомендуется предварительно выделить ее и освободить, когда она не требуется.
- Неправильное управление памятью может привести к утечкам памяти, и это действительно трудно отследить. Таким образом, использование стековых объектов в рамках функции является проверенным методом
- Недостатком использования объектов стека является то, что он создает несколько копий объектов при возврате, передаче в функции и т.д. Однако умные компиляторы хорошо знают об этих ситуациях и хорошо оптимизированы для повышения производительности
- Это действительно утомительно в C++, если память выделяется и освобождается в двух разных местах. Ответственность за выпуск всегда остается под вопросом, и в основном мы полагаемся на некоторые общедоступные указатели, объекты стека (максимально возможный) и методы, такие как auto_ptr (объекты RAII)
- Лучше всего то, что у вас есть контроль над памятью, а хуже всего то, что вы не будете иметь никакого контроля над памятью, если мы используем неправильное управление памятью для приложения. Сбои, вызванные повреждениями памяти, являются самыми неприятными и их трудно отследить.
Ответ 6
Я вижу, что несколько важных причин для того, чтобы сделать как можно меньше новых, пропущены:
Оператор new
имеет недетерминированное время выполнения
Вызов new
может или не может заставить ОС выделять новую физическую страницу для вашего процесса, это может быть довольно медленным, если вы часто это делаете. Или у этого уже может быть подходящее место памяти, мы не знаем. Если ваша программа должна иметь согласованное и прогнозируемое время выполнения (например, в режиме реального времени или в игре/физическом моделировании), вам нужно избегать new
в ваших критических циклах времени.
Оператор new
- это неявная синхронизация потоков
Да, вы меня слышали, ваша ОС должна убедиться, что ваши таблицы страниц согласованы, и поскольку такой вызов new
приведет к тому, что ваш поток получит неявный блокировку мьютекса. Если вы последовательно вызываете new
из многих потоков, вы фактически сериализуете свои потоки (я сделал это с 32 процессорами, каждый из которых попадал на new
, чтобы получить несколько сотен байт каждый, ох! Это была королевская лаваша для отладки )
Остальные, такие как медленная, фрагментация, склонность к ошибкам и т.д., уже упоминались в других ответах.
Ответ 7
Pre-С++ 17:
Потому что он подвержен тонким утечкам, даже если вы оберните результат в умный указатель.
Рассмотрим "осторожного" пользователя, который не забывает оборачивать объекты в умные указатели:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
Этот код опасен, потому что нет никакой гарантии, что shared_ptr
до T1
или T2
. Следовательно, если один из new T1()
или new T2()
завершится неудачно после того, как другой преуспеет, то первый объект будет пропущен, потому что не существует shared_ptr
для его уничтожения и освобождения.
Решение: используйте make_shared
.
Post-С++ 17:
Это больше не проблема: С++ 17 налагает ограничение на порядок этих операций, в этом случае гарантируя, что за каждым вызовом new()
должен немедленно следовать построение соответствующего умного указателя, без других операций в между. Это подразумевает, что ко времени вызова второго new()
гарантируется, что первый объект уже обернут в его умный указатель, таким образом предотвращая любые утечки в случае возникновения исключения.
Более подробное объяснение нового порядка оценки, введенного С++ 17, было предоставлено Барри в другом ответе.
Спасибо @Remy Lebeau за то, что он указал, что это все еще проблема в С++ 17 (хотя и не так): конструктор shared_ptr
может не выделить свой управляющий блок и выполнить throw, и в этом случае переданный ему указатель не удаляется.
Решение: используйте make_shared
.
Ответ 8
В значительной степени тот, кто поднимает свои слабости до общего правила. Там нет ничего плохого в создании объектов с помощью оператора new
. Для некоторых есть аргумент в том, что вы должны сделать это с некоторой дисциплиной: если вы создаете объект, вам нужно убедиться, что он будет уничтожен.
Самый простой способ сделать это - создать объект в автоматическом хранилище, поэтому С++ знает, как его уничтожить, когда он выходит из области видимости:
{
File foo = File("foo.dat");
// do things
}
Теперь заметите, что, когда вы отвалитесь от этого блока после конечной скобки, foo
выходит за рамки. С++ вызовет его dtor автоматически для вас. В отличие от Java, вам не нужно ждать, пока GC ее не найдет.
Если бы вы написали
{
File * foo = new File("foo.dat");
вы хотите явно сопоставить его с
delete foo;
}
или даже лучше, выделите File *
как "умный указатель". Если вы не будете осторожны, это может привести к утечкам.
Сам ответ ошибочно полагает, что если вы не используете new
, вы не выделяете кучу; на самом деле, на С++ вы этого не знаете. В лучшем случае вы знаете, что небольшое количество памяти, скажем, одного указателя, конечно же выделяется в стеке. Однако рассмотрим, является ли реализация файла чем-то вроде
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
то FileImpl
по-прежнему будет выделено в стеке.
И да, вы должны быть уверены, что
~File(){ delete fd ; }
в классе; без него вы будете утечка памяти из кучи, даже если вы явно не выделили кучу вообще.
Ответ 9
new()
не следует использовать как можно меньше. Его следует использовать как можно более тщательно. И он должен использоваться так часто, как необходимо, как это продиктовано прагматизмом.
Распределение объектов в стеке, опираясь на их неявное разрушение, является простой моделью. Если требуемая область действия объекта подходит для этой модели, тогда нет необходимости использовать new()
, с привязкой delete()
и проверкой указателей NULL.
В случае, когда у вас есть много недолговечных объектов, выделение в стеке должно уменьшить проблемы фрагментации кучи.
Однако, если время жизни вашего объекта должно превышать текущую область, то new()
- правильный ответ. Просто убедитесь, что вы обращаете внимание на то, когда и как вы называете delete()
, и возможности указателей NULL, используя удаленные объекты и все другие ошибки, которые идут с использованием указателей.
Ответ 10
Когда вы используете новое, объекты присваиваются куче. Он обычно используется, когда вы ожидаете расширения. Когда вы объявляете объект, например,
Class var;
он помещается в стек.
Вам всегда нужно вызвать destroy на объект, который вы разместили в куче, с новым. Это открывает возможности утечки памяти. Объекты, помещенные в стек, не подвержены утечке памяти!
Ответ 11
Одна из важных причин избежать чрезмерного использования кучи для производительности - это, в частности, использование механизма управления памятью по умолчанию, используемого С++. Хотя распределение может быть довольно быстрым в тривиальном случае, выполнение большого количества new
и delete
на объектах неравномерного размера без строгого порядка приводит не только к фрагментации памяти, но также усложняет алгоритм распределения и может полностью уничтожить производительность в определенных случаях.
Проблема с тем, что пулы памяти, которые созданы для решения, позволяя смягчить присущие ему недостатки традиционных реализаций кучи, в то же время позволяя вам используйте кучу по мере необходимости.
Лучше все же, чтобы избежать проблемы вообще. Если вы можете поместить его в стек, сделайте это.
Ответ 12
Я думаю, что плакат означал You do not have to allocate everything on the
heap
, а не stack
.
В общем случае объекты выделяются в стеке (если размер объекта позволяет, конечно) из-за дешевой стоимости распределения стека, а не для распределения на основе кучи, что требует значительной работы распределителя и добавляет многословия, потому что тогда вы должны управлять данными, выделенными в куче.
Ответ 13
Я склонен не соглашаться с идеей использования нового "слишком много". Хотя использование оригинального плаката нового с системными классами немного смешно. (int *i; i = new int[9999];
? действительно? int i[9999];
намного яснее.) Я думаю, что это то, что получало кокер-комментатор.
Когда вы работаете с системными объектами, очень редко вам понадобится более одной ссылки на тот же самый объект. Пока значение одно и то же, все, что имеет значение. И системные объекты обычно не занимают много места в памяти. (один байт на символ, в строке). И если они это сделают, библиотеки должны быть разработаны для учета этого управления памятью (если они хорошо написаны). В этих случаях (все, кроме одного или двух из новостей в его коде) новое практически бессмысленно и служит лишь для того, чтобы вводить путаницы и возможности для ошибок.
Однако, когда вы работаете со своими собственными классами/объектами (например, исходный класс Line poster), вы должны начать думать о проблемах, таких как объем памяти, постоянство данных и т.д. самостоятельно. На данный момент использование нескольких ссылок на одно и то же значение неоценимо - оно позволяет создавать такие конструкции, как связанные списки, словари и графики, где несколько переменных должны иметь не только одно и то же значение, но и ссылаться на один и тот же объект в памяти. Однако класс Line не имеет ни одного из этих требований. Таким образом, исходный код плаката на самом деле абсолютно не нужен для new
.
Ответ 14
Две причины:
- В этом случае это не нужно. Вы делаете свой код ненужным более сложным.
- Он выделяет пространство в куче, и это означает, что вы должны помнить
delete
позже, иначе это приведет к утечке памяти.
Ответ 15
new
есть новый goto
.
Напомним, почему goto
настолько оскорблен: хотя он является мощным инструментом низкого уровня для управления потоком, люди часто использовали его излишне сложными способами, из-за которых сложный код не выполнялся. Более того, наиболее полезные и простые для чтения шаблоны были закодированы в структурированных операциях программирования (например, for
или в while
); конечный эффект заключается в том, что код, где goto
является подходящим способом, довольно редок, если у вас возникает соблазн написать goto
, вы, вероятно, делаете что-то плохо (если вы действительно не знаете, что делаете).
new
аналогичен - он часто используется, чтобы сделать вещи излишне сложными и трудными для чтения, а наиболее полезные шаблоны использования могут быть закодированы, были закодированы в различные классы. Кроме того, если вам нужно использовать любые новые шаблоны использования, для которых еще нет стандартных классов, вы можете написать свои собственные классы, которые их кодируют!
Я бы даже утверждал, что new
хуже, чем goto
, из-за необходимости соединять new
и delete
утверждения.
Как и goto
, если вы когда-нибудь думаете, что вам нужно использовать new
, вы, вероятно, делаете что-то плохое, особенно если вы делаете это за пределами реализации класса, целью которого в жизни является инкапсуляция любых динамических распределений, которые вам нужно сделать.
Ответ 16
Основная причина в том, что объекты в куче всегда сложны в использовании и управлении, чем простые значения. Написание кода, который легко читать и поддерживать, всегда является первоочередной задачей любого серьезного программиста.
Другой сценарий - библиотека, которую мы используем, предоставляет семантику значений и ненужное динамическое распределение. Std::string
- хороший пример.
Однако для объектно-ориентированного кода использование указателя, которое означает использование new
для его создания заранее, является обязательным. Чтобы упростить сложность управления ресурсами, у нас есть десятки инструментов, позволяющих сделать это как можно проще, например интеллектуальные указатели. Парадигма, основанная на объекте или генерическая парадигма, предполагает семантику значений и требует меньше или вообще не new
, как отмечали другие плакаты.
Традиционные шаблоны проектирования, особенно те, о которых говорится в GoF, используйте new
много, так как они являются типичным кодом OO.
Ответ 17
Еще одно замечание ко всем приведенным выше правильным ответам, это зависит от того, какое программирование вы делаете. Например, разработка ядра в Windows → Стек сильно ограничен, и вы не сможете воспринимать ошибки страницы, как в режиме пользователя.
В таких средах новые или C-подобные вызовы API предпочтительны и даже необходимы.
Конечно, это всего лишь исключение из правил.
Ответ 18
new
выделяет объекты в куче. В противном случае объекты выделяются в стеке. Посмотрите разницу между двумя.