Создание экземпляра объекта C ++

113

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

Dog* sparky = new Dog();

что означает, что позже вы сделаете:

delete sparky;

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

Dog sparky;

и позвольте деструктору вызываться, когда Sparky выходит за пределы области видимости?

Спасибо!

el_champo
источник

Ответы:

166

Напротив, вы всегда должны отдавать предпочтение распределению стека, поскольку, как показывает практическое правило, вы никогда не должны иметь new / delete в вашем пользовательском коде.

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

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

Наиболее распространенное название этой идиомы - RAII.

Также изучите классы интеллектуальных указателей, которые используются для обертывания результирующих указателей в редких случаях, когда вам действительно нужно выделить что-то с новым за пределами выделенного объекта RAII. Вместо этого вы передаете указатель на интеллектуальный указатель, который затем отслеживает его время жизни, например, путем подсчета ссылок, и вызывает деструктор, когда последняя ссылка выходит за пределы области видимости. Стандартная библиотека предназначена std::unique_ptrдля простого управления на основе области действия и std::shared_ptrподсчета ссылок для реализации совместного владения.

Многие учебные пособия демонстрируют создание экземпляров объекта с помощью таких фрагментов, как ...

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

Jalf
источник
Необработанные указатели полезны, когда вам нужна семантика, подобная auto_ptr (передача владения), но при этом сохраняется операция замены, не вызывающая выброса, и не требуются накладные расходы на подсчет ссылок. Крайний случай, возможно, но полезный
Грег Роджерс,
2
Это правильный ответ, но причина, по которой я бы никогда не привык создавать объекты в стеке, заключается в том, что никогда не бывает совершенно очевидно, насколько большим будет этот объект. Вы просто запрашиваете исключение переполнения стека.
dviljoen 02
3
Грег: Конечно. Но, как вы говорите, крайний случай. В общем, указателей лучше избегать. Но они на языке не зря, этого нельзя отрицать. :) dviljoen: Если объект большой, вы оборачиваете его в объект RAII, который может быть размещен в стеке и содержит указатель на данные, размещенные в куче.
jalf 02
3
@dviljoen: Нет. Компиляторы C ++ не раздувают объекты без надобности. Худшее, что вы увидите, это то, что он обычно округляется до ближайшего кратного четырех байтов. Обычно класс, содержащий указатель, занимает столько же места, сколько и сам указатель, поэтому использование стека ничего не стоит.
jalf
20

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

  1. Возможно, вы не захотите размещать огромные объекты в стеке.

  2. Динамическая рассылка! Рассмотрим этот код:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Это напечатает "B". Теперь посмотрим, что происходит при использовании Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Будет выведено «A», что может быть не интуитивно понятно тем, кто знаком с Java или другими объектно-ориентированными языками. Причина в том, что у вас больше нет указателя на экземпляр B. Вместо этого создается экземпляр, Bкоторый копируется в aпеременную типа A.

Некоторые вещи могут происходить неожиданно, особенно если вы новичок в C ++. В C у вас есть указатели, и все. Вы знаете, как их использовать, и они ВСЕГДА делают то же самое. В C ++ это не так. Только представьте, что происходит, когда вы используете в этом примере в качестве аргумента для метода - все становится сложнее, и он ДЕЙСТВИТЕЛЬНО имеет огромное значение, если aон типа Aили A*или даже A&(вызов по ссылке). Возможны многие комбинации, и все они ведут себя по-разному.

вселенная
источник
2
-1: люди, не способные понять семантику значений, и тот простой факт, что полиморфизму нужна ссылка / указатель (чтобы избежать нарезки объектов), не представляют собой каких-либо проблем с языком. Возможности c ++ не следует рассматривать как недостаток только потому, что некоторые люди не могут изучить его основные правила.
underscore_d
Спасибо за внимание. У меня была аналогичная проблема с методом, принимающим объект вместо указателя или ссылки, и что это был за беспорядок. У объекта есть указатели внутри, и они были удалены слишком рано из-за этого копирования.
BoBoDev 03
@underscore_d Я согласен; последний абзац этого ответа следует удалить. Не воспринимайте тот факт, что я редактировал этот ответ, как то, что я согласен с ним; Я просто не хотел, чтобы в нем были прочитаны ошибки.
TamaMcGlinn 01
@TamaMcGlinn согласился. Спасибо за хорошее редактирование. Я удалил часть мнения.
UniversE 01
13

