Tworzenie instancji obiektu w C ++

113

Jestem programistą C, próbującym zrozumieć C ++. Wiele samouczków demonstruje tworzenie instancji obiektu za pomocą fragmentu kodu, takiego jak:

Dog* sparky = new Dog();

co oznacza, że ​​później zrobisz:

delete sparky;

co ma sens. Teraz, w przypadku, gdy dynamiczna alokacja pamięci nie jest konieczna, czy istnieje jakikolwiek powód, aby użyć powyższego zamiast

Dog sparky;

i pozwolić wywołać destruktor, gdy sparky wyjdzie poza zasięg?

Dzięki!

el_champo
źródło

Odpowiedzi:

166

Wręcz przeciwnie, zawsze powinieneś preferować alokacje stosu, do tego stopnia, że ​​z reguły nigdy nie powinieneś mieć nowego / usuwania w kodzie użytkownika.

Jak powiedziałeś, kiedy zmienna jest zadeklarowana na stosie, jej destruktor jest wywoływany automatycznie, gdy wyjdzie poza zakres, co jest głównym narzędziem do śledzenia czasu życia zasobów i unikania wycieków.

Ogólnie rzecz biorąc, za każdym razem, gdy musisz przydzielić zasób, niezależnie od tego, czy jest to pamięć (przez wywołanie new), uchwyty plików, gniazda czy cokolwiek innego, opakuj go w klasę, w której konstruktor pozyskuje zasób, a destruktor zwalnia go. Następnie możesz utworzyć obiekt tego typu na stosie i masz gwarancję, że Twój zasób zostanie zwolniony, gdy wyjdzie poza zakres. W ten sposób nie musisz wszędzie śledzić nowych / usuwanych par, aby uniknąć wycieków pamięci.

Najpopularniejszą nazwą tego idiomu jest RAII

Przyjrzyj się także klasom inteligentnych wskaźników, które są używane do zawijania wynikowych wskaźników w rzadkich przypadkach, gdy musisz przydzielić coś nowym poza dedykowanym obiektem RAII. Zamiast tego przekazujesz wskaźnik do inteligentnego wskaźnika, który następnie śledzi jego czas życia, na przykład przez zliczanie odwołań, i wywołuje destruktor, gdy ostatnie odwołanie wykracza poza zakres. Biblioteka standardowa std::unique_ptrumożliwia proste zarządzanie w oparciu o zakres i std::shared_ptrzliczanie odniesień w celu wdrożenia współwłasności.

Wiele samouczków demonstruje tworzenie instancji obiektu za pomocą fragmentu kodu, takiego jak ...

Odkryłeś więc, że większość samouczków jest do niczego. ;) Większość samouczków uczy cię kiepskich praktyk C ++, w tym wywoływania new / delete w celu tworzenia zmiennych, gdy nie jest to konieczne, i utrudnia śledzenie czasu życia przydziałów.

jalf
źródło
Surowe wskaźniki są przydatne, gdy potrzebujesz semantyki podobnej do auto_ptr (przenoszenie własności), ale zachowują operację wymiany bez rzucania i nie chcą narzutu zliczania odwołań. Może sprawa krawędziowa, ale przydatna.
Greg Rogers
2
To jest prawidłowa odpowiedź, ale powodem, dla którego nigdy nie nabrałbym nawyku tworzenia obiektów na stosie, jest to, że nigdy nie jest do końca oczywiste, jak duży będzie ten obiekt. Po prostu prosisz o wyjątek przepełnienia stosu.
dviljoen
3
Greg: Oczywiście. Ale jak mówisz, przypadek skrajny. Ogólnie rzecz biorąc, najlepiej unikać wskazówek. Ale są w języku z jakiegoś powodu, nie można temu zaprzeczyć. :) dviljoen: Jeśli obiekt jest duży, zawijasz go w obiekt RAII, który może być przydzielony na stosie i zawiera wskaźnik do danych przydzielonych na stosie.
jalf
3
@dviljoen: Nie, nie jestem. Kompilatory C ++ nie powiększają niepotrzebnie obiektów. Najgorsze, jakie zobaczysz, to zazwyczaj zaokrąglanie w górę do najbliższej wielokrotności czterech bajtów. Zwykle klasa zawierająca wskaźnik zajmuje tyle samo miejsca, co sam wskaźnik, więc użycie stosu nic Cię nie kosztuje.
jalf
20

