Kiedy powinienem używać nowego słowa kluczowego w C ++?


272

Używam C ++ przez krótki czas i zastanawiam się nad nowym słowem kluczowym. Po prostu powinienem go używać, czy nie?

1) Z nowym słowem kluczowym ...

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

2) Bez nowego słowa kluczowego ...

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

Z punktu widzenia implementacji nie wydają się tak różne (ale jestem pewien, że tak) ... Jednak moim podstawowym językiem jest C # i oczywiście pierwsza metoda jest tym, do czego jestem przyzwyczajony.

Wydaje się, że trudność polega na tym, że metoda 1 jest trudniejsza w użyciu z klasami std C ++.

Jakiej metody powinienem użyć?

Aktualizacja 1:

Ostatnio użyłem nowego słowa kluczowego dla pamięci sterty (lub darmowego magazynu ) dla dużej tablicy, która wychodziła poza zakres (tj. Była zwracana z funkcji). Tam, gdzie wcześniej korzystałem ze stosu, co spowodowało uszkodzenie połowy elementów poza zakresem, przełączenie na użycie sterty zapewniło, że elementy są taktowane. Tak!

Aktualizacja 2:

Mój przyjaciel niedawno powiedział mi, że istnieje prosta zasada używania newsłowa kluczowego; za każdym razem, gdy piszesz new, pisz delete.

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

Pomaga to uniknąć wycieków pamięci, ponieważ zawsze musisz gdzieś usunąć miejsce (tj. Kiedy wycinasz i wklejasz je do destruktora lub w inny sposób).


6
Krótka odpowiedź brzmi: skorzystaj z krótkiej wersji, kiedy będziesz w stanie uciec. :)
czerwiec

11
Lepsza technika niż zawsze pisanie odpowiedniego usuwania - użyj kontenerów STL i inteligentnych wskaźników, takich jak std::vectori std::shared_ptr. Zawierają one połączenia do ciebie newi deletedla ciebie, więc jeszcze mniej prawdopodobne jest wyciek pamięci. Zadaj sobie na przykład: czy zawsze pamiętasz o umieszczeniu odpowiedniego deletewszędzie tam, gdzie mógłby zostać zgłoszony wyjątek? deleteRęczne wkładanie ws jest trudniejsze niż mogłoby się wydawać.
AshleysBrain

@nbolton Re: UPDATE 1 - Jedną z najpiękniejszych rzeczy w C ++ jest to, że pozwala on przechowywać typy zdefiniowane przez użytkownika na stosie, podczas gdy śmieci zbierające języki, takie jak C #, zmuszają cię do przechowywania danych na stosie . Przechowywanie danych na temat sterty zużywa więcej zasobów niż przechowywanie danych na stosie , a zatem należy preferować stos na stercie , z wyjątkiem gdy UDT wymaga dużej ilości pamięci do przechowywania danych. (Oznacza to również, że obiekty są domyślnie przekazywane wartościowo). Lepszym rozwiązaniem problemu byłoby przekazanie tablicy do funkcji przez odwołanie .
Charles Addis,

Odpowiedzi:


303

Metoda 1 (przy użyciu new)

  • Przydziela pamięć dla obiektu w wolnym magazynie (często jest to to samo, co sterty )
  • Wymaga deletepóźniejszego jawnego określenia obiektu. (Jeśli go nie usuniesz, możesz spowodować wyciek pamięci)
  • Pamięć zostaje przydzielona do momentu deletejej przydzielenia . (tzn. możesz returnstworzyć obiekt za pomocą new)
  • Przykład w pytaniu spowoduje wyciek pamięci, chyba że wskaźnikiem jest deleted; i zawsze powinien zostać usunięty , niezależnie od tego, którą ścieżkę kontroli wybrano lub jeśli zgłoszono wyjątki.

