Dlaczego programiści C ++ powinni zminimalizować użycie „nowego”?


873

Natknąłem się na pytanie o przepełnienie stosu Przeciek pamięci ze std :: string przy użyciu std :: list <std :: string> , a jeden z komentarzy mówi:

Przestań używać newtak dużo. Nigdzie nie widziałem powodu, dla którego używałeś nowego. Możesz tworzyć obiekty według wartości w C ++ i jest to jedna z ogromnych zalet korzystania z języka.
Nie musisz alokować wszystkiego na stercie.
Przestań myśleć jak programista Java .

Nie jestem do końca pewien, co przez to rozumie.

Dlaczego obiekty powinny być tworzone według wartości w C ++ tak często, jak to możliwe i jaką różnicę robi to wewnętrznie?
Czy źle zinterpretowałem odpowiedź?

Odpowiedzi:


1037

Istnieją dwie powszechnie stosowane techniki alokacji pamięci: alokacja automatyczna i alokacja dynamiczna. Zwykle dla każdego istnieje odpowiedni region pamięci: stos i sterta.

Stos

Stos zawsze przydziela pamięć w sposób sekwencyjny. Może to zrobić, ponieważ wymaga zwolnienia pamięci w odwrotnej kolejności (pierwsze wejście, ostatnie wyjście: FILO). Jest to technika alokacji pamięci dla zmiennych lokalnych w wielu językach programowania. Jest bardzo, bardzo szybki, ponieważ wymaga minimalnej księgowości, a następny adres do przydzielenia jest domyślny.

W C ++ nazywa się to automatycznym magazynowaniem, ponieważ magazyn jest zgłaszany automatycznie na końcu zakresu. Po {}zakończeniu wykonywania bieżącego bloku kodu (ograniczonego za pomocą ), pamięć dla wszystkich zmiennych w tym bloku jest automatycznie gromadzona. Jest to także moment, w którym przywoływane są destruktory w celu oczyszczenia zasobów.

Sterta

Sterta pozwala na bardziej elastyczny tryb alokacji pamięci. Księgowość jest bardziej złożona, a alokacja wolniejsza. Ponieważ nie ma niejawnego punktu zwolnienia, musisz zwolnić pamięć ręcznie, używając deletelub delete[]( freew C). Jednak brak niejawnego punktu zwolnienia jest kluczem do elastyczności stosu.

Powody korzystania z alokacji dynamicznej

Nawet jeśli korzystanie ze sterty jest wolniejsze i potencjalnie prowadzi do wycieków pamięci lub fragmentacji pamięci, istnieją bardzo dobre przypadki użycia dla alokacji dynamicznej, ponieważ jest ona mniej ograniczona.

Dwa kluczowe powody korzystania z alokacji dynamicznej:

  • Nie wiesz, ile pamięci potrzebujesz w czasie kompilacji. Na przykład podczas odczytywania pliku tekstowego do łańcucha zwykle nie wiesz, jaki rozmiar ma ten plik, więc nie możesz zdecydować, ile pamięci ma zostać przydzielone, dopóki nie uruchomisz programu.

  • Chcesz przydzielić pamięć, która pozostanie po opuszczeniu bieżącego bloku. Na przykład możesz chcieć napisać funkcję, string readfile(string path)która zwraca zawartość pliku. W takim przypadku nawet jeśli stos może pomieścić całą zawartość pliku, nie można wrócić z funkcji i zachować przydzielonego bloku pamięci.

Dlaczego dynamiczna alokacja jest często niepotrzebna

W C ++ istnieje zgrabna konstrukcja zwana destruktorem . Ten mechanizm pozwala zarządzać zasobami, dopasowując czas życia zasobu do czasu życia zmiennej. Ta technika nazywa się RAII i jest wyróżnikiem C ++. „Zawija” zasoby w obiekty. std::stringjest doskonałym przykładem. Ten fragment kodu:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

faktycznie przydziela zmienną ilość pamięci. std::stringPrzydziela pamięć obiektów za pomocą sterty i uwalnia go w jego destruktora. W takim przypadku nie trzeba ręcznie zarządzać zasobami, a korzyści z dynamicznej alokacji pamięci są nadal możliwe.

W szczególności oznacza to, że w tym fragmencie:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

istnieje niepotrzebna dynamiczna alokacja pamięci. Program wymaga więcej pisania (!) I stwarza ryzyko zapomnienia o zwolnieniu pamięci. Robi to bez widocznych korzyści.

Dlaczego warto korzystać z automatycznego przechowywania tak często, jak to możliwe

Zasadniczo ostatni akapit podsumowuje. Używanie automatycznego przechowywania tak często, jak to możliwe, sprawia, że ​​twoje programy:

  • szybciej pisać;
  • szybciej po uruchomieniu;
  • mniej podatne na wycieki pamięci / zasobów.

Punkty bonusowe

W cytowanym pytaniu pojawiają się dodatkowe obawy. W szczególności następująca klasa:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Jest o wiele bardziej ryzykowny w użyciu niż następujący:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

