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


873

Я наткнулся на вопрос переполнения стека Утечка памяти с помощью std :: string при использовании std :: list <std :: string> , и один из комментариев говорит об этом:

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

Я не совсем уверен, что он имеет в виду.

Почему объекты должны создаваться по значению в C ++ как можно чаще, и какое это имеет внутреннее значение?
Я неправильно истолковал ответ?

Ответы:


1037

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

стек

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

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

отвал

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

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

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

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

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

  • Вы хотите выделить память, которая будет сохраняться после выхода из текущего блока. Например, вы можете написать функцию, 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экземпляр и все содержимое строки, и все освобождается автоматически .


55
+1 за упоминание RAII в конце, но должно быть что-то об исключениях и раскручивании стека.
Тобу

7
@Tobu: да, но этот пост уже довольно длинный, и я хотел бы сосредоточиться на вопросе OP. Я в конечном итоге напишу сообщение в блоге или что-то и ссылку на него отсюда.
Андре Карон

15
Было бы хорошим дополнением упомянуть обратную сторону для размещения стека (по крайней мере, до C ++ 1x) - вам часто нужно копировать вещи без необходимости, если вы неосторожны. например, Monsterвыплевывает Treasureк тому, Worldкогда он умирает. В своем Die()методе он добавляет сокровище в мир. Его нужно использовать world->Add(new Treasure(/*...*/))в другом, чтобы сохранить сокровище после его смерти. Альтернативы shared_ptr(могут быть излишними), auto_ptr(плохая семантика для передачи права собственности), передача по значению (расточительная) и move+ unique_ptr(пока не получили широкого применения).
kizzx2

7
То, что вы сказали о локальных переменных, размещенных в стеке, может немного ввести в заблуждение. «Стек» относится к стеку вызовов, в котором хранятся кадры стека . Именно эти стековые фреймы хранятся в стиле LIFO. Локальные переменные для определенного фрейма распределяются так, как если бы они были членами структуры.
Someguy

7
@ Someguy: Действительно, объяснение не идеально. Реализация имеет свободу в своей политике распределения. Однако переменные должны быть инициализированы и уничтожены способом LIFO, поэтому аналогия верна. Я не думаю, что это усложняет ответ дальше.
Андре Карон

171

Потому что стек быстрее и герметичнее

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


18
«требуется всего одна инструкция для выделения места» - о, чепуха. Конечно, для добавления к указателю стека требуется всего одна инструкция, но если у класса есть какая-либо интересная внутренняя структура, будет гораздо больше, чем просто добавление к указателю стека. В равной степени справедливо сказать, что в Java не требуется никаких инструкций для выделения пространства, потому что компилятор будет управлять ссылками во время компиляции.
Чарли Мартин

31
@ Чарли прав. Автоматические переменные бывают быстрыми и надежными .
Оливер Чарльзуорт

28
@Charlie: Внутренние классы должны быть настроены в любом случае. Проводится сравнение по выделению необходимого места.
Оливер Чарльзуорт

51
кашель int x; return &x;
peterchen

16
быстро да Но, конечно, не надежно. Нет ничего надежного. Вы можете получить StackOverflow :)
rxantos

107

Причина, почему это сложно.

Во-первых, 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 */ }

Видишь идею?


1
На это замечание ... Если объект динамически размещается в main(), существует на время программы, не может быть легко создан в стеке из-за ситуации, и указатели на него передаются любым функциям, которые требуют к нему доступа Может ли это привести к утечке в случае сбоя программы, или это будет безопасно? Я бы предположил последнее, поскольку ОС, освобождающая всю память программы, должна также логически освобождать ее, но я не хочу ничего предполагать, когда дело доходит до new.
Джастин Тайм - Восстановить Монику

4
@JustinTime Вам не нужно беспокоиться об освобождении памяти динамически размещаемых объектов, которые должны оставаться в течение всей жизни программы. Когда программа выполняется, ОС создает для нее атлас физической памяти или виртуальной памяти. Каждый адрес в пространстве виртуальной памяти сопоставляется с адресом физической памяти, и при выходе из программы все, что отображается в его виртуальной памяти, освобождается. Таким образом, пока программа полностью завершает работу, вам не нужно беспокоиться о том, что выделенная память никогда не будет удалена.
Айман Аль-Эриани

75

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

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

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

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

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


9
+1. Re "Объекты, созданные с помощью newдолжны быть в конечном итоге, deleteчтобы они утекли". - что еще хуже, new[]должно соответствовать delete[], и вы получите неопределенное поведение, если вы delete new[]-ed память или delete[] new-ed память - очень немногие компиляторы предупреждают об этом (некоторые инструменты, такие как Cppcheck, когда они могут).
Тони Делрой