Metoda 2 (nieużywanie new)

  • Przydziela pamięć dla obiektu na stosie (gdzie idą wszystkie zmienne lokalne) Na stosie jest ogólnie mniej pamięci; jeśli przydzielisz zbyt wiele obiektów, ryzykujesz przepełnienie stosu.
  • Nie będziesz tego potrzebować deletepóźniej.
  • Pamięć nie jest już przydzielana, gdy wykracza poza zakres. (tzn. nie powinieneś returnwskazywać na obiekt na stosie)

O ile użyć; wybierasz metodę, która najlepiej Ci odpowiada, biorąc pod uwagę powyższe ograniczenia.

Kilka łatwych przypadków:

  • Jeśli nie chcesz się martwić połączeniami delete(i potencjalnym powodem wycieków pamięci ), nie powinieneś używać new.
  • Jeśli chcesz zwrócić wskaźnik do obiektu z funkcji, musisz użyć new

4
Jeden nitpick - wierzę, że nowy operator przydziela pamięć z „darmowego sklepu”, podczas gdy malloc przydziela z „sterty”. Nie są gwarantowane, że są tym samym, chociaż w praktyce zwykle są. Zobacz gotw.ca/gotw/009.htm .
Fred Larson

4
Myślę, że twoja odpowiedź może być bardziej zrozumiała, z której korzystać. (99% czasu wybór jest prosty. Użyj metody 2 na obiekcie otoki, który wywołuje new / delete w konstruktorze / destruktorze)
jalf

4
@jalf: Metoda 2 to ta, która nie korzysta z nowego: - / W każdym razie wiele razy kodowanie będzie znacznie prostsze (np. obsługa przypadków błędów) przy użyciu Metody 2 (ta bez nowej)
Daniel LeCheminant,

Kolejny chwyt ... Powinieneś uczynić bardziej oczywistym, że pierwszy przykład Nicka przecieka pamięć, podczas gdy jego drugi nie, nawet w obliczu wyjątków.
Arafangion

4
@Fred, Arafangion: Dzięki za wgląd; Włączyłem twoje komentarze do odpowiedzi.
Daniel LeCheminant

118

Istnieje ważna różnica między nimi.

Wszystko, co nie jest przydzielone, newzachowuje się podobnie do typów wartości w języku C # (a ludzie często mówią, że te obiekty są przydzielane na stosie, co jest prawdopodobnie najczęstszym / oczywistym przypadkiem, ale nie zawsze jest prawdziwe. Dokładniej, obiekty przydzielone bez użycia newmają automatyczne przechowywanie czas trwania Wszystko przydzielone za pomocą newjest przydzielane na stercie i zwracany jest do niego wskaźnik, dokładnie tak jak typy referencyjne w języku C #.

Wszystko przydzielone na stosie musi mieć stały rozmiar, określony w czasie kompilacji (kompilator musi poprawnie ustawić wskaźnik stosu, lub jeśli obiekt należy do innej klasy, musi dostosować rozmiar tej innej klasy) . Dlatego tablice w języku C # są typami referencyjnymi. Muszą być, ponieważ w przypadku typów referencyjnych możemy w czasie wykonywania decydować o wymaganej ilości pamięci. To samo dotyczy tutaj. Tylko tablice o stałym rozmiarze (rozmiar, który można określić w czasie kompilacji) mogą być przydzielane z automatycznym czasem przechowywania (na stosie). Tablice o dynamicznych rozmiarach muszą być przydzielane na stercie przez wywołanie new.

