Jakie są ogólne wskazówki, które pozwolą uniknąć wycieków pamięci w programach C ++? Jak ustalić, kto powinien zwolnić pamięć przydzieloną dynamicznie?
Jakie są ogólne wskazówki, które pozwolą uniknąć wycieków pamięci w programach C ++? Jak ustalić, kto powinien zwolnić pamięć przydzieloną dynamicznie?
Odpowiedzi:
Zamiast ręcznie zarządzać pamięcią, w stosownych przypadkach spróbuj użyć inteligentnych wskaźników.
Spójrz na Boost lib , TR1 i inteligentne wskaźniki .
Również inteligentne wskaźniki są teraz częścią standardu C ++ o nazwie C ++ 11 .
Całkowicie popieram wszystkie rady dotyczące RAII i inteligentnych wskaźników, ale chciałbym również dodać nieco wyższą wskazówkę: najłatwiejszą do zarządzania pamięcią jest pamięć, której nigdy nie przydzielono. W przeciwieństwie do języków takich jak C # i Java, w których prawie wszystko jest odniesieniem, w C ++ powinieneś umieszczać obiekty na stosie, kiedy tylko możesz. Jak zauważyło kilka osób (w tym dr Stroustrup), głównym powodem, dla którego zbieranie śmieci nigdy nie było popularne w C ++, jest to, że dobrze napisany C ++ nie produkuje zbyt wiele śmieci.
Nie pisz
Object* x = new Object;
lub nawet
shared_ptr<Object> x(new Object);
kiedy możesz po prostu pisać
Object x;
Ten post wydaje się powtarzalny, ale w C ++ najbardziej podstawowym wzorcem, jaki należy poznać, jest RAII .
Naucz się używać inteligentnych wskaźników, zarówno z boost, TR1, jak i nawet niskiego (ale często wystarczająco wydajnego) auto_ptr (ale musisz znać jego ograniczenia).
RAII jest podstawą zarówno bezpieczeństwa wyjątków, jak i usuwania zasobów w C ++, i żaden inny wzorzec (kanapka itp.) Nie zapewni Ci obu (i przez większość czasu nie da Ci żadnego).
Zobacz poniżej porównanie kodu RAII i kodu innego niż RAII:
void doSandwich()
{
T * p = new T() ;
// do something with p
delete p ; // leak if the p processing throws or return
}
void doRAIIDynamic()
{
std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
void doRAIIStatic()
{
T p ;
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
Podsumowując (po komentarzu Ogre Psalm33 ), RAII opiera się na trzech koncepcjach:
Oznacza to, że w poprawnym kodzie C ++ większość obiektów nie zostanie zbudowana za pomocą new
i zostanie zamiast tego zadeklarowana na stosie. A dla tych, zbudowany przy użyciu new
, wszystko będzie jakoś scoped (np dołączony do inteligentnego wskaźnika).
Jako programista jest to naprawdę bardzo potężne, ponieważ nie musisz przejmować się ręczną obsługą zasobów (jak to zrobiono w C lub w przypadku niektórych obiektów w Javie, które intensywnie używają try
/ finally
w tym przypadku) ...
„obiekty z lunetą… zostaną zniszczone… bez względu na wyjście” to nie do końca prawda. istnieją sposoby na oszukanie RAII. jakikolwiek wariant terminate () pominie czyszczenie. exit (EXIT_SUCCESS) jest pod tym względem oksymoronem.
wilhelmtell ma co do tego całkowitą rację: istnieją wyjątkowe sposoby na oszukanie RAII, a wszystkie prowadzą do nagłego zatrzymania procesu.
Są to wyjątkowe sposoby, ponieważ kod C ++ nie jest zaśmiecony zakończeniem, zakończeniem itp. Lub w przypadku wyjątków, chcemy, aby nieobsługiwany wyjątek spowodował awarię procesu i rdzeń zrzucił obraz pamięci tak, jak jest, a nie po wyczyszczeniu.
Ale nadal musimy wiedzieć o tych przypadkach, ponieważ chociaż rzadko się zdarzają, nadal mogą się zdarzyć.
(kto wywołuje terminate
lub exit
w zwykłym kodzie C ++? ... Pamiętam, że musiałem radzić sobie z tym problemem podczas zabawy z GLUT : Ta biblioteka jest bardzo zorientowana na język C, posuwając się do tego, że aktywnie ją projektuje, aby utrudniać programistom C ++ takie problemy, jak brak troski o danych zaalokowanych na stosie , czy o „ciekawych” decyzjach o nigdy nie powracaniu z ich głównej pętli … Nie będę tego komentował) .
Będziesz chciał spojrzeć na inteligentne wskazówki, takie jak inteligentne wskaźniki doładowania .
Zamiast
int main()
{
Object* obj = new Object();
//...
delete obj;
}
boost :: shared_ptr zostanie automatycznie usunięty, gdy liczba odwołań wyniesie zero:
int main()
{
boost::shared_ptr<Object> obj(new Object());
//...
// destructor destroys when reference count is zero
}
Zwróć uwagę na moją ostatnią uwagę: „kiedy liczba odniesień wynosi zero, co jest najfajniejszą częścią. Jeśli więc masz wielu użytkowników swojego obiektu, nie będziesz musiał śledzić, czy obiekt jest nadal używany. Gdy nikt nie odwołuje się do Twojego obiektu wspólny wskaźnik, zostanie zniszczony.
Nie jest to jednak panaceum. Chociaż możesz uzyskać dostęp do wskaźnika podstawowego, nie chciałbyś przekazać go do interfejsu API innej firmy, chyba że jesteś pewien, co robi. Wiele razy „wysyłasz” rzeczy do innego wątku w celu wykonania pracy PO zakończeniu tworzenia zakresu. Jest to typowe z PostThreadMessage w Win32:
void foo()
{
boost::shared_ptr<Object> obj(new Object());
// Simplified here
PostThreadMessage(...., (LPARAM)ob.get());
// Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}
Jak zawsze, użyj czapki myślenia z dowolnym narzędziem ...
Większość wycieków pamięci wynika z braku jasności co do własności obiektu i czasu jego życia.
Pierwszą rzeczą do zrobienia jest alokacja na stosie, kiedy tylko możesz. Dotyczy to większości przypadków, w których trzeba przydzielić pojedynczy obiekt do jakiegoś celu.
Jeśli naprawdę potrzebujesz „nowego” obiektu, przez większość czasu będzie on miał jednego oczywistego właściciela przez resztę swojego życia. W tej sytuacji używam wielu szablonów kolekcji, które są przeznaczone do „posiadania” obiektów w nich przechowywanych za pomocą wskaźnika. Są one implementowane z kontenerami wektorów STL i map, ale mają pewne różnice:
Mój beaf z STL polega na tym, że jest tak skoncentrowany na obiektach wartości, podczas gdy w większości aplikacji obiekty są unikalnymi jednostkami, które nie mają znaczącej semantyki kopiowania wymaganej do użycia w tych kontenerach.
Aha, wy, małe dzieci i wasi nowomodni zbieracze śmieci ...
Bardzo rygorystyczne zasady dotyczące „własności” - jaki obiekt lub część oprogramowania ma prawo usunąć obiekt. Jasne komentarze i mądre nazwy zmiennych, aby było oczywiste, czy wskaźnik „posiada”, czy jest „po prostu patrz, nie dotykaj”. Aby pomóc zdecydować, kto jest właścicielem czego, postępuj zgodnie z jak największym schematem „kanapki” w każdym podprogramie lub metodzie.
create a thing
use that thing
destroy that thing
Czasami trzeba tworzyć i niszczyć w bardzo różnych miejscach; Myślę, że trudno tego uniknąć.
W każdym programie wymagającym złożonych struktur danych tworzę ścisłe, wyraźne drzewo obiektów zawierających inne obiekty - używając wskaźników „właściciela”. To drzewo modeluje podstawową hierarchię koncepcji domeny aplikacji. Na przykład scena 3D posiada obiekty, światła, tekstury. Pod koniec renderowania, gdy program kończy pracę, istnieje jasny sposób na zniszczenie wszystkiego.
Wiele innych wskaźników jest definiowanych w razie potrzeby, ilekroć jeden podmiot potrzebuje dostępu do innego, aby przeskanować przebieg lub cokolwiek; są to „tylko patrzące”. Na przykładzie sceny 3D - obiekt używa tekstury, ale jej nie posiada; inne obiekty mogą używać tej samej tekstury. Zniszczenie obiektu ma nie wywoływać zniszczenie wszelkich faktur.
Tak, to czasochłonne, ale to właśnie robię. Rzadko mam wycieki pamięci lub inne problemy. Ale potem pracuję na ograniczonej arenie wysokowydajnego oprogramowania naukowego, akwizycji danych i grafiki. Nieczęsto zajmuję się transakcjami, takimi jak w bankowości i e-commerce, graficznych interfejsów użytkownika sterowanych zdarzeniami lub asynchronicznego chaosu o dużej sieci. Może nowe sposoby mają tam przewagę!
Świetne pytanie!
Jeśli używasz języka C ++ i tworzysz aplikację do obsługi procesora i pamięci w czasie rzeczywistym (np. gry), musisz napisać własnego Menedżera pamięci.
Myślę, że im lepiej możesz scalić kilka ciekawych prac różnych autorów, mogę ci podpowiedzieć:
Podzielnik o stałym rozmiarze jest szeroko omawiany wszędzie w sieci
Przydział małych obiektów został wprowadzony przez Alexandrescu w 2001 roku w jego doskonałej książce „Modern c ++ design”
Ogromny postęp (wraz z rozpowszechnianiem kodu źródłowego) można znaleźć w niesamowitym artykule w Game Programming Gem 7 (2008) zatytułowanym "Wysokowydajny alokator sterty" napisany przez Dimitar Lazarov
W tym artykule można znaleźć obszerną listę zasobów
Nie zaczynaj samodzielnie pisać bezużytecznego alokatora noob ... Najpierw DOKUMENTUJ SAM.
Jedną z technik, która stała się popularna w zarządzaniu pamięcią w C ++, jest RAII . Zasadniczo używasz konstruktorów / destruktorów do obsługi alokacji zasobów. Oczywiście w C ++ jest kilka innych nieprzyjemnych szczegółów związanych z bezpieczeństwem wyjątków, ale podstawowa idea jest dość prosta.
Generalnie sprawa sprowadza się do kwestii własności. Gorąco polecam przeczytanie serii Effective C ++ autorstwa Scotta Meyersa i Modern C ++ Design autorstwa Andrei Alexandrescu.
Jest już wiele o tym, jak uniknąć wycieków, ale jeśli potrzebujesz narzędzia, które pomoże Ci śledzić wycieki, spójrz na:
Udostępniaj i poznaj zasady własności pamięci w całym projekcie. Korzystanie z reguł COM zapewnia najlepszą spójność (parametry [in] są własnością wywołującego, wywoływany musi kopiować; [out] parametry są własnością wywołującego, wywoływany musi wykonać kopię, jeśli zachowuje referencję; itp.)
Valgrind to również dobre narzędzie do sprawdzania przecieków pamięci programów w czasie wykonywania.
Jest dostępny dla większości wersji Linuksa (w tym Androida) i Darwina.
Jeśli używasz do pisania testów jednostkowych dla swoich programów, powinieneś nabrać nawyku systematycznego uruchamiania Valgrind na testach. Potencjalnie pozwoli to uniknąć wielu wycieków pamięci na wczesnym etapie. Zwykle łatwiej jest je również zlokalizować w prostych testach niż w pełnym oprogramowaniu.
Oczywiście ta rada obowiązuje dla każdego innego narzędzia do sprawdzania pamięci.
Jeśli nie możesz / nie możesz użyć inteligentnego wskaźnika do czegoś (chociaż powinna to być ogromna czerwona flaga), wpisz swój kod za pomocą:
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
To oczywiste, ale upewnij się, że wpisałeś go, zanim wpiszesz kod w zakresie
Częstym źródłem tych błędów jest metoda, która akceptuje odniesienie lub wskaźnik do obiektu, ale pozostawia niejasną własność. Styl i konwencje komentowania mogą zmniejszyć prawdopodobieństwo wystąpienia tego problemu.
Niech przypadek, w którym funkcja przejmuje własność obiektu, będzie przypadkiem specjalnym. We wszystkich sytuacjach, w których tak się dzieje, pamiętaj o wpisaniu komentarza obok funkcji w pliku nagłówkowym, która to wskazuje. Należy dążyć do upewnienia się, że w większości przypadków moduł lub klasa, która alokuje obiekt, jest również odpowiedzialna za jego zwolnienie.
Używanie const może w niektórych przypadkach bardzo pomóc. Jeśli funkcja nie modyfikuje obiektu i nie przechowuje odniesienia do niego, które utrzymuje się po zwróceniu, zaakceptuj odwołanie do stałej. Czytając kod wywołującego będzie oczywiste, że Twoja funkcja nie przyjęła własności obiektu. Mogłeś mieć tę samą funkcję, która akceptowała wskaźnik inny niż stały, a wywołujący mógł założyć lub nie, że wywoływany zaakceptował własność, ale przy odwołaniu do stałej nie ma wątpliwości.
Nie używaj odwołań innych niż stałe w listach argumentów. Czytając kod dzwoniącego, jest bardzo niejasne, że wywoływany mógł zachować odniesienie do parametru.
Nie zgadzam się z komentarzami zalecającymi liczone wskaźniki referencyjne. Zwykle działa to dobrze, ale gdy masz błąd i nie działa, zwłaszcza jeśli twój destruktor robi coś nietrywialnego, na przykład w programie wielowątkowym. Zdecydowanie spróbuj dostosować swój projekt, aby nie potrzebować liczenia referencji, jeśli nie jest to zbyt trudne.
Wskazówki w kolejności ważności:
-Tip # 1 Zawsze pamiętaj, aby zadeklarować swoje destruktory jako „wirtualne”.
-Tip # 2 Użyj RAII
-Tip # 3 Użyj inteligentnych wskaźników doładowania
-Tip # 4 Nie pisz swoich własnych błędnych Smartpointerów, używaj boost (w projekcie, w którym teraz jestem, nie mogę użyć boostu i cierpiałem na konieczność debugowania własnych inteligentnych wskaźników, na pewno bym nie wziął znowu ta sama trasa, ale w tej chwili nie mogę dodać wzmocnienia do naszych zależności)
-Porada nr 5 Jeśli jest to przypadkowe / niekrytyczne dla wydajności (jak w grach z tysiącami obiektów), spójrz na kontener wskaźnika doładowania Thorstena Ottosena
-Wskazówka 6 Znajdź nagłówek wykrywania wycieków dla wybranej platformy, taki jak nagłówek „vld” Visual Leak Detection
Jeśli możesz, użyj boost shared_ptr i standardowego C ++ auto_ptr. Te przekazują semantykę własności.
Kiedy zwracasz auto_ptr, mówisz dzwoniącemu, że dajesz mu prawo własności do pamięci.
Kiedy zwracasz shared_ptr, mówisz dzwoniącemu, że masz do niego odniesienie i że przejmuje on część własności, ale nie jest to wyłącznie jego odpowiedzialność.
Ta semantyka dotyczy również parametrów. Jeśli dzwoniący przekazuje Ci auto_ptr, przekazuje Ci własność.
Inni wspominali przede wszystkim o sposobach unikania wycieków pamięci (jak inteligentne wskaźniki). Jednak narzędzie do profilowania i analizy pamięci jest często jedynym sposobem na wyśledzenie problemów z pamięcią, gdy już się pojawią.
Valgrind memcheck to doskonały darmowy.
Tylko w przypadku MSVC dodaj następujący tekst na początku każdego pliku .cpp:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
Następnie, podczas debugowania w VS2003 lub nowszym, zostaniesz poinformowany o wszelkich wyciekach, gdy twój program zostanie zamknięty (śledzi nowe / usunięte). To podstawowe, ale pomogło mi w przeszłości.
valgrind (dostępny tylko dla platform * nix) jest bardzo dobrym narzędziem do sprawdzania pamięci
Jeśli zamierzasz zarządzać pamięcią ręcznie, masz dwa przypadki:
Jeśli chcesz złamać którąkolwiek z tych zasad, udokumentuj to.
Chodzi o własność wskaźnika.
Możesz przechwycić funkcje alokacji pamięci i sprawdzić, czy są jakieś strefy pamięci, które nie są zwalniane po zakończeniu programu (chociaż nie jest to odpowiednie dla wszystkich aplikacji).
Można to również zrobić w czasie kompilacji, zastępując operatorów new i delete oraz inne funkcje alokacji pamięci.
Na przykład sprawdź w tej witrynie [Debugowanie alokacji pamięci w C ++] Uwaga: Istnieje sztuczka z operatorem usuwania również coś takiego:
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
Możesz przechowywać w niektórych zmiennych nazwę pliku i kiedy przeciążony operator usuwania będzie wiedział, z którego miejsca został wywołany. W ten sposób możesz mieć ślad każdego usunięcia i malloc ze swojego programu. Pod koniec sekwencji sprawdzania pamięci powinieneś być w stanie zgłosić, który przydzielony blok pamięci nie został „usunięty”, identyfikując go za pomocą nazwy pliku i numeru linii, co jest chyba tym, czego chcesz.
Możesz także wypróbować coś takiego jak BoundsChecker w programie Visual Studio, które jest dość interesujące i łatwe w użyciu.
Otaczamy wszystkie nasze funkcje alokacji warstwą, do której dołączamy krótki ciąg z przodu i flagę wartowniczą na końcu. Na przykład miałbyś wywołanie "myalloc (pszSomeString, iSize, iAlignment); lub new (" description ", iSize) MyObject (); które wewnętrznie przydziela określony rozmiar plus wystarczającą ilość miejsca na nagłówek i wartownika. Oczywiście , nie zapomnij skomentować tego w przypadku kompilacji bez debugowania! To zajmuje trochę więcej pamięci, ale korzyści znacznie przewyższają koszty.
Ma to trzy zalety - po pierwsze, pozwala łatwo i szybko śledzić, który kod przecieka, wykonując szybkie wyszukiwanie kodu przydzielonego w określonych „strefach”, ale nie czyszczonego, gdy te strefy powinny się zwolnić. Przydatne może być również wykrycie, kiedy granica została nadpisana, sprawdzając, czy wszystkie wartowniki są nienaruszone. Uratowało nas to wiele razy, gdy próbowaliśmy znaleźć te dobrze ukryte awarie lub błędy w tablicy. Trzecią korzyścią jest śledzenie wykorzystania pamięci, aby zobaczyć, kim są najwięksi gracze - na przykład zestawienie niektórych opisów w MemDump informuje, kiedy „dźwięk” zajmuje o wiele więcej miejsca, niż się spodziewałeś.
C ++ jest zaprojektowany z myślą o RAII. Myślę, że nie ma lepszego sposobu na zarządzanie pamięcią w C ++. Uważaj jednak, aby nie przydzielać bardzo dużych porcji (takich jak obiekty bufora) w zakresie lokalnym. Może to spowodować przepełnienie stosu, a jeśli jest błąd w sprawdzaniu granic podczas korzystania z tego fragmentu, możesz nadpisać inne zmienne lub zwrócić adresy, co prowadzi do wszelkiego rodzaju luk w zabezpieczeniach.
Jednym z jedynych przykładów przydzielania i niszczenia w różnych miejscach jest tworzenie wątków (przekazywany parametr). Ale nawet w tym przypadku jest to łatwe. Oto funkcja / metoda tworzenia wątku:
struct myparams {
int x;
std::vector<double> z;
}
std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...
Tutaj zamiast funkcji wątku
extern "C" void* th_func(void* p) {
try {
std::auto_ptr<myparams> param((myparams*)p);
...
} catch(...) {
}
return 0;
}
Dość łatwo, prawda? W przypadku niepowodzenia tworzenia wątku zasób zostanie zwolniony (usunięty) przez auto_ptr, w przeciwnym razie własność zostanie przekazana do wątku. Co jeśli wątek jest tak szybki, że po utworzeniu zwalnia zasób przed rozszerzeniem
param.release();
jest wywoływany w głównej funkcji / metodzie? Nic! Ponieważ „powiemy” auto_ptr, aby zignorował cofnięcie alokacji. Czy zarządzanie pamięcią w C ++ jest łatwe, prawda? Twoje zdrowie,
Ema!
Zarządzaj pamięcią w taki sam sposób, jak zarządzasz innymi zasobami (uchwytami, plikami, połączeniami bazy danych, gniazdami ...). GC też ci nie pomoże.
Dokładnie jeden zwrot z dowolnej funkcji. W ten sposób możesz tam dokonać zwolnienia i nigdy tego nie przegapić.
W przeciwnym razie zbyt łatwo popełnić błąd:
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.