3
@TonyDelroy Бывают ситуации, когда компилятор не может этого предупредить. Если функция возвращает указатель, она может быть создана, если new (единственный элемент) или new [].
fbafelipe

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

5
На самом деле, любой язык, который выделяет память, имеет менеджер памяти, включая c. Большинство просто очень просто, то есть int * x = malloc (4); int * y = malloc (4); ... первый вызов будет выделять память, то есть запрашивать у памяти (обычно в блоках 1k / 4k), так что второй вызов фактически не выделяет память, а дает вам часть последнего блока, для которого он выделен. IMO, сборщики мусора не являются менеджерами памяти, потому что они обрабатывают только автоматическое освобождение памяти. Чтобы называться диспетчером памяти, он должен не только обрабатывать освобождение, но и выделять память.
Рахли

1
Локальные переменные используют стек, поэтому компилятор не отправляет вызов malloc()или его друзей для выделения необходимой памяти. Однако стек не может освободить какой-либо элемент в стеке, единственный способ, которым когда-либо освобождается память стека, - это раскручивание с вершины стека.
Микко Ранталайнен

C ++ не «использует процедуры операционной системы»; это не часть языка, это просто обычная реализация. C ++ может даже работать без какой-либо операционной системы.
einpoklum

22

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

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

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

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

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

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


2
И того, и другого можно избежать, используя размещение new / delete и выделение памяти заранее. Или вы можете выделить / освободить память самостоятельно, а затем вызвать конструктор / деструктор. Так обычно работает std :: vector.
rxantos

1
@ rxantos Пожалуйста, прочитайте OP, этот вопрос о том, как избежать ненужного выделения памяти. Кроме того, нет места размещения удалить.
Эмили Л.

@ Эмили Это то, что имел в виду ОП, я думаю:void * someAddress = ...; delete (T*)someAddress
xryl669

1
Использование стека также не является детерминированным во время выполнения. Если вы не звонили mlock()или что-то подобное. Это связано с тем, что системе может не хватать памяти, и для стека нет готовых страниц физической памяти, поэтому ОС может потребоваться обменять или записать некоторые кэши (очистить грязную память) на диск, прежде чем можно будет продолжить выполнение.
Микко Ранталайнен,

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

21

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, было предоставлено Барри в другом ответе .

Спасибо @Remy Lebeau за то, что он указал, что это все еще проблема в C ++ 17 (хотя и в меньшей степени): shared_ptrконструктор может не выделить свой блок управления и выполнить команду throw, и в этом случае переданный ему указатель не удаляется.

Решение: использовать make_shared.


5
Другое решение: никогда не выделяйте динамически более одного объекта в строке.
Сурьма

3
@Antimony: Да, гораздо более заманчиво выделить более одного объекта, когда вы уже выделили один, по сравнению с тем, когда вы еще не выделяли.
user541686

1
Я думаю, что лучшим ответом будет то, что smart_ptr утечет, если вызвано исключение, и ничто его не перехватит.
Натали Адамс

2
Даже в случае после C ++ 17 утечка все же может произойти, если newона shared_ptrзавершится успешно, а затем последующее построение завершится неудачно. std::make_shared()решил бы это тоже
Реми Лебо

1
@ Mehrdad рассматриваемый shared_ptrконструктор выделяет память для блока управления, в котором хранятся общий указатель и средство удаления, так что да, теоретически это может вызвать ошибку памяти. Только конструкторы копирования, перемещения и создания псевдонимов не выбрасывают. make_sharedвыделяет общий объект внутри самого блока управления, поэтому вместо 2 выделяется только 1
Remy Lebeau

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 ; }

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


4
Вы должны взглянуть на код в указанном вопросе. В этом коде определенно много чего не так.
Андре Карон

7
Я согласен, что нет ничего плохого в использовании new per se , но если вы посмотрите на исходный код, на который ссылался комментарий, то newэто злоупотребление. Код написан так, как если бы это был Java или C #, где newон используется практически для каждой переменной, когда в стеке есть куда больше смысла.
Люк

5
Честная оценка. Но общие правила обычно применяются, чтобы избежать распространенных ошибок. Была ли это слабость индивидуума или нет, управление памятью достаточно сложно, чтобы гарантировать общее правило, подобное этому! :)
Robben_Ford_Fan_boy

9
@Charlie: комментарий не говорит, что вы никогда не должны использовать new. Это говорит о том, что если у вас есть выбор между динамическим размещением и автоматическим хранением, используйте автоматическое хранение.
Андре Карон

