Я наткнулся на вопрос "Переполнение стека". Утечка памяти с помощью std :: string при использовании std :: list <std :: string>, и один из комментариев говорит об этом:
Прекратите использовать
new
так много. Я не вижу причины, по которой вы использовали новое, где бы вы ни были. Вы можете создавать объекты по значению в C++ и это одно из огромных преимуществ использования языка. Вам не нужно выделять все в кучу. Прекратите думать, как программист на Java.
Я не совсем уверен, что он имеет в виду. Почему объекты должны быть созданы по значению в C++ настолько часто, насколько это возможно, и какая разница делает его внутренне? Я неправильно понял ответ?
Существует два широко используемых метода выделения памяти: автоматическое распределение и динамическое распределение. Обычно для каждой области памяти имеется соответствующая область памяти: стек и куча.
Стек всегда выделяет память последовательным образом. Он может сделать это, потому что он требует освобождения памяти в обратном порядке (First-In, Last-Out: FILO). Это метод распределения памяти для локальных переменных на многих языках программирования. Это очень, очень быстро, потому что он требует минимальной бухгалтерской отчетности, а следующий адрес для выделения является неявным.
В С++ это называется автоматическим хранилищем, поскольку хранилище автоматически заявляется в конце области. Как только выполняется выполнение текущего блока кода (с разделителем с помощью {}
), память для всех переменных в этом блоке будет автоматически собрана. Это также момент, когда деструкторы вызываются для очистки ресурсов.
Куча позволяет использовать более гибкий режим выделения памяти. Бухгалтерия является более сложной и распределение происходит медленнее. Поскольку не существует неявной точки выпуска, вы должны освободить память вручную, используя 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
и все содержимое строки, и все автоматически освобождается.
В С++ для каждой локальной области видимости в заданной функции требуется только одна команда для выделения пространства - в стеке, и невозможно утечка какой-либо из этой памяти. Этот комментарий предполагал (или должен был предполагать) сказать что-то вроде "использовать стек, а не кучу".
Это сложно.
Во-первых, С++ не собирает мусор. Поэтому для каждого нового должно быть соответствующее удаление. Если вы не введете это удаление, тогда у вас будет утечка памяти. Теперь для простого случая:
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 */ }
Посмотрите идею?
main()
, существует на время выполнения программы, не может быть легко создан в стеке из-за ситуации, и указатели на него передаются любым функциям, которые требуют доступ к нему, может ли это привести к утечке в случае сбоя программы, или это будет безопасно? Я бы предположил последнее, поскольку операционная система, освобождающая всю память программы, должна также логически освобождать ее, но я не хочу ничего предполагать, когда дело касается new
.
Объекты, созданные new
, должны быть в конечном итоге delete
d, чтобы они не протекали. Деструктор не будет вызван, память не будет освобождена, весь бит. Поскольку С++ не содержит сборку мусора, это проблема.
Объекты, созданные по значению (то есть на стеке), автоматически умирают, когда они выходят за рамки. Деструкторный вызов вставляется компилятором, и память автоматически освобождается после возврата функции.
Умные указатели, такие как auto_ptr
, shared_ptr
, решают проблему с обвисшей ссылкой, но им нужна дисциплина кодирования и есть другие проблемы (копируемость, циклы ссылок и т.д.).
Кроме того, в сценариях с большой степенью многопоточности new
является точкой раздора между потоками; может возникнуть влияние производительности на чрезмерное использование new
. Создание объекта стека по определению является поточно-локальным, поскольку каждый поток имеет свой собственный стек.
Недостатком объектов значений является то, что они умирают после возвращения функции хоста - вы не можете передавать ссылку на тех, кто возвращается к вызывающему, только путем копирования или возврата по значению.
new
должны быть в конечном итоге delete
чтобы они не просочились". - что еще хуже, new[]
должен соответствовать delete[]
, и вы получите неопределенное поведение, если delete
new[]
-ed память или delete[]
new
-ed memory - очень немногие компиляторы предупреждают об этом (некоторые инструменты, такие как Cppcheck, делают когда они могут).
malloc()
или его друзьям для выделения необходимой памяти. Однако стек не может освободить какой-либо элемент в стеке, единственный способ, которым когда-либо освобождается память стека, - это раскручивание с вершины стека.
Я вижу, что несколько важных причин для того, чтобы сделать как можно меньше новых, пропущены:
new
имеет недетерминированное время выполненияВызов new
может или не может заставить ОС выделять новую физическую страницу для вашего процесса, это может быть довольно медленным, если вы часто это делаете. Или у этого уже может быть подходящее место памяти, мы не знаем. Если ваша программа должна иметь согласованное и прогнозируемое время выполнения (например, в режиме реального времени или в игре/физическом моделировании), вам нужно избегать new
в ваших критических циклах времени.
new
- это неявная синхронизация потоковДа, вы меня слышали, ваша ОС должна убедиться, что ваши таблицы страниц согласованы, и поскольку такой вызов new
приведет к тому, что ваш поток получит неявный блокировку мьютекса. Если вы последовательно вызываете new
из многих потоков, вы фактически сериализуете свои потоки (я сделал это с 32 процессорами, каждый из которых попадал на new
, чтобы получить несколько сотен байт каждый, ох! Это была королевская лаваша для отладки )
Остальные, такие как медленная, фрагментация, склонность к ошибкам и т.д., уже упоминались в других ответах.
В значительной степени тот, кто поднимает свои слабости до общего правила. Там нет ничего плохого в создании объектов с помощью оператора 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 ; }
в классе; без него вы будете утечка памяти из кучи, даже если вы явно не выделили кучу вообще.
new
как такового , но если вы посмотрите на оригинальный код, на который ссылался комментарий, то new
злоупотребляют. Код написан так, как будто это был Java или C #, где new
используется практически для каждой переменной, когда в стеке гораздо больше смысла.
Рассмотрим "осторожного" пользователя, который не забывает оборачивать объекты в умные указатели:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
Этот код опасен, потому что нет никакой гарантии, что shared_ptr
до T1
или T2
. Следовательно, если один из new T1()
или new T2()
завершится неудачно после того, как другой преуспеет, то первый объект будет пропущен, потому что не существует shared_ptr
для его уничтожения и освобождения.
Решение: используйте make_shared
.
Это больше не проблема: С++ 17 налагает ограничение на порядок этих операций, в этом случае гарантируя, что за каждым вызовом new()
должен немедленно следовать построение соответствующего умного указателя, без других операций в между.Это подразумевает, что ко времени вызова второго new()
гарантируется, что первый объект уже обернут в его умный указатель, таким образом предотвращая любые утечки в случае возникновения исключения.
Более подробное объяснение нового порядка оценки, введенного С++ 17, было предоставлено Барри в другом ответе.
Спасибо @Remy Lebeau за то, что он указал, что это все еще проблема в С++ 17 (хотя и не так): конструктор shared_ptr
может не выделить свой управляющий блок и выполнить throw, и в этом случае переданный ему указатель не удаляется.
Решение: используйте make_shared
.
new()
не следует использовать как можно меньше. Его следует использовать как можно более тщательно. И он должен использоваться так часто, как необходимо, как это продиктовано прагматизмом.
Распределение объектов в стеке, опираясь на их неявное разрушение, является простой моделью. Если требуемая область действия объекта подходит для этой модели, тогда нет необходимости использовать new()
, с привязкой delete()
и проверкой указателей NULL.
В случае, когда у вас есть много недолговечных объектов, выделение в стеке должно уменьшить проблемы фрагментации кучи.
Однако, если время жизни вашего объекта должно превышать текущую область, то new()
- правильный ответ. Просто убедитесь, что вы обращаете внимание на то, когда и как вы называете delete()
, и возможности указателей NULL, используя удаленные объекты и все другие ошибки, которые идут с использованием указателей.
const
ref или указателя ...?
Когда вы используете новое, объекты присваиваются куче. Он обычно используется, когда вы ожидаете расширения. Когда вы объявляете объект, например,
Class var;
он помещается в стек.
Вам всегда нужно вызвать destroy на объект, который вы разместили в куче, с новым. Это открывает возможности утечки памяти. Объекты, помещенные в стек, не подвержены утечке памяти!
std::string
или std::map
, да, острое понимание. Моей первоначальной реакцией было «но также очень часто отделить время жизни объекта от области действия создаваемого кода», но на самом деле лучше возвращать по значению или принимать значения в области вызывающего абонента по const
ссылке или указателю, за исключением случаев, когда есть «расширение» участвует тоже. Хотя есть и другие способы использования звука, такие как заводские методы ....
Одна из важных причин избежать чрезмерного использования кучи для производительности - это, в частности, использование механизма управления памятью по умолчанию, используемого С++. Хотя распределение может быть довольно быстрым в тривиальном случае, выполнение большого количества new
и delete
на объектах неравномерного размера без строгого порядка приводит не только к фрагментации памяти, но также усложняет алгоритм распределения и может полностью уничтожить производительность в определенных случаях.
Проблема с тем, что пулы памяти, которые созданы для решения, позволяя смягчить присущие ему недостатки традиционных реализаций кучи, в то же время позволяя вам используйте кучу по мере необходимости.
Лучше все же, чтобы избежать проблемы вообще. Если вы можете поместить его в стек, сделайте это.
Я склонен не соглашаться с идеей использования нового "слишком много". Хотя использование оригинального плаката нового с системными классами немного смешно. (int *i; i = new int[9999];
? действительно? int i[9999];
намного яснее.) Я думаю, что это то, что получало кокер-комментатор.
Когда вы работаете с системными объектами, очень редко вам понадобится более одной ссылки на тот же самый объект. Пока значение одно и то же, все, что имеет значение. И системные объекты обычно не занимают много места в памяти. (один байт на символ, в строке). И если они это сделают, библиотеки должны быть разработаны для учета этого управления памятью (если они хорошо написаны). В этих случаях (все, кроме одного или двух из новостей в его коде) новое практически бессмысленно и служит лишь для того, чтобы вводить путаницы и возможности для ошибок.
Однако, когда вы работаете со своими собственными классами/объектами (например, исходный класс Line poster), вы должны начать думать о проблемах, таких как объем памяти, постоянство данных и т.д. самостоятельно. На данный момент использование нескольких ссылок на одно и то же значение неоценимо - оно позволяет создавать такие конструкции, как связанные списки, словари и графики, где несколько переменных должны иметь не только одно и то же значение, но и ссылаться на один и тот же объект в памяти. Однако класс Line не имеет ни одного из этих требований. Таким образом, исходный код плаката на самом деле абсолютно не нужен для new
.
When you're working with your own classes/objects
... у вас часто нет причин делать это! Крошечная доля Qs находится на деталях конструкции контейнера опытными программистами. В противоположности этому , угнетающая доля около смешения новичков , которые не знают , что STDLIB существует - или активно заданных жуткие заданий в «программировании» «курсы», где репетитор требование , которые они бесцельно изобретать колесо - прежде , чем они даже узнал что такое колесо и почему оно работает. Содействуя более абстрактному распределению, C ++ может спасти нас от бесконечного «segfault со связанным списком» C; пожалуйста, давай позволим .
Я думаю, что плакат означал You do not have to allocate everything on the
heap
, а не stack
.
В общем случае объекты выделяются в стеке (если размер объекта позволяет, конечно) из-за дешевой стоимости распределения стека, а не для распределения на основе кучи, что требует значительной работы распределителя и добавляет многословия, потому что тогда вы должны управлять данными, выделенными в куче.
Две причины:
delete
позже, иначе это приведет к утечке памяти.Еще одно замечание ко всем приведенным выше правильным ответам, это зависит от того, какое программирование вы делаете. Например, разработка ядра в Windows → Стек сильно ограничен, и вы не сможете воспринимать ошибки страницы, как в режиме пользователя.
В таких средах новые или C-подобные вызовы API предпочтительны и даже необходимы.
Конечно, это всего лишь исключение из правил.
new
есть новый goto
.
Напомним, почему goto
настолько оскорблен: хотя он является мощным инструментом низкого уровня для управления потоком, люди часто использовали его излишне сложными способами, из-за которых сложный код не выполнялся. Более того, наиболее полезные и простые для чтения шаблоны были закодированы в структурированных операциях программирования (например, for
или в while
); конечный эффект заключается в том, что код, где goto
является подходящим способом, довольно редок, если у вас возникает соблазн написать goto
, вы, вероятно, делаете что-то плохо (если вы действительно не знаете, что делаете).
new
аналогичен - он часто используется, чтобы сделать вещи излишне сложными и трудными для чтения, а наиболее полезные шаблоны использования могут быть закодированы, были закодированы в различные классы. Кроме того, если вам нужно использовать любые новые шаблоны использования, для которых еще нет стандартных классов, вы можете написать свои собственные классы, которые их кодируют!
Я бы даже утверждал, что new
хуже, чем goto
, из-за необходимости соединять new
и delete
утверждения.
Как и goto
, если вы когда-нибудь думаете, что вам нужно использовать new
, вы, вероятно, делаете что-то плохое, особенно если вы делаете это за пределами реализации класса, целью которого в жизни является инкапсуляция любых динамических распределений, которые вам нужно сделать.
Основная причина в том, что объекты в куче всегда сложны в использовании и управлении, чем простые значения. Написание кода, который легко читать и поддерживать, всегда является первоочередной задачей любого серьезного программиста.
Другой сценарий - библиотека, которую мы используем, предоставляет семантику значений и ненужное динамическое распределение. Std::string
- хороший пример.
Однако для объектно-ориентированного кода использование указателя, которое означает использование new
для его создания заранее, является обязательным. Чтобы упростить сложность управления ресурсами, у нас есть десятки инструментов, позволяющих сделать это как можно проще, например интеллектуальные указатели. Парадигма, основанная на объекте или генерическая парадигма, предполагает семантику значений и требует меньше или вообще не new
, как отмечали другие плакаты.
Традиционные шаблоны проектирования, особенно те, о которых говорится в GoF, используйте new
много, так как они являются типичным кодом OO.
For object oriented code, using a pointer [...] is a must
: нонсенс . Если вы обесцениваете «OO», ссылаясь только на небольшое подмножество, полиморфизм - тоже нонсенс: ссылки тоже работают. [pointer] means use new to create it beforehand
: особенно бессмыслица : ссылки или указатели могут быть взяты на автоматически размещенные объекты и использованы полиморфно; следи за мной [typical OO code] use new a lot
: может быть, в какой-то старой книге, но кого это волнует? Любой смутно современный C ++ избегает new
/ необработанных указателей везде, где это возможно - и ни в коем случае не является чем-то вроде OO, делая это
new
выделяет объекты в куче. В противном случае объекты выделяются в стеке. Посмотрите разницу между двумя.