(I tu kończy się wszelkie podobieństwo do C #)

Teraz wszystko, co przydzielono na stosie, ma „automatyczny” czas przechowywania (można faktycznie zadeklarować zmienną jako auto, ale jest to ustawienie domyślne, jeśli nie określono innego typu pamięci, więc słowo kluczowe nie jest tak naprawdę używane w praktyce, ale właśnie tam pochodzi z)

Automatyczny czas przechowywania oznacza dokładnie, jak to brzmi, czas trwania zmiennej jest obsługiwany automatycznie. Natomiast wszystko przydzielone na stercie musi zostać ręcznie usunięte przez Ciebie. Oto przykład:

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

Ta funkcja tworzy trzy wartości, które warto rozważyć:

W wierszu 1 deklaruje zmienną btypu barna stosie (automatyczny czas trwania).

W linii 2 deklaruje barwskaźnik b2na stosie (automatyczny czas trwania) i wywołuje new, przydzielając barobiekt na stercie. (dynamiczny czas trwania)

Kiedy funkcja powróci, wydarzy się: Po pierwsze, b2wykracza poza zakres (kolejność niszczenia jest zawsze przeciwna do kolejności konstrukcji). Ale b2to tylko wskazówka, więc nic się nie dzieje, pamięć, którą zajmuje, jest po prostu uwolniona. I co ważne, pamięć, na którą wskazuje ( barinstancja na stercie) NIE jest dotykana. Uwolniony jest tylko wskaźnik, ponieważ tylko wskaźnik miał automatyczny czas trwania. Po drugie, bwychodzi poza zakres, więc ponieważ ma on automatyczny czas trwania, wywoływany jest jego destruktor, a pamięć jest zwalniana.

A barinstancja na stercie? Prawdopodobnie wciąż tam jest. Nikt nie zadał sobie trudu, aby go usunąć, więc wyciekła pamięć.

Z tego przykładu możemy zobaczyć, że wszystko, co ma automatyczny czas trwania, ma gwarancję, że zostanie wywołany jego destruktor, gdy wykroczy poza zakres. To się przydaje. Ale wszystko, co jest przydzielane na stosie, trwa tak długo, jak jest to potrzebne, i może być dynamicznie zmieniane, jak w przypadku tablic. To też jest przydatne. Możemy to wykorzystać do zarządzania przydziałami pamięci. Co jeśli klasa Foo przydzieli część pamięci na stercie w swoim konstruktorze i usunie tę pamięć w swoim destruktorze. Wtedy moglibyśmy uzyskać to, co najlepsze z obu światów, bezpieczne przydziały pamięci, które z pewnością zostaną uwolnione, ale bez ograniczeń zmuszania wszystkiego do umieszczenia na stosie.

Dokładnie tak działa większość kodu C ++. Spójrz na przykład na bibliotekę standardową std::vector. Jest to zwykle przydzielane na stosie, ale można je dynamicznie zmieniać i zmieniać rozmiar. I robi to poprzez wewnętrzne przydzielanie pamięci na stercie, jeśli to konieczne. Użytkownik klasy nigdy tego nie widzi, więc nie ma szansy na wyciek pamięci lub zapomnienie o wyczyszczeniu przydzielonych zasobów.

Zasada ta nosi nazwę RAII (Resource Acquisition is Initialization) i można ją rozszerzyć na dowolny zasób, który należy nabyć i zwolnić. (gniazda sieciowe, pliki, połączenia z bazą danych, blokady synchronizacji). Wszystkie z nich można zdobyć w konstruktorze i uwolnić w destruktorze, więc masz gwarancję, że wszystkie zdobyte zasoby zostaną ponownie uwolnione.

Zasadniczo nigdy nie używaj new / delete bezpośrednio z kodu wysokiego poziomu. Zawsze zawiń go w klasę, która może zarządzać pamięcią za Ciebie i która zapewni, że zostanie ponownie uwolniona. (Tak, mogą istnieć wyjątki od tej reguły. W szczególności inteligentne wskaźniki wymagają newbezpośredniego wywołania i przekazania wskaźnika do jego konstruktora, który następnie przejmuje kontrolę i zapewnia deleteprawidłowe wywołanie. Ale to nadal bardzo ważna zasada )


2
„Wszystko, co nie jest przypisane do nowego, jest umieszczane na stosie”. Nie w systemach, nad którymi pracowałem ... zwykle zinicjalizowane (i niezainicjowane) dane globalne (statyczne) są umieszczane we własnych segmentach. Na przykład .data, .bss itp. Segmenty linkera. Pedantyczny, wiem ...
Dan

Oczywiście masz rację. Tak naprawdę nie myślałem o danych statycznych. Oczywiście mój zły. :)
czerwiec

2
Dlaczego cokolwiek przydzielonego na stosie musi mieć stały rozmiar?
user541686,

