Najpierw nauczyłem się C #, a teraz zaczynam od C ++. Jak rozumiem, operator new
w 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:
Co się dzieje
Podczas pisania
T t;
tworzysz obiekt typuT
z automatycznym czasem przechowywania . Zostanie wyczyszczony automatycznie, gdy wyjdzie poza zakres.Podczas pisania
new T()
tworzysz obiekt typuT
z dynamicznym czasem przechowywania . Nie zostanie wyczyszczone automatycznie.Musisz przekazać do niego wskaźnik, aby go
delete
wyczyścić: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!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
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_pointer
rzecz już istnieje w różnych formach, podałem ją tylko jako przykład. Bardzo podobna klasa istnieje w standardowej bibliotece o nazwiestd::unique_ptr
.Istnieje również stara nazwa (sprzed C ++ 11),
auto_ptr
ale 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.źródło
*p += 2
tak, 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.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
delete
przydzielonej 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
new
uwolnićdelete
. (to wszystko jest z grubsza ujęte)Pomyśl, że powinieneś mieć
delete
przydzielony for każdy obiektnew
.EDYTOWAĆ
Pomyśl o tym,
object2
nie 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ż
other
jest przekazywana przez referencję, będzie to dokładny obiekt wskazywany przeznew B()
. Dlatego pobranie adresu przez&other
i 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ę.
źródło
Biorąc pod uwagę dwa „obiekty”:
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()
doobject1
. Wartość jest wskaźnikiem, co oznaczaobject1 == 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 przypadkudelete (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.źródło
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:
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.
źródło
automatic
idynamic
. Jest teżstatic
.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.
źródło
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.
źródło
Podczas tworzenia
object2
tworzysz 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ćobject2
odniesienie.źródło
Cóż, tworzysz wyciek pamięci, jeśli w pewnym momencie nie zwolnisz pamięci, którą przydzieliłeś za pomocą
new
operatora, przekazując wskaźnik do tej pamięcidelete
operatorowi.W dwóch powyższych przypadkach:
A *object1 = new A();
Tutaj nie używasz
delete
do zwolnienia pamięci, więc jeśli i kiedyobject1
wskaźnik wyjdzie poza zakres, będziesz mieć wyciek pamięci, ponieważ straciłeś wskaźnik i nie możesz użyć na nimdelete
operatora.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ćdelete
pamięć. Stąd kolejny wyciek pamięci.źródło
To ta linia, która natychmiast przecieka:
B object2 = *(new B());
Tutaj tworzysz nowy
B
obiekt 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
delete
byłybyobject1
jednak.źródło