Dlaczego użycie słowa „nowy” powoduje wycieki pamięci?


132

Najpierw nauczyłem się C #, a teraz zaczynam od C ++. Jak rozumiem, operator neww C ++ nie jest podobny do tego w C #.

Czy możesz wyjaśnić przyczynę wycieku pamięci w tym przykładowym kodzie?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

Odpowiedzi:


465

Co się dzieje

Podczas pisania T t;tworzysz obiekt typu Tz automatycznym czasem przechowywania . Zostanie wyczyszczony automatycznie, gdy wyjdzie poza zakres.

Podczas pisania new T()tworzysz obiekt typu Tz dynamicznym czasem przechowywania . Nie zostanie wyczyszczone automatycznie.

nowy bez czyszczenia

Musisz przekazać do niego wskaźnik, aby go deletewyczyścić:

nowe z usunięciem

Jednak twój drugi przykład jest gorszy: wyłuskujesz wskaźnik i tworzysz kopię obiektu. W ten sposób tracisz wskaźnik do obiektu utworzonego za pomocą new, więc nigdy nie możesz go usunąć, nawet jeśli chcesz!

newing z deref

Co powinieneś zrobić

Powinieneś preferować automatyczny czas przechowywania. Potrzebujesz nowego obiektu, po prostu napisz:

A a; // a new object of type A
B b; // a new object of type B

Jeśli potrzebujesz dynamicznego czasu trwania przechowywania, przechowuj wskaźnik do przydzielonego obiektu w obiekcie automatycznego czasu trwania, który usunie go automatycznie.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

newing z automatic_pointer

Jest to powszechny idiom, który ma niezbyt opisową nazwę RAII ( pozyskiwanie zasobów to inicjalizacja ). Kiedy zdobywasz zasób, który wymaga czyszczenia, umieszczasz go w obiekcie o automatycznym czasie przechowywania, więc nie musisz się martwić o jego czyszczenie. Dotyczy to każdego zasobu, czy to pamięci, otwartych plików, połączeń sieciowych czy czegokolwiek zechcesz.

Ta automatic_pointerrzecz już istnieje w różnych formach, podałem ją tylko jako przykład. Bardzo podobna klasa istnieje w standardowej bibliotece o nazwie std::unique_ptr.

Istnieje również stara nazwa (sprzed C ++ 11), auto_ptrale teraz jest przestarzała, ponieważ ma dziwne zachowanie podczas kopiowania.

Są też jeszcze inteligentniejsze przykłady, na przykład std::shared_ptr, które pozwalają na wiele wskaźników do tego samego obiektu i czyści go tylko wtedy, gdy ostatni wskaźnik zostanie zniszczony.


4
@ user1131997: cieszę się, że zadałeś kolejne pytanie. Jak widać w komentarzach niełatwo to wytłumaczyć :)
R. Martinho Fernandes

@ R.MartinhoFernandes: doskonała odpowiedź. Tylko jedno pytanie. Dlaczego użyłeś zwrotu przez odwołanie w funkcji operatora * ()?
Destructor,

@Destructor późna odpowiedź: D. Powrót przez odniesienie pozwala zmodyfikować punkt, dzięki czemu możesz to zrobić, na przykład *p += 2tak, jak w przypadku zwykłego wskaźnika. Gdyby nie zwrócił przez odniesienie, nie naśladowałby normalnego zachowania wskaźnika, co jest tutaj zamiarem.
R. Martinho Fernandes

Bardzo dziękuję za poradę dotyczącą „przechowywania wskaźnika do przydzielonego obiektu w obiekcie z automatycznym czasem przechowywania, który usuwa go automatycznie”. Gdyby tylko istniał sposób, aby wymagać od programistów nauczenia się tego wzorca, zanim będą w stanie skompilować jakikolwiek C ++!
Andy,

35

Wyjaśnienie krok po kroku:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Więc pod koniec tego, masz obiekt na stercie bez wskaźnika do niego, więc nie można go usunąć.