Chociaż posiadanie rzeczy na stosie może być zaletą pod względem alokacji i automatycznego zwalniania, ma pewne wady.

  1. Możesz nie chcieć przydzielać dużych obiektów na stosie.

  2. Dynamiczna wysyłka! Rozważ ten kod:

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

Spowoduje to wydrukowanie „B”. Zobaczmy teraz, co się dzieje podczas używania stosu:

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

Spowoduje to wydrukowanie „A”, co może nie być intuicyjne dla osób znających Javę lub inne języki obiektowe. Powodem jest to, że nie masz już wskaźnika do instancji B. Zamiast tego Btworzona jest instancja programu i kopiowana do azmiennej typu A.

Niektóre rzeczy mogą się wydarzyć nieintuicyjnie, zwłaszcza gdy jesteś nowicjuszem w C ++. W C masz swoje wskaźniki i to wszystko. Wiesz, jak ich używać i ZAWSZE robią to samo. W C ++ tak nie jest. Wystarczy wyobrazić sobie, co się dzieje, gdy używasz w tym przykładzie jako argument do metody - robi się bardziej skomplikowane i to robi ogromną różnicę, jeżeli ajest typu Alub A*nawet A&(call-by-reference). Możliwych jest wiele kombinacji i wszystkie zachowują się inaczej.

Wszechświat
źródło
2
-1: ludzie, którzy nie są w stanie zrozumieć semantyki wartości i prosty fakt, że polimorfizm wymaga odniesienia / wskaźnika (aby uniknąć wycinania obiektów), nie stanowią żadnego problemu z językiem. Moc c ++ nie powinna być traktowana jako wada tylko dlatego, że niektórzy ludzie nie mogą nauczyć się jego podstawowych zasad.
underscore_d
Dzięki za ostrzeżenie. Miałem podobny problem z metodą pobierającą obiekt zamiast wskaźnika lub odniesienia i jaki to był bałagan. Obiekt ma wewnątrz wskaźniki i zostały one usunięte zbyt szybko z powodu tego radzenia sobie.
BoBoDev
@underscore_d Zgadzam się; ostatni akapit tej odpowiedzi powinien zostać usunięty. Nie myśl, że zredagowałem tę odpowiedź w taki sposób, że się z nią zgadzam; Po prostu nie chciałem, aby odczytywano zawarte w nim błędy.
TamaMcGlinn
@TamaMcGlinn zgodził się. Dzięki za dobrą edycję. Usunąłem część opinii.
UniversE
13

Cóż, powód użycia wskaźnika byłby dokładnie taki sam, jak powód użycia wskaźników w C przydzielonych za pomocą malloc: jeśli chcesz, aby twój obiekt żył dłużej niż zmienna!

Jest nawet wysoce zalecane, aby NIE używać nowego operatora, jeśli możesz tego uniknąć. Zwłaszcza jeśli używasz wyjątków. Generalnie znacznie bezpieczniej jest pozwolić kompilatorowi zwolnić obiekty.

PierreBdR
źródło
13

Widziałem ten anty-wzorzec od ludzi, którzy nie do końca rozumieją operator & address-of. Jeśli potrzebują wywołać funkcję ze wskaźnikiem, zawsze alokują na stercie, aby uzyskać wskaźnik.

void FeedTheDog(Dog* hungryDog);

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

Dog goodDog;
FeedTheDog(&goodDog);
Mark Okup
źródło
Alternatywnie: void FeedTheDog (Dog & hungryDog); Pies pies; FeedTheDog (pies);
Scott Langham,
1
@ScottLangham prawda, ale nie o to mi chodziło. Czasami wywołujesz funkcję, która pobiera na przykład opcjonalny wskaźnik NULL, a może jest to istniejący interfejs API, którego nie można zmienić.
Mark Ransom
7

Traktuj stertę jako bardzo ważną nieruchomość i używaj jej bardzo rozsądnie. Podstawową zasadą kciuka jest używanie stosu, gdy tylko jest to możliwe, i sterty, gdy nie ma innego wyjścia. Alokując obiekty na stosie, możesz uzyskać wiele korzyści, takich jak:

(1). Nie musisz martwić się o rozwijanie stosu w przypadku wyjątków

(2). Nie musisz martwić się fragmentacją pamięci spowodowaną przydzieleniem większej ilości miejsca niż jest to konieczne przez menedżera sterty.