Nie zawsze jest kilka sposobów na obejście tego, ale w ogólnym przypadku tak, ponieważ jest na stosie. Jeśli znajduje się na górze stosu, może być możliwa zmiana jego rozmiaru, ale gdy coś innego zostanie na niego wciśnięte, zostanie „zamurowane”, otoczone obiektami po obu stronach, więc nie można tak naprawdę zmienić jego rozmiaru . Tak, powiedzenie, że zawsze musi mieć ustalony rozmiar, jest trochę uproszczeniem, ale przekazuje podstawową ideę (i nie
zalecałbym bałagania się

14

Jakiej metody powinienem użyć?

Prawie nigdy nie zależy to od twoich preferencji pisania, ale od kontekstu. Jeśli chcesz umieścić obiekt na kilku stosach lub jeśli jest zbyt ciężki dla stosu, przydziel go w darmowym sklepie. Ponadto, ponieważ przydzielasz obiekt, jesteś również odpowiedzialny za zwolnienie pamięci. Wyszukaj deleteoperatora.

Aby zmniejszyć ciężar korzystania z zarządzania darmowymi sklepami, ludzie wymyślili takie rzeczy jak auto_ptri unique_ptr. Zdecydowanie polecam je przejrzeć. Mogą nawet pomóc w problemach z pisaniem ;-)


10

Jeśli piszesz w C ++, prawdopodobnie piszesz dla wydajności. Korzystanie z nowego i darmowego sklepu jest znacznie wolniejsze niż korzystanie ze stosu (szczególnie przy użyciu wątków), więc używaj go tylko wtedy, gdy go potrzebujesz.

Jak powiedzieli inni, potrzebujesz nowego, gdy twój obiekt musi żyć poza zakresem funkcji lub obiektu, obiekt jest naprawdę duży lub gdy nie znasz rozmiaru tablicy w czasie kompilacji.

Staraj się też unikać usuwania. Zamiast tego zawiń swój nowy w inteligentny wskaźnik. Pozwól, aby połączenie inteligentnego wskaźnika zostało usunięte.

W niektórych przypadkach inteligentny wskaźnik nie jest inteligentny. Nigdy nie przechowuj std :: auto_ptr <> wewnątrz kontenera STL. Zbyt wcześnie usunie wskaźnik ze względu na operacje kopiowania wewnątrz kontenera. Innym przypadkiem jest posiadanie naprawdę dużego kontenera STL wskaźników do obiektów. boost :: shared_ptr <> będzie miał mnóstwo narzutu prędkości, ponieważ powoduje wzrost liczby referencji w górę iw dół. W takim przypadku lepszym sposobem jest umieszczenie kontenera STL w innym obiekcie i nadanie temu obiektowi destruktora, który wywoła funkcję usuwania na każdym wskaźniku w kontenerze.


10

Krótka odpowiedź brzmi: jeśli jesteś początkującym w C ++, nigdy nie powinieneś używać newani deletesiebie.

Zamiast tego powinieneś używać inteligentnych wskaźników, takich jak std::unique_ptri std::make_unique(lub rzadziej std::shared_ptri std::make_shared). W ten sposób nie musisz martwić się tak bardzo o wycieki pamięci. A nawet jeśli jesteś bardziej zaawansowany, najlepszą praktyką byłoby zazwyczaj umieszczenie niestandardowego sposobu korzystania z niego newi umieszczenie deletego w małej klasie (takiej jak niestandardowy inteligentny wskaźnik), która jest przeznaczona tylko do rozwiązywania problemów związanych z cyklem życia obiektu.

Oczywiście za kulisami te inteligentne wskaźniki nadal wykonują dynamiczne przydzielanie i zwalnianie, więc użycie ich przez kod nadal wiązałoby się z dodatkowym obciążeniem środowiska wykonawczego. Inne odpowiedzi tutaj omawiały te problemy i jak podejmować decyzje projektowe, kiedy używać inteligentnych wskaźników, a nie tylko tworzyć obiekty na stosie lub włączać je jako bezpośrednie elementy obiektu, na tyle, że ich nie powtórzę. Ale moje streszczenie byłoby następujące: nie używaj inteligentnych wskaźników ani dynamicznej alokacji, dopóki coś cię do tego nie zmusi.