Druga próbka:

A *object1 = new A();

jest wyciekiem pamięci tylko wtedy, gdy zapomnisz o deleteprzydzielonej pamięci:

delete object1;

W C ++ istnieją obiekty z automatycznym magazynowaniem, te utworzone na stosie, które są automatycznie usuwane, oraz obiekty z magazynem dynamicznym, na stercie, z którym alokujesz i które musisz newuwolnić delete. (to wszystko jest z grubsza ujęte)

Pomyśl, że powinieneś mieć deleteprzydzielony for każdy obiekt new.

EDYTOWAĆ

Pomyśl o tym, object2nie musi to być wyciek pamięci.

Poniższy kod jest tylko po to, by zwrócić uwagę, to zły pomysł, nigdy nie lubię takiego kodu:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

W tym przypadku, ponieważ otherjest przekazywana przez referencję, będzie to dokładny obiekt wskazywany przez new B(). Dlatego pobranie adresu przez &otheri usunięcie wskaźnika zwolniłoby pamięć.

Ale nie mogę tego wystarczająco podkreślić, nie rób tego. Jest tu tylko po to, aby zwrócić uwagę.


2
Myślałem tak samo: możemy go zhakować, aby nie przeciekać, ale nie chcesz tego robić. object1 również nie musi przeciekać, ponieważ jego konstruktor mógłby dołączyć się do jakiejś struktury danych, która w pewnym momencie go usunie.
CashCow

2
Zawsze tak kuszące jest pisanie odpowiedzi „można to zrobić, ale nie”! :-) Znam to uczucie
Kos

11

Biorąc pod uwagę dwa „obiekty”:

obj a;
obj b;

Nie zajmą tego samego miejsca w pamięci. Innymi słowy,&a != &b

Przypisanie wartości jednego do drugiego nie zmieni ich lokalizacji, ale zmieni ich zawartość:

obj a;
obj b = a;
//a == b, but &a != &b

Intuicyjnie „obiekty” wskaźnika działają w ten sam sposób:

obj *a;
obj *b = a;
//a == b, but &a != &b

Spójrzmy teraz na Twój przykład:

A *object1 = new A();

To jest przypisywanie wartości new A()do object1. Wartość jest wskaźnikiem, co oznacza object1 == new A(), ale &object1 != &(new A()). (Zwróć uwagę, że ten przykład nie jest prawidłowym kodem, służy tylko do wyjaśnienia)

Ponieważ wartość wskaźnika jest zachowana, możemy zwolnić pamięć, na którą wskazuje: delete object1;Z powodu naszej reguły zachowuje się to tak samo, jak w przypadku delete (new A());braku przecieku.


W drugim przykładzie kopiujesz wskazany obiekt. Wartość to zawartość tego obiektu, a nie rzeczywisty wskaźnik. Jak w każdym innym przypadku &object2 != &*(new A()).

B object2 = *(new B());

Straciliśmy wskaźnik do przydzielonej pamięci i dlatego nie możemy jej zwolnić. delete &object2;może wydawać się, że to zadziała, ale ponieważ &object2 != &*(new A())nie jest równoważne, delete (new A())a więc nieważne.


9

W C # i Javie, używasz new do tworzenia instancji dowolnej klasy i nie musisz się martwić o jej późniejsze zniszczenie.

C ++ ma również słowo kluczowe „new”, które tworzy obiekt, ale w przeciwieństwie do Javy czy C # nie jest to jedyny sposób tworzenia obiektu.

C ++ ma dwa mechanizmy tworzenia obiektu:

  • automatyczny
  • dynamiczny

Dzięki automatycznemu tworzeniu obiekt tworzysz w środowisku o określonym zakresie: - w funkcji lub - jako element członkowski klasy (lub struktury).

W funkcji utworzyłbyś ją w ten sposób:

int func()
{
   A a;
   B b( 1, 2 );
}