8
@ Чарли: нет ничего плохого в использовании new, но если вы используете delete, вы делаете это неправильно!
Матье М.

16

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

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

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


9
«если время жизни вашего объекта должно выходить за пределы текущей области видимости, тогда new () является правильным ответом» ... почему бы не вернуть преимущественно значение или не принять переменную в области вызова с помощью non- constref или pointer ...?
Тони Делрой

2
@ Тони: Да, да! Я рад слышать, что кто-то защищает ссылки. Они были созданы, чтобы предотвратить эту проблему.
Натан Осман

@TonyD ... или объединить их: вернуть умный указатель по значению. Таким образом, вызывающий абонент и во многих случаях (то есть, где make_shared/_uniqueэто возможно) вызываемый абонент никогда не должен newили delete. В этом ответе пропущены реальные моменты: (A) C ++ предоставляет такие вещи, как RVO, семантика перемещения и выходные параметры, что часто означает, что обработка создания объекта и продления срока службы путем возврата динамически распределенной памяти становится ненужной и небрежной. (B) Даже в ситуациях, когда требуется динамическое размещение, stdlib предоставляет оболочки RAII, которые освобождают пользователя от уродливых внутренних деталей.
underscore_d

14

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

Class var;

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

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


2
+1 "[куча] обычно используется, когда вы ожидаете расширения" - как добавление к std::stringили std::map, да, острое понимание. Моей первоначальной реакцией было «но также очень часто отделить время жизни объекта от области действия создаваемого кода», но на самом деле constлучше возвращать по значению или принимать значения в области вызывающего абонента без ссылки или указателя, за исключением случаев, когда требуется «расширение» тоже. Хотя есть и другие способы использования звука, например, заводские методы ...
Тони Делрой,

12

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

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

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


Вы всегда можете выделить достаточно большой объем памяти и затем использовать размещение new / delete, если скорость является проблемой.
rxantos

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

10

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

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


10

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

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

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


обычно new / delete будет использоваться, когда вы не знаете заранее размер массива. Конечно, std :: vector скрывает новый / удалить для вас. Вы все еще используете их, но через std :: vector. Так что в настоящее время он будет использоваться, когда вы не знаете размер массива и хотите по какой-то причине избежать издержек std :: vector (который небольшой, но все еще существует).
rxantos

When you're working with your own classes/objects... у вас часто нет причин делать это! Крошечная доля Qs находится на деталях конструкции контейнера опытными программистами. В противоположность этому , угнетающее доля в о путанице новичков , которые не знают , что STDLIB существует - или активно данной жуткие задания в «программирования» «курсы», где учитель требует они бесцельно изобретать колесо - прежде , чем они даже узнал что такое колесо и почему оно работает. Продвигая более абстрактное распределение, C ++ может спасти нас от бесконечного «segfault со связанным списком» C; пожалуйста, давай позволим .
underscore_d

« Использование оригинального плаката новых с системными классами немного нелепо. (« int *i; i = new int[9999];Действительно? » int i[9999];намного яснее.)« Да, это более понятно, но, играя в адвокат дьявола, этот тип не обязательно является плохим аргументом. С 9999 элементами я могу представить себе жесткую встроенную систему, в которой недостаточно стека для 9999 элементов: 9999x4 байта составляет ~ 40 кБ, x8 ~ 80 кБ. Таким образом, такие системы, возможно, должны использовать динамическое распределение, предполагая, что они реализуют его, используя альтернативную память. Тем не менее, это может только оправдать динамическое распределение, а не new; в этом случае vectorбыло бы настоящим исправлением
underscore_d

Согласитесь с @underscore_d - это не такой хороший пример. Я бы не добавил 40000 или 80000 байт в свой стек просто так. Я бы на самом деле, вероятно, распределить их по куче ( std::make_unique<int[]>()конечно).
einpoklum

3

Две причины:

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

2

newэто новое goto.

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

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

Я бы даже утверждать , что newэто хуже , чем goto, в связи с необходимостью пары newи deleteотчетности.

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


И я бы добавил: «Тебе это просто не нужно».
einpoklum

1

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

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

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

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


4
Это ужасный ответ. 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, делая это
underscore_d

1

Еще один момент ко всем приведенным выше правильным ответам, это зависит от того, какое программирование вы делаете. Например, разработка ядра в Windows -> Стек сильно ограничен, и вы не сможете воспринимать ошибки страницы, как в режиме пользователя.

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

Конечно, это всего лишь исключение из правил.


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.