Powodem jest to, że std::stringpoprawnie definiuje konstruktor kopii. Rozważ następujący program:

int main ()
{
    Line l1;
    Line l2 = l1;
}

Korzystając z oryginalnej wersji, ten program najprawdopodobniej ulegnie awarii, ponieważ używa deletetego samego ciągu dwukrotnie. Korzystając ze zmodyfikowanej wersji, każda Lineinstancja będzie posiadać własną instancję łańcuchową , każda z własną pamięcią i obie zostaną zwolnione na końcu programu.

Inne notatki

Szerokie zastosowanie RAII jest uważane za najlepszą praktykę w C ++ ze wszystkich powyższych powodów. Istnieje jednak dodatkowa korzyść, która nie jest od razu oczywista. Zasadniczo jest lepszy niż suma jego części. Cały mechanizm się komponuje . Skaluje się.

Jeśli użyjesz Lineklasy jako elementu konstrukcyjnego:

 class Table
 {
      Line borders[4];
 };

Następnie

 int main ()
 {
     Table table;
 }

przydziela cztery std::stringinstancje, cztery Lineinstancje, jedną Tableinstancję i całą zawartość łańcucha, a wszystko jest automatycznie uwalniane automatycznie .


55
+1 za wspomnienie RAII na końcu, ale powinno być coś o wyjątkach i odwijaniu stosu.
Tobu

7
@Tobu: tak, ale ten post jest już dość długi i chciałem raczej skupić się na pytaniu OP. W końcu skończę pisać blog lub coś i link do tego stąd.
André Caron

15
Byłby świetnym uzupełnieniem, aby wspomnieć o wadach alokacji stosu (przynajmniej do C ++ 1x) - często musisz niepotrzebnie kopiować rzeczy, jeśli nie jesteś ostrożny. np. Monsterwypluwa a Treasuredo Worldmomentu śmierci. W swojej Die()metodzie dodaje skarb do świata. Musi użyć go world->Add(new Treasure(/*...*/))w innym, aby zachować skarb po jego śmierci. Alternatywami są shared_ptr(może być nadmiar), auto_ptr(zły semantyczny termin przeniesienia własności), przekazywanie według wartości (marnotrawstwo) i move+ unique_ptr(jeszcze powszechnie nie wdrażane).
kizzx2

7
To, co powiedziałeś o zmiennych lokalnych przypisywanych do stosu, może być nieco mylące. „Stos” odnosi się do stosu wywołań, który przechowuje ramki stosu . To te ramki stosu są przechowywane w stylu LIFO. Zmienne lokalne dla określonej ramki są przydzielane tak, jakby były członkami struktury.
któregoś dnia

7
@someguy: Rzeczywiście, wyjaśnienie nie jest idealne. Wdrożenie ma swobodę w polityce alokacji. Jednak zmienne muszą być inicjowane i niszczone w sposób LIFO, więc analogia się utrzymuje. Nie sądzę, żeby to komplikowało komplikowanie odpowiedzi.
André Caron,

171

Ponieważ stos jest szybszy i szczelny

W C ++ potrzeba tylko jednej instrukcji, aby przydzielić miejsce - na stosie - dla każdego lokalnego obiektu zakresu w danej funkcji i nie można przeciekać żadnej z tych pamięci. Ten komentarz zamierzał (lub powinien był zamierzyć) powiedzieć coś w stylu „użyj stosu, a nie stosu”.


18
„przydzielenie miejsca zajmuje tylko jedną instrukcję” - och, nonsens. Oczywiście, aby dodać wskaźnik stosu, wystarczy tylko jedna instrukcja, ale jeśli klasa ma jakąkolwiek interesującą strukturę wewnętrzną, będzie o wiele więcej niż dodawanie wskaźnika stosu. Równie słuszne jest stwierdzenie, że w Javie nie trzeba instrukcji, aby przydzielić miejsce, ponieważ kompilator będzie zarządzał referencjami w czasie kompilacji.
Charlie Martin

31
@Charlie ma rację. Automatyczne zmienne są szybkie i niezawodne byłyby bardziej dokładne.
Oliver Charlesworth

28
@Charlie: Wewnętrzne elementy klasy muszą być skonfigurowane w obu kierunkach. Dokonuje się porównania przydzielenia wymaganego miejsca.
Oliver Charlesworth

51
kaszel int x; return &x;
peterchen

16
szybko tak. Ale z pewnością nie był niezawodny. Nic nie jest niezawodne. Możesz otrzymać StackOverflow :)
rxantos

107

Powód jest skomplikowany.

Po pierwsze, C ++ nie jest śmieciami. Dlatego dla każdego nowego musi istnieć odpowiednie usunięcie. Jeśli nie umieścisz tego usuwania, oznacza to wyciek pamięci. Teraz w przypadku prostego przypadku takiego:

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

To jest proste. Ale co się stanie, jeśli „Do stuff” zgłosi wyjątek? Ups: wyciek pamięci. Co się stanie, jeśli problem „Zrób rzeczy” returnwcześnie? Ups: wyciek pamięci.