W klasie normalnie utworzyłbyś ją w ten sposób:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

W pierwszym przypadku obiekty są niszczone automatycznie po wyjściu z bloku zasięgu. Może to być funkcja lub blok zakresu w funkcji.

W tym drugim przypadku obiekt b jest niszczony wraz z instancją A, w której jest członkiem.

Obiekty są przydzielane jako nowe, gdy trzeba kontrolować okres istnienia obiektu, a następnie wymaga usunięcia, aby go zniszczyć. Dzięki technice znanej jako RAII zajmujesz się usuwaniem obiektu w miejscu, w którym go tworzysz, umieszczając go w obiekcie automatycznym i czekając, aż destruktor tego automatycznego obiektu zacznie działać.

Jednym z takich obiektów jest shared_ptr, który wywoła logikę „deleter”, ale tylko wtedy, gdy wszystkie instancje shared_ptr, które współużytkują obiekt, zostaną zniszczone.

Ogólnie, chociaż twój kod może mieć wiele wywołań new, powinieneś mieć ograniczone wywołania do usuwania i zawsze powinieneś upewnić się, że są one wywoływane z destruktorów lub obiektów „deleter”, które są umieszczane w inteligentnych wskaźnikach.

Twoje destruktory również nigdy nie powinny rzucać wyjątków.

Jeśli to zrobisz, będziesz miał kilka wycieków pamięci.


4
Jest więcej niż automatici dynamic. Jest też static.
Mooing Duck

9
B object2 = *(new B());

Ta linia jest przyczyną wycieku. Oddzielmy to trochę od siebie ..

object2 to zmienna typu B, przechowywana pod, powiedzmy, adresem 1 (tak, wybieram tutaj dowolne liczby). Po prawej stronie poprosiłeś o nowe B lub wskaźnik do obiektu typu B. Program chętnie ci to da i przypisuje twoje nowe B do adresu 2, a także tworzy wskaźnik w adresie 3. Teraz, jedynym sposobem uzyskania dostępu do danych pod adresem 2 jest użycie wskaźnika w adresie 3. Następnie wyłuskiwałeś wskaźnik przy użyciu, *aby uzyskać dane, na które wskazuje wskaźnik (dane w adresie 2). To skutecznie tworzy kopię tych danych i przypisuje ją do obiektu 2, przypisanego w adresie 1. Pamiętaj, że jest to KOPIA, a nie oryginał.

Oto problem:

Nigdy nie przechowywałeś tego wskaźnika w dowolnym miejscu, w którym możesz go użyć! Po zakończeniu tego przypisania wskaźnik (pamięć w adresie3, którego użyłeś do uzyskania dostępu do adresu2) jest poza zakresem i poza twoim zasięgiem! Nie możesz już wywołać usuwania na nim i dlatego nie możesz wyczyścić pamięci w adresie2. Zostaje Ci kopia danych z address2 w address1. Dwie takie same rzeczy tkwią w pamięci. Do jednego masz dostęp, do drugiego nie (bo zgubiłeś do niego ścieżkę). Dlatego jest to wyciek pamięci.

Sugerowałbym, że pochodząc z Twojego C # tła, dużo czytasz o tym, jak działają wskaźniki w C ++. Są tematem zaawansowanym i może zająć trochę czasu, ale ich użycie będzie dla ciebie bezcenne.


8

Jeśli to ułatwi, pomyśl o pamięci komputera jak o hotelu, a programy to klienci, którzy wynajmują pokoje, kiedy ich potrzebują.

Sposób działania tego hotelu polega na rezerwacji pokoju i poinformowaniu portiera o wyjeździe.

Jeśli zaprogramujesz rezerwację pokoju i wyjdziesz bez powiadomienia portiera, portier pomyśli, że pokój jest nadal używany i nie pozwoli nikomu go używać. W takim przypadku występuje wyciek w pomieszczeniu.