Naveen
źródło
1
Należy wziąć pod uwagę rozmiar obiektu. Stos ma ograniczony rozmiar, więc bardzo duże obiekty powinny być przydzielane do Sterty.
Adam
1
@Adam Myślę, że Naveen nadal ma rację. Jeśli możesz położyć duży przedmiot na stosie, zrób to, bo tak jest lepiej. Jest wiele powodów, dla których prawdopodobnie nie możesz. Ale myślę, że ma rację.
McKay
5

Jedynym powodem, dla którego bym się martwił, jest to, że Pies jest teraz przydzielony na stosie, a nie na stosie. Jeśli więc pies ma rozmiar megabajtów, możesz mieć problem,

Jeśli musisz przejść nową / usunąć trasę, uważaj na wyjątki. Z tego powodu powinieneś używać auto_ptr lub jednego z typów inteligentnych wskaźników doładowania, aby zarządzać czasem życia obiektu.

Roddy
źródło
1

Nie ma powodu do nowego (na stercie), kiedy możesz alokować na stosie (chyba że z jakiegoś powodu masz mały stos i chcesz go użyć.

Możesz rozważyć użycie shared_ptr (lub jednego z jego wariantów) z biblioteki standardowej, jeśli chcesz alokować na stercie. To zajmie się usunięciem za Ciebie, gdy wszystkie odniesienia do shared_ptr znikną.

Scott Langham
źródło
Jest wiele powodów, na przykład zobacz odpowiedź.
dangerousdave
Przypuszczam, że możesz chcieć, aby obiekt przeżył zakres funkcji, ale OP nie wydawał się sugerować, że to właśnie próbowali zrobić.
Scott Langham,
0

Istnieje dodatkowy powód, o którym nikt inny nie wspomniał, dla którego możesz zdecydować się na dynamiczne tworzenie obiektu. Dynamiczne obiekty oparte na stercie pozwalają na wykorzystanie polimorfizmu .

dangerousdave
źródło
2
Możesz również pracować z obiektami opartymi na stosie w sposób polimorficzny, nie widzę żadnej różnicy między obiektami przydzielonymi stosem / i stertą pod tym względem. Np .: void MyFunc () {Pies; Nakarm psa); } void Feed (Animal & Animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (żywność); } // W tym przykładzie GetFavouriteFood może być funkcją wirtualną, którą każde zwierzę zastępuje swoją własną implementacją. Będzie to działać polimorficznie bez stosu.
Scott Langham,
-1: polimorfizm wymaga tylko odniesienia lub wskaźnika; jest całkowicie niezależny od czasu przechowywania podstawowej instancji.
underscore_d
@underscore_d "Używanie uniwersalnej klasy bazowej implikuje koszt: obiekty muszą być przydzielane do sterty, aby były polimorficzne;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave
LOL, nie obchodzi mnie, czy sam Stroustrup to powiedział, ale to dla niego niesamowicie żenujący cytat, ponieważ się myli. Nie jest trudno przetestować to samemu, wiesz: utwórz na stosie instancje DerivedA, DerivedB i DerivedC; Utwórz również instancję wskaźnika przydzielonego na stosie do Base i potwierdź, że możesz umieścić go w A / B / C i używać ich polimorficznie. Zastosuj krytyczną myśl i standardowe odniesienia do twierdzeń, nawet jeśli są one autorstwa oryginalnego autora języka. Tutaj: stackoverflow.com/q/5894775/2757035
underscore_d
Ujmując to w ten sposób, mam obiekt zawierający 2 oddzielne rodziny po prawie 1000 obiektów polimorficznych z automatycznym czasem przechowywania. Tworzę instancję tego obiektu na stosie i odwołując się do tych elementów członkowskich za pomocą odniesień lub wskaźników, uzyskuje on pełne możliwości polimorfizmu względem nich. Nic w moim programie nie jest przydzielane dynamicznie / na stertę (poza zasobami należącymi do klas z przydzielonymi stosami) i nie zmienia to ani trochę możliwości mojego obiektu.
underscore_d
-4

Miałem ten sam problem w Visual Studio. Musisz użyć:

yourClass-> classMethod ();

zamiast:

yourClass.classMethod ();

Hamed
źródło
3
To nie odpowiada na pytanie. Należy raczej zauważyć, że aby uzyskać dostęp do obiektu przydzielonego na stercie (przez wskaźnik do obiektu) i obiektu przydzielonego na stosie, należy użyć innej składni.
Alexey Ivanov