Когда я должен использовать новое ключевое слово в C ++?


273

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

1) С новым ключевым словом ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Без нового ключевого слова ...

MyClass myClass;
myClass.MyField = "Hello world!";

С точки зрения реализации, они не кажутся такими уж разными (но я уверен, что они есть) ... Тем не менее, мой основной язык - C #, и, конечно, первый метод - то, к чему я привык.

Трудность заключается в том, что метод 1 сложнее использовать с классами std C ++.

Какой метод я должен использовать?

Обновление 1:

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

Обновление 2:

Мой друг недавно сказал мне, что есть простое правило для использования newключевого слова; каждый раз, когда вы печатаете new, печатайте delete.

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

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


6
Краткий ответ: используйте короткую версию, когда вам это сойдет с рук. :)
Jalf

11
Лучше, чем всегда писать соответствующее удаление - использовать контейнеры STL и умные указатели, такие как std::vectorи std::shared_ptr. Они обертывают звонки для вас newи deleteдля вас, поэтому у вас еще меньше шансов утечки памяти. Например, спросите себя: всегда ли вы помните, чтобы поместить соответствующий deleteсимвол везде, где может быть создано исключение? Вводить deletes вручную, сложнее, чем вы думаете.
AshleysBrain

@nbolton Re: ОБНОВЛЕНИЕ 1 - Одна из прекрасных особенностей C ++ состоит в том, что он позволяет хранить пользовательские типы в стеке, тогда как сборщики мусора, такие как C #, вынуждают хранить данные в куче . Хранение данных на кучу потребляет больше ресурсов , чем хранить данные в стеке , таким образом , вы должны предпочесть стек в кучу , кроме случаев , когда ваш UDT требует большого объема памяти для хранения своих данных. (Это также означает, что объекты передаются по значению по умолчанию). Лучшим решением вашей проблемы будет передача массива функции по ссылке .
Чарльз Аддис

Ответы:


304

Способ 1 (с использованием new)

  • Выделяет память для объекта в бесплатном хранилище (часто это то же самое, что и куча )
  • Требует от вас явно deleteвашего объекта позже. (Если вы не удалите его, вы можете создать утечку памяти)
  • Память остается выделенной до вас delete. (то есть вы можете returnиспользовать объект, который вы создали new)
  • Пример в вопросе будет пропускать память, если указатель не deleted; и он всегда должен быть удален , независимо от того, какой путь управления выбран, или если выброшены исключения.

Способ 2 (не используется new)

  • Выделяет память для объекта в стеке (куда идут все локальные переменные) Как правило, для стека доступно меньше памяти; если вы выделяете слишком много объектов, вы рискуете переполнить стек.
  • Вам это не понадобится deleteпозже.
  • Память больше не выделяется, когда она выходит из области видимости. (т.е. вы не должны returnуказатель на объект в стеке)

Насколько использовать? Вы выбираете метод, который лучше всего подходит для вас, учитывая вышеуказанные ограничения.

Несколько простых случаев:

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

4
Один нюанс - я считаю, что новый оператор выделяет память из «свободного хранилища», а malloc выделяет из «кучи». Это не обязательно одно и то же, хотя на практике это обычно так. Смотрите gotw.ca/gotw/009.htm .
Фред Ларсон

4
Я думаю, что ваш ответ может быть более понятным для использования. (99% времени выбор прост. Используйте метод 2 для объекта-оболочки, который вызывает new / delete в конструкторе / деструкторе)
jalf

4
@jalf: Метод 2 - это тот, который не использует новый: - / В любом случае, во многих случаях код будет намного проще (например, обработка ошибок), используя метод 2 (тот, который не содержит новый)
Даниэль ЛеЧеминант

Еще один придира ... Вы должны сделать более очевидным, что первый пример Ника утечка памяти, в то время как его второй нет, даже несмотря на исключения.
Арафангион

4
@Fred, Арафангион: Спасибо за ваше понимание; Я включил ваши комментарии в ответ.
Даниэль ЛеЧеминант

118

Между ними есть важное различие.

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

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