Jeśli twój program przydziela pamięć i nie usuwa jej (po prostu przestaje jej używać), to komputer uważa, że ​​pamięć jest nadal używana i nie pozwoli nikomu innemu jej używać. To wyciek pamięci.

Nie jest to dokładna analogia, ale może pomóc.


5
Całkiem podoba mi się ta analogia, nie jest doskonała, ale zdecydowanie jest to dobry sposób na wyjaśnienie przecieków pamięci osobom, które są w niej nowe!
AdamM

1
Wykorzystałem to w wywiadzie dla starszego inżyniera w Bloomberg w Londynie, aby wyjaśnić dziewczynie z działu HR wycieki pamięci. Przeszedłem przez ten wywiad, ponieważ byłem w stanie wyjaśnić wycieki pamięci (i problemy z tworzeniem wątków) osobie, która nie jest programistą, w sposób dla niej zrozumiały.
Stefan

7

Podczas tworzenia object2tworzysz kopię obiektu, który utworzyłeś za pomocą nowego, ale tracisz (nigdy nie przypisany) wskaźnik (więc nie ma sposobu, aby go później usunąć). Aby tego uniknąć, musisz zrobić object2odniesienie.


3
Korzystanie z adresu odniesienia w celu usunięcia obiektu jest niezwykle złą praktyką. Użyj inteligentnego wskaźnika.
Tom Whittock

3
Niesamowicie zła praktyka, co? Jak myślisz, czego inteligentne wskazówki używają za kulisami?
Blindy

3
@Blindy inteligentne wskaźniki (przynajmniej przyzwoicie wdrożone) używają wskaźników bezpośrednio.
Luchian Grigore

2
Cóż, szczerze mówiąc, cały pomysł nie jest taki wspaniały, prawda? Właściwie nie jestem nawet pewien, gdzie wzorzec wypróbowany w OP byłby przydatny.
Mario

7

Cóż, tworzysz wyciek pamięci, jeśli w pewnym momencie nie zwolnisz pamięci, którą przydzieliłeś za pomocą newoperatora, przekazując wskaźnik do tej pamięci deleteoperatorowi.

W dwóch powyższych przypadkach:

A *object1 = new A();

Tutaj nie używasz deletedo zwolnienia pamięci, więc jeśli i kiedy object1wskaźnik wyjdzie poza zakres, będziesz mieć wyciek pamięci, ponieważ straciłeś wskaźnik i nie możesz użyć na nim deleteoperatora.

I tu

B object2 = *(new B());

odrzucasz wskaźnik zwrócony przez new B(), więc nigdy nie możesz przekazać tego wskaźnika do, aby zwolnić deletepamięć. Stąd kolejny wyciek pamięci.


7

To ta linia, która natychmiast przecieka:

B object2 = *(new B());

Tutaj tworzysz nowy Bobiekt na stercie, a następnie tworzysz kopię na stosie. Nie można już uzyskać dostępu do tego, który został przydzielony na stercie, i stąd wyciek.

Ta linia nie jest od razu nieszczelna:

A *object1 = new A();

Nie byłoby wyciek, jeśli nigdy nie deletebyłyby object1jednak.


4
Nie używaj stosu / stosu przy wyjaśnianiu dynamicznego / automatycznego przechowywania.
Pubby

2
@Pubby, dlaczego nie używać? Ponieważ dynamiczne / automatyczne przechowywanie jest zawsze stertą, a nie stosem? I dlatego nie ma potrzeby szczegółowego opisywania stosu / stosu, prawda?

4
@ user1131997 Sterta / stos to szczegóły implementacji. Ważne jest, aby o nich wiedzieć, ale nie mają one znaczenia dla tego pytania.
Pubby

2
Hmm, chciałbym osobnej odpowiedzi na to pytanie, tj. Takiej samej jak moja, ale zastąpienie sterty / stosu tym, co uważasz za najlepsze. Chciałbym się dowiedzieć, jak wolałbyś to wyjaśnić.
mattjgalloway
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.