I to jest w najprostszym przypadku . Jeśli zdarzy ci się zwrócić komuś ten ciąg, teraz musi go usunąć. A jeśli przekażą to jako argument, to czy osoba otrzymująca go musi go usunąć? Kiedy powinni je usunąć?

Lub możesz po prostu to zrobić:

std::string someString(...);
//Do stuff

Nie delete. Obiekt został utworzony na „stosie” i zostanie zniszczony, gdy znajdzie się poza zasięgiem. Możesz nawet zwrócić obiekt, przenosząc jego zawartość do funkcji wywołującej. Możesz przekazać obiekt do funkcji (zwykle jako odwołanie lub stałe odwołanie: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)itd.).

Wszystko bez newi delete. Nie ma wątpliwości, kto jest właścicielem pamięci lub kto jest odpowiedzialny za jej usunięcie. Jeśli zrobisz:

std::string someString(...);
std::string otherString;
otherString = someString;

Zrozumiałe jest, że otherStringposiada kopię danych z someString. To nie jest wskaźnik; jest to osobny obiekt. Może się zdarzyć, że mają tę samą zawartość, ale możesz zmienić jedną bez wpływu na drugą:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Widzisz pomysł?


1
Na tej notatce ... Jeśli obiekt jest dynamicznie przydzielany main(), istnieje przez czas trwania programu, nie można go łatwo utworzyć na stosie ze względu na sytuację, a wskaźniki do niego są przekazywane do wszystkich funkcji wymagających dostępu do niego , czy może to spowodować wyciek w przypadku awarii programu, czy też byłoby bezpieczne? Zakładam to drugie, ponieważ system operacyjny zwalniający całą pamięć programu powinien również logicznie zwolnić go, ale nie chcę niczego zakładać, jeśli chodzi o to new.
Justin Time - Przywróć Monikę

4
@JustinTime Nie musisz się martwić o zwolnienie pamięci dynamicznie przydzielanych obiektów, które mają pozostać przez cały czas istnienia programu. Podczas wykonywania programu system operacyjny tworzy dla niego atlas pamięci fizycznej lub pamięci wirtualnej. Każdy adres w przestrzeni pamięci wirtualnej jest odwzorowywany na adres pamięci fizycznej, a po wyjściu programu zwalniane jest wszystko, co jest mapowane na jego pamięć wirtualną. Tak więc, dopóki program całkowicie się zakończy, nie musisz się martwić, że przydzielona pamięć nigdy nie zostanie usunięta.
Aiman ​​Al-Eryani

75

Obiekty utworzone przez newmuszą w końcu deletenie przeciekać. Destruktor nie zostanie wywołany, pamięć nie zostanie uwolniona, cały czas. Ponieważ C ++ nie ma śmieci, jest to problem.

Obiekty tworzone przez wartość (tj. Na stosie) automatycznie umierają, gdy wykraczają poza zakres. Wywołanie destruktora jest wstawiane przez kompilator, a pamięć jest automatycznie zwalniana po powrocie funkcji.

Inteligentne wskaźniki, takie jak unique_ptr, shared_ptrrozwiązują wiszący problem odniesienia, ale wymagają dyscypliny kodowania i mają inne potencjalne problemy (kopiowanie, pętle odniesienia itp.).

Również w mocno wielowątkowych scenariuszach newistnieje spór między wątkami; nadużycie może mieć wpływ na wydajność new. Tworzenie obiektu stosu jest z definicji wątkiem lokalnym, ponieważ każdy wątek ma swój własny stos.

Wadą obiektów wartości jest to, że umierają one po powrocie funkcji hosta - nie można przekazać odwołania do tych z powrotem do obiektu wywołującego, tylko poprzez kopiowanie, zwracanie lub przenoszenie według wartości.


9
+1. Odp: „Obiekty utworzone przez newmuszą w końcu deletenie przeciekać”. - jeszcze gorzej, new[]należy je dopasować delete[], a zachowanie jest niezdefiniowane, jeśli użyjesz delete new[]pamięci lub delete[] newpamięci -ed - bardzo niewiele kompilatorów ostrzega o tym (niektóre narzędzia, takie jak Cppcheck, robią to, kiedy mogą).
Tony Delroy

3
@TonyDelroy Są sytuacje, w których kompilator nie może tego ostrzec. Jeśli funkcja zwraca wskaźnik, można go utworzyć, jeśli jest nowy (pojedynczy element) lub nowy [].
fbafelipe