ciekawe, jak odpowiedź może się zmieniać w miarę upływu czasu;)
Wolf


2

Prosta odpowiedź brzmi: tak - new () tworzy obiekt na stercie (z niefortunnym efektem ubocznym, że musisz zarządzać jego żywotnością (poprzez jawne wywołanie na nim delete), podczas gdy druga forma tworzy obiekt na stosie w bieżącym zakres i ten obiekt zostanie zniszczony, gdy wyjdzie poza zakres.


1

Jeśli twoja zmienna jest używana tylko w kontekście jednej funkcji, lepiej jest użyć zmiennej stosu, tj. Opcji 2. Jak powiedzieli inni, nie musisz zarządzać czasem życia zmiennych stosu - są one zbudowane i zniszczone automatycznie. Również przydzielanie / zwalnianie zmiennej na stercie jest powolne w porównaniu. Jeśli twoja funkcja jest wywoływana wystarczająco często, zobaczysz ogromną poprawę wydajności, jeśli użyjesz zmiennych stosu względem zmiennych stosu.

To powiedziawszy, istnieje kilka oczywistych przypadków, w których zmienne stosu są niewystarczające.

Jeśli zmienna stosu ma duży obszar pamięci, wówczas istnieje ryzyko przepełnienia stosu. Domyślnie rozmiar stosu każdego wątku wynosi 1 MB w systemie Windows. Jest mało prawdopodobne, że utworzysz zmienną stosu o wielkości 1 MB, ale musisz pamiętać, że wykorzystanie stosu jest kumulatywne. Jeśli twoja funkcja wywołuje funkcję, która wywołuje inną funkcję, która wywołuje inną funkcję, która ..., zmienne stosu we wszystkich tych funkcjach zajmują miejsce na tym samym stosie. Funkcje rekurencyjne mogą szybko napotkać ten problem, w zależności od głębokości rekurencji. Jeśli jest to problem, możesz zwiększyć rozmiar stosu (niezalecane) lub przydzielić zmienną na stercie za pomocą nowego operatora (zalecane).

Innym, bardziej prawdopodobnym warunkiem jest to, że twoja zmienna musi „żyć” poza zakresem twojej funkcji. W takim przypadku należy przypisać zmienną do sterty, aby można było do niej dotrzeć poza zakresem dowolnej funkcji.


1

Czy przekazujesz myClass z funkcji, czy spodziewasz się, że istnieje poza tą funkcją? Jak powiedzieli niektórzy inni, chodzi o zakres, gdy nie przydzielasz sterty. Po wyjściu z funkcji znika (ostatecznie). Jednym z klasycznych błędów popełnianych przez początkujących jest próba utworzenia lokalnego obiektu jakiejś klasy w funkcji i zwrócenia go bez przydzielania go na stercie. Pamiętam debugowanie tego rodzaju rzeczy w moich wcześniejszych czasach, gdy robiłem c ++.


0

Druga metoda tworzy instancję na stosie, wraz z takimi rzeczami, jak coś zadeklarowane inti lista parametrów, które są przekazywane do funkcji.

Pierwsza metoda pozwala na umieszczenie wskaźnika na stosie, który ustawiłeś w miejscu w pamięci, w którym nowyMyClass został przydzielony na stosie - lub w sklepie darmowym.

Pierwsza metoda wymaga również tego, deleteco tworzysz new, podczas gdy w drugiej metodzie klasa jest automatycznie niszczona i uwalniana, gdy wypadnie poza zakres (zwykle następny nawias zamykający).


-1

Krótka odpowiedź brzmi: tak, słowo kluczowe „new” jest niezwykle ważne, ponieważ kiedy go używasz, dane obiektowe są przechowywane na stosie, a nie na stosie, co jest najważniejsze!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.