Почему программисты на C ++ должны минимизировать использование «нового»? - PullRequest
815 голосов
/ 28 июня 2011

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

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

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

Ответы [ 18 ]

968 голосов
/ 28 июня 2011

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

Стек

Стек всегда выделяет память последовательно.Это может быть сделано, потому что это требует от вас освобождения памяти в обратном порядке (первый вход, последний выход: 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 экземпляр и все содержимое строки и все автоматически освобождается .

165 голосов
/ 28 июня 2011

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

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

102 голосов
/ 28 июня 2011

Это сложно.

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

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

Это просто. Но что произойдет, если «Do stuff» создаст исключение? Упс: утечка памяти. Что произойдет, если "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 */ }

Видите идею?

73 голосов
/ 28 июня 2011

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

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

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

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

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

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

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

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

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

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

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

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

18 голосов
/ 15 августа 2013

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

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

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

17 голосов
/ 28 июня 2011

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

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

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

    // do things

 }

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

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

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

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

     delete foo;
  }

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

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

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

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

И да, вам лучше иметь

     ~File(){ delete fd ; }

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

15 голосов
/ 28 июня 2011

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

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

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

13 голосов
/ 28 июня 2011

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

Class var;

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

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

...