32
  • C ++ nie używa własnego menedżera pamięci. Inne języki, takie jak C #, Java ma moduł czyszczenia pamięci do obsługi pamięci
  • Implementacje w C ++ zwykle używają procedur systemu operacyjnego do przydzielania pamięci, a zbyt dużo nowych / usunięć może fragmentować dostępną pamięć
  • W przypadku dowolnej aplikacji, jeśli pamięć jest często używana, zaleca się jej wstępne przydzielenie i zwolnienie, gdy nie jest wymagane.
  • Nieprawidłowe zarządzanie pamięcią może prowadzić do wycieków pamięci i jest bardzo trudne do wyśledzenia. Zatem stosowanie obiektów stosu w zakresie funkcji jest sprawdzoną techniką
  • Wadą używania obiektów stosu jest to, że tworzy wiele kopii obiektów po powrocie, przekazaniu do funkcji itp. Jednak inteligentne kompilatory są w pełni świadome tych sytuacji i zostały zoptymalizowane pod kątem wydajności
  • To jest bardzo nudne w C ++, jeśli pamięć jest przydzielana i zwalniana w dwóch różnych miejscach. Odpowiedzialność za wydanie jest zawsze pytaniem i przeważnie polegamy na niektórych powszechnie dostępnych wskaźnikach, obiektach stosu (maksimum możliwe) i technikach takich jak auto_ptr (obiekty RAII)
  • Najlepsze jest to, że masz kontrolę nad pamięcią, a najgorsze jest to, że nie będziesz mieć żadnej kontroli nad pamięcią, jeśli zastosujemy niewłaściwe zarządzanie pamięcią dla aplikacji. Awarie spowodowane uszkodzeniami pamięci są najgorsze i trudne do wyśledzenia.

5
W rzeczywistości każdy język, który przydziela pamięć, ma menedżera pamięci, w tym c. Większość jest po prostu bardzo prosta, tzn. Int * x = malloc (4); int * y = malloc (4); ... pierwsze połączenie alokuje pamięć, czyli proszenie o pamięć (zwykle w porcjach 1k / 4k), dzięki czemu drugie połączenie tak naprawdę nie alokuje pamięci, ale daje kawałek ostatniego fragmentu, na który została przydzielona. IMO, śmieciarze nie są menedżerami pamięci, ponieważ obsługuje tylko automatyczne zwolnienie pamięci. Aby zostać nazwanym menedżerem pamięci, powinien on nie tylko obsługiwać cofanie przydziału, ale także alokować pamięć.
Rahly,

1
Zmienne lokalne używają stosu, więc kompilator nie emituje wywołania malloc()ani jego przyjaciół w celu przydzielenia wymaganej pamięci. Jednak stos nie może zwolnić żadnego elementu w stosie, jedynym sposobem na zwolnienie pamięci stosu jest odwijanie z góry stosu.
Mikko Rantalainen,

C ++ nie „używa procedur systemu operacyjnego”; to nie jest część języka, to tylko powszechna implementacja. C ++ może nawet działać bez żadnego systemu operacyjnego.
einpoklum

22

Widzę, że pominięto kilka ważnych powodów, dla których należy robić jak najmniej nowości:

Operator newma niedeterministyczny czas wykonania

Wywołanie newmoże, ale nie musi, spowodować, że system operacyjny przydzieli nową fizyczną stronę procesowi. Może to być dość powolne, jeśli robisz to często. Lub może mieć już przygotowaną odpowiednią lokalizację pamięci, nie wiemy. Jeśli Twój program musi mieć spójny i przewidywalny czas wykonania (np. W systemie czasu rzeczywistego lub symulacji gry / fizyki), musisz unikać newkrytycznych pętli czasowych.

Operator newto niejawna synchronizacja wątków

Tak, słyszałeś mnie, twój system operacyjny musi się upewnić, że tabele stron są spójne i dlatego takie wywołanie newspowoduje, że Twój wątek uzyska ukrytą blokadę mutex. Jeśli konsekwentnie dzwonisz newz wielu wątków, w rzeczywistości szeregujesz swoje wątki (zrobiłem to z 32 procesorami, z których każdy uderzył o newkilkaset bajtów, ouch! To była królewska pita do debugowania)

Pozostałe odpowiedzi, takie jak powolność, fragmentacja, podatność na błędy itp. Zostały już wspomniane w innych odpowiedziach.


2
Oba można uniknąć, umieszczając nowe / usuń i przydzielając pamięć przed ręką. Lub możesz samodzielnie przydzielić / zwolnić pamięć, a następnie wywołać konstruktor / destruktor. Tak zazwyczaj działa std :: vector.
rxantos

1
@rxantos Proszę przeczytać OP, to pytanie dotyczy unikania niepotrzebnego przydziału pamięci. Ponadto nie ma usunięcia miejsca docelowego.
Emily L.,

@Emily Oto, co miał na myśli OP:void * someAddress = ...; delete (T*)someAddress
xryl669,

1
Używanie stosu również nie jest deterministyczne w czasie wykonywania. Chyba że zadzwoniłeś mlock()lub coś podobnego. Jest tak, ponieważ w systemie może brakować pamięci i nie ma gotowych stron pamięci fizycznej dla stosu, więc system operacyjny może potrzebować zamienić lub zapisać niektóre pamięci podręczne (wyczyścić brudną pamięć) na dysku, zanim będzie można kontynuować wykonywanie.
Mikko Rantalainen,

