Почему программисты на C++ должны минимизировать использование "нового"?
Я наткнулся на вопрос переполнения стека Утечка памяти с помощью std::string при использовании std::list
Прекратить использование
new
так много. Я не вижу никакой причины, по которой ты использовал новое, где бы ты ни был. Вы можете создавать объекты по значению в C++, и это одно из огромных преимуществ использования языка. Вам не нужно распределять все по куче. Перестань думать как программист на Java.
Я не совсем уверен, что он имеет в виду. Почему объекты должны создаваться по значению в C++ как можно чаще, и какое это имеет внутреннее значение? Я неправильно истолковал ответ?
19 ответов
Существует два широко используемых метода выделения памяти: автоматическое распределение и динамическое распределение. Обычно для каждой области памяти существует соответствующая область: стек и куча.
стек
Стек всегда выделяет память последовательно. Это может быть сделано, потому что это требует от вас освобождения памяти в обратном порядке (первый вход, последний выход: FILO). Это метод выделения памяти для локальных переменных во многих языках программирования. Это очень, очень быстро, потому что требует минимальной бухгалтерии, а следующий адрес для выделения неявный.
В C++ это называется автоматическим хранением, потому что хранилище запрашивается автоматически в конце области. Как только выполнение текущего блока кода (разделено с помощью {}
), память для всех переменных в этом блоке автоматически собирается. Это также момент, когда деструкторы вызываются для очистки ресурсов.
отвал
Куча обеспечивает более гибкий режим выделения памяти. Бухгалтерия более сложная, а распределение медленнее. Поскольку нет неявной точки освобождения, вы должны освободить память вручную, используя delete
или же delete[]
(free
в с). Однако отсутствие неявной точки освобождения является ключом к гибкости кучи.
Причины использовать динамическое распределение
Даже если использование кучи медленнее и потенциально приводит к утечкам памяти или фрагментации памяти, для динамического выделения есть совершенно хорошие сценарии использования, поскольку они менее ограничены.
Две основные причины использовать динамическое распределение:
Вы не знаете, сколько памяти вам нужно во время компиляции. Например, когда вы читаете текстовый файл в строку, вы обычно не знаете, какой размер у файла, поэтому вы не можете решить, какой объем памяти выделить, пока не запустите программу.
Вы хотите выделить память, которая будет сохраняться после выхода из текущего блока. Например, вы можете написать функцию
string readfile(string path)
который возвращает содержимое файла. В этом случае, даже если в стеке может содержаться все содержимое файла, вы не сможете вернуться из функции и сохранить выделенный блок памяти.
Почему динамическое распределение часто не нужно
В C++ есть аккуратная конструкция, называемая деструктором. Этот механизм позволяет вам управлять ресурсами путем выравнивания времени жизни ресурса с временем жизни переменной. Эта техника называется RAII и является отличительной чертой C++. Он "оборачивает" ресурсы в объекты. 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 считается лучшей практикой в 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
имеет копию данных someString
, Это не указатель; это отдельный объект. Они могут иметь одинаковое содержимое, но вы можете изменить одно, не влияя на другое:
someString += "More text.";
if(otherString == someString) { /*Will never get here */ }
Видишь идею?
Объекты, созданные new
должно быть в конце концов delete
чтобы они не протекали. Деструктор не будет вызван, память не будет освобождена, в целом. Поскольку в C++ нет сборки мусора, это проблема.
Объекты, созданные по значению (т. Е. В стеке), автоматически умирают, когда выходят из области видимости. Вызов деструктора вставляется компилятором, а память автоматически освобождается при возврате функции.
Умные указатели, такие как auto_ptr
, shared_ptr
решить проблему с висящими ссылками, но они требуют дисциплины кодирования и имеют другие проблемы (копируемость, циклы ссылок и т. д.).
Кроме того, в многопоточных сценариях, new
является предметом спора между потоками; может быть влияние на производительность из-за чрезмерного использования new
, Создание объекта стека по определению является локальным для потока, поскольку каждый поток имеет свой собственный стек.
Недостатком объектов-значений является то, что они умирают после возврата из функции хоста - вы не можете передать ссылку на них обратно вызывающей стороне, только путем копирования или возврата по значению.
- C++ не использует какой-либо менеджер памяти сам по себе. В других языках, таких как C#, в Java есть сборщик мусора для обработки памяти
- C++ использует процедуры операционной системы для выделения памяти и слишком много нового / удаления может фрагментировать доступную память
- В любом приложении, если память часто используется, рекомендуется предварительно выделить ее и освободить, когда она не требуется.
- Неправильное управление памятью может привести к утечкам памяти, и это действительно трудно отследить. Таким образом, использование стековых объектов в рамках функции является проверенной техникой.
- Недостатком использования стековых объектов является то, что он создает несколько копий объектов при возврате, передаче в функции и т. Д. Однако умные компиляторы хорошо осведомлены об этих ситуациях и хорошо оптимизированы для повышения производительности.
- Это действительно утомительно в C++, если память выделяется и освобождается в двух разных местах. Ответственность за выпуск - это всегда вопрос, и в основном мы полагаемся на некоторые общедоступные указатели, объекты стека (максимально возможный) и методы, такие как auto_ptr (объекты RAII)
- Лучше всего то, что вы контролируете память, а хуже всего то, что у вас не будет никакого контроля над памятью, если мы используем неправильное управление памятью для приложения. Сбои, вызванные повреждениями памяти, являются самыми неприятными и трудно отслеживаемыми.
Я вижу, что пропущено несколько важных причин сделать как можно меньше новых:
оператор new
имеет недетерминированное время выполнения
призвание new
может или не может заставить ОС выделять новую физическую страницу вашему процессу, это может быть довольно медленным, если вы делаете это часто. Или у него уже может быть подходящая ячейка памяти, мы не знаем. Если ваша программа должна иметь согласованное и предсказуемое время выполнения (как в системе реального времени или симуляции игры / физики), вам следует избегать new
в ваше время критические петли.
оператор new
неявная синхронизация потоков
Да, вы слышали меня, ваша ОС должна убедиться, что ваши таблицы страниц согласованы и как таковые вызовы new
заставит ваш поток получить неявную блокировку мьютекса. Если вы постоянно звоните new
из многих потоков вы фактически сериализуете свои потоки (я сделал это с 32 процессорами, каждый из которых new
чтобы получить несколько сотен байтов каждый, ой! это была королевская лава для отладки)
Остальные, такие как медленный, фрагментация, склонность к ошибкам и т. Д., Уже упоминались в других ответах.
Pre-C++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-C++17:
Это больше не проблема: C++17 накладывает ограничение на порядок этих операций, в этом случае гарантируя, что каждый вызов new()
должно немедленно сопровождаться созданием соответствующего интеллектуального указателя, без каких-либо других операций между ними. Это подразумевает, что ко времени второго new()
вызывается, гарантируется, что первый объект уже был обернут в его умный указатель, таким образом предотвращая любые утечки в случае возникновения исключения.
Более подробное объяснение нового порядка оценки, введенного C++ 17, было предоставлено Барри в другом ответе.
В значительной степени это тот, кто поднимает свои слабости до общего правила. В создании объектов с использованием 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++ вы этого не знаете. Самое большее, вы знаете, что небольшой объем памяти, скажем, один указатель, определенно выделяется в стеке. Тем не менее, рассмотрим, является ли реализация File чем-то вроде
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
затем FileImpl
будет по- прежнему размещаться в стеке.
И да, вам лучше иметь
~File(){ delete fd ; }
в классе тоже; без этого вы потеряете память из кучи, даже если вы явно не выделяете ее.
new()
не следует использовать как можно меньше. Его следует использовать как можно осторожнее. И это должно использоваться так часто, как это необходимо, продиктовано прагматизмом.
Распределение объектов в стеке, основанное на их неявном уничтожении, является простой моделью. Если требуемый объем объекта соответствует этой модели, то нет необходимости использовать new()
со связанным delete()
и проверка нулевых указателей. В случае, когда у вас много короткоживущих объектов размещения в стеке, следует уменьшить проблемы фрагментации кучи.
Однако, если время жизни вашего объекта должно выходить за рамки текущей области, то new()
это правильный ответ. Просто убедитесь, что вы обращаете внимание на то, когда и как вы звоните delete()
и возможности указателей NULL, использование удаленных объектов и всех других ошибок, которые приходят с использованием указателей.
Когда вы используете новый, объекты размещаются в куче. Обычно используется, когда вы ожидаете расширения. Когда вы объявляете объект, такой как,
Class var;
он помещается в стек.
Вы всегда должны будете вызывать уничтожение на объекте, который вы поместили в кучу с новым. Это открывает возможность для утечек памяти. Помещенные в стек объекты не подвержены утечке памяти!
Одна заметная причина, по которой следует избегать чрезмерного использования кучи, связана с производительностью, особенно с производительностью механизма управления памятью по умолчанию, используемого C++. Хотя в тривиальном случае распределение может быть довольно быстрым, new
а также delete
На объектах неоднородного размера без строгого порядка это приводит не только к фрагментации памяти, но также усложняет алгоритм выделения и может в определенных случаях абсолютно разрушить производительность.
Это проблема, которую пулы памяти создавали для решения, позволяя смягчить недостатки, присущие традиционным реализациям кучи, и в то же время позволять использовать кучи по мере необходимости.
Тем не менее, лучше вообще избежать этой проблемы. Если вы можете положить его в стек, то сделайте это.
Я склонен не соглашаться с идеей использования нового "слишком много". Хотя использование оригинального плаката новых с системными классами немного нелепо. (int *i; i = new int[9999];
? действительно? int i[9999];
гораздо яснее.) Я думаю, что это то, что получал козу комментатора.
Когда вы работаете с системными объектами, очень редко вам понадобится более одной ссылки на один и тот же объект. Пока значение одинаково, это все, что имеет значение. А системные объекты обычно не занимают много места в памяти. (один байт на символ в строке). И если они это сделают, библиотеки должны быть спроектированы так, чтобы учитывать это управление памятью (если они хорошо написаны). В этих случаях (все, кроме одной или двух новостей в его коде) новость практически бессмысленна и служит только для введения в заблуждение и потенциальной ошибки.
Однако, когда вы работаете со своими собственными классами / объектами (например, классом Line первоначального автора), вы должны сами начать думать о таких проблемах, как объем памяти, постоянство данных и т. Д. На этом этапе разрешение нескольких ссылок на одно и то же значение неоценимо - оно позволяет создавать такие конструкции, как связанные списки, словари и графики, где несколько переменных должны не только иметь одно и то же значение, но и ссылаться на один и тот же объект в памяти. Однако класс Line не имеет ни одного из этих требований. Таким образом, исходный код плаката на самом деле не нуждается в new
,
Я думаю, что плакат хотел сказать You do not have to allocate everything on the
heap
а не stack
,
В основном объекты размещаются в стеке (если, конечно, размер объекта позволяет) из-за дешевой стоимости размещения в стеке, а не распределения на основе кучи, которое требует довольно большой работы со стороны распределителя и добавляет многословность, потому что тогда вам нужно управлять данными, размещенными в куче.
Две причины:
- Это не нужно в этом случае. Вы делаете свой код без необходимости более сложным.
- Он выделяет место в куче, и это означает, что вы должны помнить
delete
это позже, или это приведет к утечке памяти.
Многие ответы были посвящены различным соображениям производительности. Я хочу обратиться к комментарию, озадачившему OP:
Перестаньте думать как программист на Java.
Действительно, в Java, как объясняется в ответе на этот вопрос,
Вы используете ключевое слово, когда объект явно создается впервые.
но в C++ объекты типа
T
создаются так:
T{}
(или
T{ctor_argument1,ctor_arg2}
для конструктора с аргументами). Вот почему обычно у вас просто нет причин хотеть использовать.
Итак, почему он вообще когда-либо использовался? Ну, по двум причинам:
- Вам нужно создать много значений, количество которых неизвестно во время компиляции.
- Из-за ограничений реализации C++ на обычных машинах - чтобы предотвратить переполнение стека , выделяя слишком много места, создавая значения обычным способом.
Теперь, помимо того, что подразумевал процитированный вами комментарий, вы должны отметить, что даже эти два случая, приведенные выше, рассмотрены достаточно хорошо, и вам не нужно «прибегать» к использованию себя:
- Вы можете использовать типы контейнеров из стандартных библиотек, которые могут содержать количество элементов, зависящих от времени выполнения (например,
std::vector
). - Вы можете использовать интеллектуальные указатели , которые дают вам указатель, похожий на, но гарантируют, что память будет освобождена там, где «указатель» выходит за пределы области видимости.
и по этой причине это официальный пункт Руководства по кодированию сообщества C++, чтобы избежать явного
new
а также
delete
: Руководство R.11.
new
это новый goto
,
Вспомните почему goto
это так поносило: хотя это мощный низкоуровневый инструмент для управления потоками, люди часто использовали его неоправданно сложными способами, которые затрудняли выполнение кода. Кроме того, наиболее полезные и простые для чтения шаблоны были закодированы в операторах структурированного программирования (например, for
или же while
); конечный эффект заключается в том, что код goto
это подходящий способ довольно редко, если вы хотите написать goto
вы, вероятно, делаете что-то плохо (если вы действительно не знаете, что делаете).
new
похож - он часто используется, чтобы сделать вещи излишне сложными и трудными для чтения, и наиболее полезные шаблоны использования могут быть закодированы в различные классы. Кроме того, если вам нужно использовать какие-либо новые шаблоны использования, для которых еще нет стандартных классов, вы можете написать свои собственные классы, которые их кодируют!
Я бы даже сказал, что new
хуже чем goto
, из-за необходимости в паре new
а также delete
заявления.
подобно goto
, если вы когда-либо думали, что вам нужно использовать new
вы, вероятно, делаете что-то плохо, особенно если вы делаете это вне реализации класса, целью которого в жизни является инкапсуляция любых динамических распределений, которые вам нужно сделать.
Еще один момент ко всем приведенным выше правильным ответам, это зависит от того, какое программирование вы делаете. Например, разработка ядра в Windows -> Стек сильно ограничен, и вы не сможете воспринимать ошибки страницы, как в режиме пользователя.
В таких средах новые или C-подобные вызовы API предпочтительны и даже необходимы.
Конечно, это всего лишь исключение из правил.
Основная причина в том, что объекты в куче всегда сложнее использовать и управлять, чем простые значения. Написание кода, который легко читать и поддерживать, всегда является приоритетом любого серьезного программиста.
Другой сценарий - библиотека, которую мы используем, обеспечивает семантику значений и делает ненужным динамическое размещение. Std::string
хороший пример
Однако для объектно-ориентированного кода используется указатель - что означает использование new
создать его заранее - обязательно. Чтобы упростить управление ресурсами, у нас есть десятки инструментов, которые делают его максимально простым, например, умные указатели. Объектная парадигма или универсальная парадигма предполагает семантику значений и требует меньше или не требует new
как и в других постерах.
Традиционные шаблоны дизайна, особенно те, которые упоминаются в книге GoF, используют new
много, так как они являются типичным ОО-кодом.
new
размещает объекты в куче. В противном случае объекты размещаются в стеке. Посмотрите на разницу между ними.