(И на этом любое сходство с C # прекращается)

Теперь все, что размещено в стеке, имеет «автоматическую» продолжительность хранения (вы можете объявить переменную как auto, но это значение по умолчанию, если другой тип хранилища не указан, поэтому ключевое слово на практике не используется, но именно здесь оно происходит от)

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

void foo() {
  bar b;
  bar* b2 = new bar();
}

Эта функция создает три значения, которые стоит рассмотреть:

В строке 1 объявляется переменная bтипа barв стеке (автоматическая продолжительность).

В строке 2 он объявляет barуказатель b2на стек (автоматическая длительность) и вызывает new, выделяя barобъект в куче. (динамическая продолжительность)

Когда функция вернется, произойдет следующее: во-первых, b2выходит из области видимости (порядок уничтожения всегда противоположен порядку построения). Но b2это всего лишь указатель, поэтому ничего не происходит, память, которую он занимает, просто освобождается. И что важно, память, на которую он указывает ( barэкземпляр в куче), НЕ затрагивается. Только указатель освобождается, потому что только указатель имел автоматическую продолжительность. Во-вторых, bвыходит из области видимости, поэтому, поскольку он имеет автоматическую продолжительность, вызывается его деструктор и освобождается память.

А barэкземпляр в куче? Это, вероятно, все еще там. Никто не удосужился удалить его, поэтому у нас просочилась память.

Из этого примера мы можем видеть, что любой объект с автоматической продолжительностью гарантированно будет вызывать свой деструктор, когда он выходит из области видимости. Это полезно Но все, что размещено в куче, длится столько времени, сколько нам нужно, и может иметь динамический размер, как в случае массивов. Это тоже полезно. Мы можем использовать это для управления распределением памяти. Что если класс Foo выделил некоторую память в куче в своем конструкторе и удалил эту память в своем деструкторе. Тогда мы могли бы получить лучшее из обоих миров - безопасное распределение памяти, которое гарантированно будет освобождено снова, но без ограничений, заставляющих все быть в стеке.

И это именно то, как работает большинство кода C ++. Посмотрите на стандартную библиотеку, std::vectorнапример. Это обычно выделяется в стеке, но может иметь динамический размер и размер. И делает это путем внутреннего выделения памяти в куче по мере необходимости. Пользователь этого класса никогда не увидит этого, поэтому нет никаких шансов утечки памяти или забывания очистить то, что вы выделили.

Этот принцип называется RAII (Resource Acquisition is Initialization), и его можно распространить на любой ресурс, который необходимо получить и освободить. (сетевые сокеты, файлы, соединения с базой данных, блокировки синхронизации). Все они могут быть получены в конструкторе и выпущены в деструкторе, так что вы гарантированно, что все ресурсы, которые вы приобретаете, будут освобождены снова.

Как правило, никогда не используйте new / delete непосредственно из кода высокого уровня. Всегда оборачивайте его в класс, который может управлять памятью для вас и который обеспечит ее освобождение снова. (Да, могут быть исключения из этого правила. В частности, умные указатели требуют, чтобы вы вызывали newнапрямую и передавали указатель на его конструктор, который затем вступает во владение и обеспечивает deleteправильный вызов. Но это все еще очень важное практическое правило )


2
«Все, что не выделено с помощью new, помещается в стек». Не в тех системах, над которыми я работал ... обычно инициализированные (и неинитизированные) глобальные (статические) данные размещаются в своих собственных сегментах. Например, .data, .bss и т. Д ... сегменты линкера. Pedantic, я знаю ...
Дан

Конечно, ты прав. Я действительно не думал о статических данных. Мой плохой, конечно. :)
Jalf

2
Почему все, что размещено в стеке, должно иметь постоянный размер?
user541686

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

14

Какой метод я должен использовать?

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

Чтобы облегчить бремя использования управления свободными магазинами, люди изобрели такие вещи, как auto_ptrи unique_ptr. Я настоятельно рекомендую вам взглянуть на это. Они могут даже помочь в ваших проблемах с печатанием ;-)


10

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

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

Кроме того, старайтесь избегать использования удаления. Вместо этого оберните ваш новый в умный указатель. Позвольте умному указателю вызвать удаление для вас.

В некоторых случаях умный указатель не является умным. Никогда не храните std :: auto_ptr <> внутри контейнера STL. Он удалит указатель слишком рано из-за операций копирования внутри контейнера. Другой случай, когда у вас есть действительно большой контейнер указателей STL на объекты. У boost :: shared_ptr <> будет тонна накладных расходов на скорость, поскольку она увеличивает счетчик ссылок вверх и вниз. В этом случае лучше всего поместить контейнер STL в другой объект и дать этому объекту деструктор, который будет вызывать delete для каждого указателя в контейнере.


10

Краткий ответ: если вы новичок в C ++, вы никогда не должны использовать newили deleteсебя.

Вместо этого вы должны использовать умные указатели, такие как std::unique_ptrи std::make_unique(или реже std::shared_ptrи std::make_shared). Таким образом, вам не придется беспокоиться о утечках памяти. И даже если вы более продвинутый, лучшая практика, как правило , будет инкапсулировать пользовательский путь , который вы используете newи deleteв небольшой класс (например, пользовательские смарт - указатель) , который посвящен только вопросам жизненного цикла объектов.

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


Интересно посмотреть, как ответ может измениться с течением времени;)
Wolf


2

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


1

Если ваша переменная используется только в контексте одной функции, вам лучше использовать переменную стека, т. Е. Вариант 2. Как уже говорили другие, вам не нужно управлять временем жизни переменных стека - они создаются и уничтожается автоматически. Кроме того, распределение / освобождение переменной в куче является медленным сравнением. Если ваша функция вызывается достаточно часто, вы увидите значительное улучшение производительности, если использовать переменные стека по сравнению с переменными кучи.

Тем не менее, есть несколько очевидных случаев, когда переменные стека недостаточны.

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

Другое, более вероятное условие - ваша переменная должна «жить» за пределами вашей функции. В этом случае вы должны разместить переменную в куче так, чтобы она могла быть достигнута за пределами любой заданной функции.


1

Вы передаете myClass из функции или ожидаете, что он будет существовать вне этой функции? Как говорили некоторые другие, все дело в области видимости, когда вы не выделяете кучу. Когда вы покидаете функцию, она исчезает (в конце концов). Одна из классических ошибок новичков - попытка создать локальный объект некоторого класса в функции и вернуть его, не размещая его в куче. Я помню, как отлаживал подобные вещи в мои ранние дни, занимаясь с ++.


0

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

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

Первый метод также требует, чтобы deleteвы создавали то, что вы создаете new, тогда как во втором методе класс автоматически уничтожается и освобождается, когда он выходит из области видимости (обычно следующая закрывающая скобка).


-1

Короткий ответ: да, ключевое слово «new» невероятно важно, так как при его использовании данные объекта сохраняются в куче, а не в стеке, что наиболее важно!

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