1
@mikkorantalainen jest to technicznie prawdą, ale w sytuacji niskiego poziomu pamięci wszystkie zakłady i tak tracą wydajność, gdy naciskasz na dysk, więc nie możesz nic zrobić. W żadnym wypadku nie unieważnia to porady unikania nowych połączeń, gdy jest to uzasadnione.
Emily L.,

21

Pre-C ++ 17:

Ponieważ jest podatny na subtelne wycieki, nawet jeśli wynik zostanie zawinięty w inteligentny wskaźnik .

Rozważ „ostrożnego” użytkownika, który pamięta zawijanie obiektów w inteligentne wskaźniki:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Ten kod jest niebezpieczne, ponieważ nie ma żadnej gwarancji, że albo shared_ptrjest zbudowany przed albo T1albo T2. Stąd, jeśli jeden new T1()lub new T2()nie powiedzie się po drugiej powiedzie, to pierwszy obiekt będzie przeciekał ponieważ nie shared_ptristnieje, aby zniszczyć i zwalnianie go.

Rozwiązanie: użyj make_shared.

Post-C ++ 17:

Nie stanowi to już problemu: C ++ 17 nakłada ograniczenie na kolejność tych operacji, w tym przypadku zapewniając, że po każdym wywołaniu new()musi natychmiast nastąpić konstrukcja odpowiedniego inteligentnego wskaźnika, bez żadnych innych operacji pomiędzy. Oznacza to, że do czasu new()wywołania drugiego jest zagwarantowane, że pierwszy obiekt został już owinięty w inteligentny wskaźnik, zapobiegając w ten sposób wyciekom w przypadku zgłoszenia wyjątku.

Barry bardziej szczegółowe objaśnienie nowego porządku oceny wprowadzonego przez C ++ 17 zostało dostarczone przez Barry'ego w innej odpowiedzi .

Dzięki @Remy Lebeau za zwrócenie uwagi, że nadal jest to problem w C ++ 17 (choć mniej): shared_ptrkonstruktor może nie przydzielić bloku kontrolnego i wyrzucić, w którym to przypadku przekazany do niego wskaźnik nie jest usuwany.

Rozwiązanie: użyj make_shared.


5
Inne rozwiązanie: Nigdy nie przydzielaj dynamicznie więcej niż jednego obiektu na linię.
Antymon

3
@Antimony: Tak, o wiele bardziej kuszące jest przydzielenie więcej niż jednego obiektu, gdy już go przydzieliłeś, w porównaniu do tego, kiedy go nie przydzieliłeś.
user541686,

1
Myślę, że lepszą odpowiedzią jest to, że smart_ptr wycieknie, jeśli zostanie wywołany wyjątek i nic go nie złapie.
Natalie Adams,

2
Nawet w przypadku po wersji C ++ 17 wyciek może się zdarzyć, jeśli się newpowiedzie, a następnie następna shared_ptrkonstrukcja się nie powiedzie. std::make_shared()też by to rozwiązał
Remy Lebeau

1
@Mehrdad, o którym mowa, shared_ptrkonstruktor przydziela pamięć blokowi kontrolnemu, który przechowuje wspólny wskaźnik i separator, więc tak, teoretycznie może wyrzucić błąd pamięci. Tylko konstruktory kopiujące, przenoszące i aliasingowe nie rzucają. make_sharedprzydziela współdzielony obiekt w samym bloku sterującym, więc jest tylko 1 przydział zamiast 2.
Remy Lebeau

17

W dużej mierze jest to ktoś, kto podnosi swoje słabości do ogólnej zasady. Nie ma nic złego per se z tworzenia obiektów za pomocą newoperatora. Argumentuje się za tym, że musisz to robić z pewną dyscypliną: jeśli tworzysz obiekt, musisz upewnić się, że zostanie zniszczony.

Najłatwiej to zrobić, tworząc obiekt w automatycznym magazynie, więc C ++ wie, jak go zniszczyć, gdy przekroczy zakres:

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

    // do things

 }

Teraz zauważ, że kiedy spadniesz z tego bloku po nawiasie końcowym, foojest poza zasięgiem. C ++ wywoła dla ciebie swój dtor automatycznie. W przeciwieństwie do Javy, nie musisz czekać, aż GC go znajdzie.

Czy napisałeś?

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

chciałbyś wyraźnie to dopasować

     delete foo;
  }

lub jeszcze lepiej, przydziel swój File *„inteligentny wskaźnik”. Jeśli nie będziesz ostrożny, może to prowadzić do wycieków.

Sama odpowiedź błędnie zakłada, że ​​jeśli nie użyjesz new, nie przydzielisz na stos; tak naprawdę w C ++ tego nie wiesz. Wiesz co najwyżej, że niewielka ilość pamięci, powiedzmy jeden wskaźnik, jest z pewnością przydzielona na stos. Zastanów się jednak, czy implementacja pliku jest podobna

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

następnie FileImplbędzie nadal być alokowane na stosie.