Что ж, причина для использования указателя будет точно такой же, как и причина использования указателей в C, выделенных с помощью malloc: если вы хотите, чтобы ваш объект жил дольше, чем ваша переменная!

Даже настоятельно рекомендуется НЕ использовать оператор new, если вы можете этого избежать. Особенно, если вы используете исключения. В общем, гораздо безопаснее позволить компилятору освобождать ваши объекты.

PierreBdR
источник
13

Я видел этот антипаттерн от людей, которые не совсем понимают оператор & address-of. Если им нужно вызвать функцию с указателем, они всегда будут выделять память в куче, чтобы получить указатель.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);
Марк Рэнсом
источник
В качестве альтернативы: void FeedTheDog (Dog & hungryDog); Собака Собака; FeedTheDog (собака);
Скотт Лэнгем
1
@ScottLangham, правда, но я не об этом пытался сказать. Иногда вы вызываете функцию, которая принимает, например, необязательный NULL-указатель, или, может быть, это существующий API, который нельзя изменить.
Марк Рэнсом,
7

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

(1). Вам не нужно беспокоиться о раскручивании стека в случае возникновения исключений.

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

Нэвин
источник
1
Следует учитывать размер объекта. Размер стека ограничен, поэтому очень большие объекты должны выделяться в куче.
Адам
1
@ Адам Я думаю, что Навин здесь прав. Если вы можете положить в стек большой объект, то сделайте это, потому что так лучше. Есть много причин, по которым вы, вероятно, не можете. Но я думаю, он прав.
McKay
5

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

Если вам все-таки нужно пойти по новому / удаленному маршруту, будьте осторожны с исключениями. И из-за этого вы должны использовать auto_ptr или один из типов интеллектуальных указателей boost для управления временем жизни объекта.

Родди
источник
1

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

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

Скотт Лэнгэм
источник
Есть много причин, например, см. Мой ответ.
dangerousdave
Я полагаю, вы могли бы хотеть, чтобы объект пережил область действия функции, но OP, похоже, не предполагал, что они пытались сделать.
Скотт Лэнгэм
0

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

опасный дэйв
источник
2
Вы также можете работать с объектами на основе стека полиморфно, я не вижу никакой разницы между объектами, выделенными стеком / и кучей, в этом отношении. Например: void MyFunc () {Dog dog; Кормить собаку); } void Feed (Животные и животные) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (еда); } // В этом примере GetFavouriteFood может быть виртуальной функцией, которую каждое животное переопределяет своей собственной реализацией. Это будет работать полиморфно без использования кучи.
Скотт Лэнгем
-1: полиморфизм требует только ссылки или указателя; он полностью не зависит от продолжительности хранения базового экземпляра.
underscore_d
@underscore_d "Использование универсального базового класса требует затрат: объекты должны быть выделены в куче, чтобы быть полиморфными;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave
LOL, меня не волнует, сказал ли это сам Страуструп, но для него это невероятно смущающая цитата, потому что он ошибается. Вы знаете, нетрудно проверить это самостоятельно: создайте экземпляры DerivedA, DerivedB и DerivedC в стеке; создайте также выделенный стеком указатель на Base и подтвердите, что вы можете разместить его с помощью A / B / C и использовать их полиморфно. Применяйте критическое мышление и стандартные ссылки к утверждениям, даже если они исходят от автора языка. Здесь: stackoverflow.com/q/5894775/2757035
underscore_d
Скажем так, у меня есть объект, содержащий 2 отдельных семейства почти по 1000 полиморфных объектов в каждом с автоматической продолжительностью хранения. Я создаю этот объект в стеке, и, обращаясь к этим членам через ссылки или указатели, он достигает всех возможностей полиморфизма над ними. Ничто в моей программе не выделяется динамически / в куче (кроме ресурсов, принадлежащих классам, выделенным стеком), и это не меняет возможности моего объекта ни на йоту.
underscore_d
-4

У меня была такая же проблема в Visual Studio. Вы должны использовать:

yourClass-> classMethod ();

скорее, чем:

yourClass.classMethod ();

Хамед
источник
3
Это не отвечает на вопрос. Вы скорее замечаете, что для доступа к объекту, размещенному в куче (через указатель на объект) и объекту, размещенному в стеке, необходимо использовать другой синтаксис.
Алексей Иванов