I tak, lepiej upewnij się, że masz

     ~File(){ delete fd ; }

również w klasie; bez niego wycieknie pamięć ze sterty, nawet jeśli najwyraźniej w ogóle nie przydzielono jej na sterty.


4
Powinieneś rzucić okiem na kod w pytaniu, do którego się odnosi. Z pewnością w tym kodzie jest wiele błędów.
André Caron

7
Zgadzam się, że nie ma nic złego w używaniu new per se , ale jeśli spojrzysz na oryginalny kod, do którego odnosi się komentarz, newjest on nadużywany. Kod jest napisany tak, jakby to był Java lub C #, gdzie newjest używany dla praktycznie każdej zmiennej, gdy na stosie jest o wiele więcej sensu.
luke

5
Uczciwy punkt. Ale ogólne zasady są zwykle egzekwowane, aby uniknąć typowych pułapek. Niezależnie od tego, czy była to słabość jednostki, czy nie, zarządzanie pamięcią jest wystarczająco złożone, aby uzasadnić taką ogólną zasadę! :)
Robben_Ford_Fan_boy

9
@Charlie: komentarz nie mówi, że nigdy nie powinieneś używać new. Mówi, że jeśli mają wybór między automatycznym i dynamicznej alokacji pamięci, użyj automatycznego zapisu.
André Caron

8
@Charlie: nie ma nic złego w korzystaniu new, ale jeśli używasz delete, robisz to źle!
Matthieu M.

16

new()nie powinien być używany tak mało, jak to możliwe. Należy go stosować tak ostrożnie, jak to możliwe. I należy go używać tak często, jak to konieczne, podyktowane pragmatyzmem.

Przydział obiektów na stosie, polegający na ich niejawnym zniszczeniu, jest prostym modelem. Jeśli wymagany zakres obiektu pasuje do tego modelu, nie ma potrzeby korzystania new()z powiązanego delete()i sprawdzania wskaźników NULL. W przypadku dużej ilości alokacji obiektów krótkotrwałych na stosie należy zmniejszyć problemy z fragmentacją sterty.

Jeśli jednak czas życia obiektu musi wykraczać poza obecny zakres, new()to właściwa odpowiedź. Upewnij się tylko, że zwracasz uwagę na to, kiedy i jak wywołujesz, delete()oraz na możliwości wskaźników NULL, używając usuniętych obiektów i wszystkich innych gotch, które pochodzą z użyciem wskaźników.


9
„jeśli czas życia obiektu musi wykraczać poza aktualny zakres, to właściwa jest odpowiedź new ()”… dlaczego nie preferencyjnie zwracać wartość lub nie przyjmować zmiennej o zasięgu funkcji wywołującej przez constodwołanie lub wskaźnik…?
Tony Delroy

2
@ Tony: Tak, tak! Cieszę się, że ktoś popiera referencje. Zostały stworzone, aby zapobiec temu problemowi.
Nathan Osman

@TonyD ... lub połącz je: zwróć inteligentny wskaźnik według wartości. W ten sposób dzwoniący i w wielu przypadkach (tzn. Gdzie make_shared/_uniquejest to użyteczne) odbiorca nigdy nie musi newlub delete. W tej odpowiedzi brakuje prawdziwych punktów: (A) C ++ zapewnia takie rzeczy, jak RVO, semantyka przenoszenia i parametry wyjściowe - co często oznacza, że ​​obsługa tworzenia obiektów i wydłużania czasu życia poprzez zwracanie dynamicznie przydzielanej pamięci staje się niepotrzebna i nieostrożna. (B) Nawet w sytuacjach, w których wymagany jest dynamiczny przydział, stdlib zapewnia opakowania RAII, które odciążają użytkownika od brzydkich wewnętrznych szczegółów.
underscore_d

14

Gdy używasz nowego, obiekty są przydzielane do sterty. Zwykle jest używany, gdy spodziewasz się rozszerzenia. Kiedy deklarujesz obiekt taki jak:

Class var;

jest umieszczony na stosie.

Zawsze będziesz musiał wywołać kill na obiekcie, który umieściłeś na stosie z nowym. Otwiera to możliwość wycieków pamięci. Obiekty umieszczone na stosie nie są podatne na wycieki pamięci!


2
+1 „[kupa] zwykle używana, gdy spodziewasz się rozszerzenia” - na przykład dołączenie do std::stringlub std::map, tak, bystry wgląd. Moją początkową reakcją było „ale także bardzo często oddzielenie czasu życia obiektu od zakresu tworzenia kodu”, ale tak naprawdę zwracanie wartości lub akceptowanie wartości o charakterze wywołującym przez constodwołanie lub wskaźnik jest do tego lepsze, z wyjątkiem sytuacji, gdy występuje „ekspansja” też. Istnieją jednak inne zastosowania dźwięku, takie jak metody fabryczne ...
Tony Delroy

12

Jednym z godnych uwagi powodów, aby uniknąć nadużywania sterty, jest wydajność - szczególnie związana z wydajnością domyślnego mechanizmu zarządzania pamięcią używanego przez C ++. Podczas gdy alokacja może być dość szybka w trywialnym przypadku, robienie dużej ilości newi deletena obiektach o niejednorodnym rozmiarze bez ścisłego porządku prowadzi nie tylko do fragmentacji pamięci, ale także komplikuje algorytm alokacji i może całkowicie zniszczyć wydajność w niektórych przypadkach.

Jest to problem, który pule pamięci zostały stworzone do rozwiązania, co pozwala złagodzić nieodłączne wady tradycyjnych implementacji sterty, jednocześnie umożliwiając korzystanie ze sterty w razie potrzeby.

Jeszcze lepiej, aby całkowicie uniknąć problemu. Jeśli możesz umieścić go na stosie, zrób to.


Zawsze możesz przydzielić rozsądnie dużą ilość pamięci, a następnie użyć umieszczania nowego / usuń, jeśli problem stanowi prędkość.
rxantos

Pule pamięci mają na celu uniknięcie fragmentacji, przyspieszenie dezalokacji (jedna dezalokacja na tysiące obiektów) i zwiększenie bezpieczeństwa dealokacji.
Lothar,

10

Myślę, że plakat miał You do not have to allocate everything on theheapraczej na myśli niż stack.

Zasadniczo obiekty są alokowane na stosie (jeśli pozwala na to rozmiar obiektu, oczywiście) ze względu na niski koszt alokacji stosu, a nie alokację opartą na stercie, która wymaga dość pracy ze strony alokatora, i dodaje gadatliwości, ponieważ wtedy musisz zarządzać danymi przydzielonymi na stercie.


10

Zwykle nie zgadzam się z pomysłem użycia nowego „za dużo”. Chociaż użycie nowego plakatu w klasach systemowych przez pierwotnego plakatu jest nieco niedorzeczne. ( int *i; i = new int[9999];? naprawdę? int i[9999];jest znacznie jaśniejszy.) Myślę, że to właśnie dostało kozę komentatora.

Podczas pracy z obiektami systemowymi bardzo rzadko potrzeba więcej niż jednego odwołania do dokładnie tego samego obiektu. Dopóki wartość jest taka sama, tylko to się liczy. A obiekty systemowe zwykle nie zajmują dużo miejsca w pamięci. (jeden bajt na znak, w ciągu). A jeśli tak, biblioteki powinny być tak zaprojektowane, aby uwzględniały to zarządzanie pamięcią (jeśli są dobrze napisane). W takich przypadkach (wszystkie oprócz jednej lub dwóch wiadomości w jego kodzie) nowe są praktycznie bezcelowe i służą jedynie wprowadzeniu zamieszania i potencjalnym błędom.

Kiedy jednak pracujesz z własnymi klasami / obiektami (np. Klasą Line oryginalnego plakatu), musisz sam zacząć myśleć o takich kwestiach, jak pamięć, trwałość danych itp. W tym momencie dopuszczenie wielu odwołań do tej samej wartości jest nieocenione - pozwala na konstrukcje takie jak listy połączone, słowniki i wykresy, w których wiele zmiennych musi mieć nie tylko tę samą wartość, ale odwoływać się dokładnie do tego samego obiektu w pamięci. Jednak klasa Line nie ma żadnego z tych wymagań. Tak więc kod oryginalnego plakatu nie ma absolutnie żadnej potrzeby new.


zwykle nowy / delete będzie używał go, gdy nie wiesz z góry wielkości tablicy. Oczywiście std :: vector ukrywa dla ciebie nowy / usuń. Nadal ich używasz, ale przez std :: vector. Tak więc w dzisiejszych czasach byłby używany, gdy nie znasz rozmiaru tablicy i chcesz z jakiegoś powodu uniknąć narzutu std :: vector (który jest mały, ale nadal istnieje).
rxantos

When you're working with your own classes/objects... często nie masz ku temu powodu! Niewielka część pytań dotyczy szczegółów projektowania pojemników przez wykwalifikowanych programistów. W przeciwieństwie, przygnębiające odsetek o pomieszaniu początkujących, którzy nie znają stdlib istnieje - albo aktywnie danego zadania w okropnych „programowania” „kursy”, gdzie nauczyciel domaga się bezsensownie wyważać otwartych drzwi - przed Oni nawet dowiedziałem się, czym jest koło i dlaczego działa. Promując bardziej abstrakcyjną alokację, C ++ może uchronić nas przed niekończącym się „segfaultem C z połączoną listą”; proszę, pozwólmy temu .
underscore_d

użycie nowego plakatu przez klasę systemową w oryginalnym plakacie jest nieco niedorzeczne. ( int *i; i = new int[9999];? naprawdę? int i[9999];jest o wiele jaśniejsze.)„ Tak, jest jaśniejsze, ale aby zagrać w adwokata diabła, ten typ niekoniecznie jest złym argumentem. Z elementami 9999 mogę sobie wyobrazić ciasno osadzony system, który nie ma wystarczającego stosu dla elementów 9999: 9999 x 4 bajty to ~ 40 kB, x 8 ~ 80 kB. Dlatego takie systemy mogą wymagać użycia alokacji dynamicznej, zakładając, że implementują ją przy użyciu alternatywnej pamięci. Może to jednak uzasadniać dynamiczną alokację, a nie new; vectorbyłby prawdziwy dylemat w tej sprawie
underscore_d

Zgadzam się z @underscore_d - to nie jest tak świetny przykład. Nie dodałbym do tego stosu 40 000 lub 80 000 bajtów. Prawdopodobnie przydzieliłbym je na stosie ( std::make_unique<int[]>()oczywiście).
einpoklum

3

Dwa powody:

  1. W tym przypadku nie jest to konieczne. Sprawiasz, że kod staje się niepotrzebnie bardziej skomplikowany.
  2. Przydziela miejsce na stercie i oznacza, że ​​musisz o tym pamiętać deletepóźniej, w przeciwnym razie spowoduje to wyciek pamięci.

2

newjest nowy goto.

Przypomnij sobie, dlaczego gotojest tak oczerniany: chociaż jest to potężne narzędzie niskiego poziomu do kontroli przepływu, ludzie często używali go w niepotrzebnie skomplikowanych sposobach, które utrudniały śledzenie kodu. Ponadto najbardziej przydatne i najłatwiejsze do odczytania wzorce zostały zakodowane w ustrukturyzowanych instrukcjach programowania (np. forLub while); ostatecznym efektem jest to, że kod, w którym gotojest odpowiedni sposób, jest raczej rzadki, jeśli masz ochotę pisać goto, prawdopodobnie robisz rzeczy źle (chyba że naprawdę wiesz, co robisz).

newjest podobny - jest często używany, aby uczynić rzeczy niepotrzebnie skomplikowanymi i trudniejszymi do odczytania, a najbardziej przydatne wzorce użytkowania, które można zakodować, zostały zakodowane w różnych klasach. Ponadto, jeśli chcesz użyć nowych wzorców użycia, dla których nie ma jeszcze klas standardowych, możesz napisać własne klasy, które je kodują!

Twierdziłbym nawet, że newjest gorzej niż gotoze względu na potrzebę parowania newi deleteoświadczeń.

Na przykład goto, jeśli kiedykolwiek uważasz, że musisz użyć new, prawdopodobnie robisz coś źle - zwłaszcza jeśli robisz to poza wdrożeniem klasy, której celem w życiu jest zawarcie wszelkich dynamicznych alokacji, które musisz zrobić.


I dodałbym: „Zasadniczo po prostu tego nie potrzebujesz”.
einpoklum

1

Głównym powodem jest to, że obiekty na stercie są zawsze trudne w użyciu i zarządzaniu niż proste wartości. Pisanie łatwego do odczytania i utrzymania kodu jest zawsze priorytetem każdego poważnego programisty.

Innym scenariuszem jest używana przez nas biblioteka, która zapewnia semantykę wartości i sprawia, że ​​alokacja dynamiczna jest niepotrzebna. Std::stringjest dobrym przykładem

Jednak w przypadku kodu obiektowego konieczne jest użycie wskaźnika - co oznacza użycie newgo wcześniej. Aby uprościć złożoność zarządzania zasobami, mamy dziesiątki narzędzi, które sprawiają, że jest to tak proste, jak to możliwe, takie jak inteligentne wskaźniki. Paradygmat oparty na obiektach lub paradygmat ogólny zakłada semantykę wartości i wymaga mniej lub wcale new, tak jak napisano w innych plakatach.

Tradycyjne wzorce projektowe, zwłaszcza te wspomniane w książce GoF , newczęsto używają , ponieważ są typowym kodem OO.


4
To fatalna odpowiedź. For object oriented code, using a pointer [...] is a must: bzdury . Jeśli dewaluujesz „OO”, odnosząc się tylko do małego podzbioru, polimorfizm - także nonsens: referencje również działają. [pointer] means use new to create it beforehand: szczególnie bzdury : odniesienia lub wskaźniki mogą być pobierane do automatycznie przydzielanych obiektów i używane polimorficznie; oglądać mnie . [typical OO code] use new a lot: może w jakiejś starej książce, ale kogo to obchodzi? Wszelkie niejasne współczesne C ++ omijają new/ surowe wskaźniki tam, gdzie to możliwe - i nie jest w żaden sposób mniejszy OO, robiąc to
underscore_d

1

Jeszcze jeden punkt do wszystkich powyższych poprawnych odpowiedzi, zależy to od rodzaju wykonywanego programowania. Jądro rozwijające się na przykład w systemie Windows -> Stos jest poważnie ograniczony i może nie być możliwe przyjmowanie błędów stron, tak jak w trybie użytkownika.

W takich środowiskach nowe lub podobne do C wywołania API są preferowane, a nawet wymagane.

Oczywiście jest to jedynie wyjątek od reguły.


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.