Java ma automatyczny GC, który co jakiś czas zatrzymuje świat, ale dba o śmieci na stosie. Teraz aplikacje C / C ++ nie mają tych zawieszeń STW, ich użycie pamięci również nie rośnie nieskończenie. Jak osiąga się to zachowanie? Jak zajmowane są martwe przedmioty?
c++
c
garbage-collection
Ju Shua
źródło
źródło
new
.What happens to garbage in C++?
Czy zwykle nie jest kompilowany do pliku wykonywalnego?Odpowiedzi:
Programista jest odpowiedzialny za zapewnienie, że obiekty, które utworzyli,
new
zostaną usunięte przezdelete
. Jeśli obiekt zostanie utworzony, ale nie zostanie zniszczony przed ostatnim wskaźnikiem lub odniesieniem do niego, który wykracza poza zakres, spada przez pęknięcia i staje się wyciekiem pamięci .Na nieszczęście dla C, C ++ i innych języków, które nie zawierają GC, to po prostu się zbiera. Może to spowodować brak pamięci w aplikacji lub systemie i uniemożliwić przydzielenie nowych bloków pamięci. W tym momencie użytkownik musi uciekać się do zakończenia aplikacji, aby system operacyjny mógł odzyskać wykorzystaną pamięć.
Jeśli chodzi o złagodzenie tego problemu, istnieje kilka rzeczy, które znacznie ułatwiają życie programisty. Są one przede wszystkim poparte charakterem zakresu .
Tutaj stworzyliśmy dwie zmienne. Istnieją w zakresie bloków , zgodnie z definicją
{}
nawiasów klamrowych. Gdy wykonanie wykracza poza ten zakres, obiekty te zostaną automatycznie usunięte. W tym przypadku,variableThatIsAPointer
jak sama nazwa wskazuje, jest wskaźnikiem do obiektu w pamięci. Gdy wykracza poza zakres, wskaźnik jest usuwany, ale obiekt, na który wskazuje, pozostaje. Tutaj mydelete
ten obiekt, zanim wyjdzie poza zakres, aby upewnić się, że nie ma wycieku pamięci. Mogliśmy jednak przekazać ten wskaźnik gdzie indziej i oczekiwać, że zostanie on później usunięty.Ten charakter zakresu rozciąga się na klasy:
Tutaj obowiązuje ta sama zasada. Nie musimy się martwić,
bar
kiedyFoo
zostanie usunięty. JednakotherBar
tylko wskaźnik jest usuwany. JeśliotherBar
jest to jedyny poprawny wskaźnik do dowolnego obiektu, na który wskazuje, prawdopodobnie powinniśmydelete
go użyć wFoo
destruktorze. To jest koncepcja prowadzenia RAIIRAII jest także typową siłą napędową Smart Pointers . W C ++ Standard Library, są
std::shared_ptr
,std::unique_ptr
istd::weak_ptr
; chociaż widziałem i korzystałem z innychshared_ptr
/weak_ptr
wdrożeń zgodnych z tymi samymi koncepcjami. W tym przypadku licznik referencyjny śledzi liczbę wskaźników dla danego obiektu i automatyczniedelete
s obiekt, gdy nie będzie już żadnych odniesień do niego.Poza tym wszystko sprowadza się do odpowiednich praktyk i dyscypliny dla programisty, aby upewnić się, że jego kod poprawnie obsługuje obiekty.
źródło
delete
- właśnie tego szukałem. Niesamowite.delete
Jest wywoływana automatycznie dla Ciebie przez inteligentne kursory , jeśli ich używać tak, należy rozważyć użycie je za każdym razem, gdy automatyczne przechowywanie nie może być używany.delete
kodu aplikacji (i począwszy od C ++ 14, to samo znew
), ale zamiast tego użyj inteligentnych wskaźników i RAII, aby usunąć obiekty sterty.std::unique_ptr
rodzaj istd::make_unique
funkcja są bezpośrednim, najprostszym zamiennikiemnew
idelete
na poziomie kodu aplikacji.C ++ nie ma funkcji wyrzucania elementów bezużytecznych.
Aplikacje C ++ są wymagane do usuwania własnych śmieci.
Programiści aplikacji C ++ muszą to zrozumieć.
Gdy zapomną, wynik nazywany jest „wyciekiem pamięci”.
źródło
new
idelete
.malloc
ifree
lubnew[]
idelete[]
lub innych podzielniki (jak windowsGlobalAlloc
,LocalAlloc
,SHAlloc
,CoTaskMemAlloc
,VirtualAlloc
,HeapAlloc
, ...) i pamięci przydzielonej dla Ciebie (na przykład za pomocąfopen
).W C, C ++ i innych systemach bez Garbage Collectora, język i jego biblioteki oferują programistom funkcje wskazujące, kiedy można odzyskać pamięć.
Najbardziej podstawowym urządzeniem jest automatyczne przechowywanie . Wiele razy sam język zapewnia, że przedmioty są usuwane:
W takich przypadkach kompilator odpowiada za informację, kiedy te wartości są nieużywane, i odzyskuje związane z nimi miejsce.
Podczas używania pamięci dynamicznej w C pamięć jest tradycyjnie przydzielana
malloc
i odzyskiwana za pomocąfree
. W C ++ pamięć jest tradycyjnie przydzielananew
i odzyskiwana za pomocądelete
.C przez lata niewiele się zmieniło, jednak współczesne C ++ całkowicie unika
new
idelete
całkowicie opiera się na bibliotekach (które same używająnew
idelete
odpowiednio):std::unique_ptr
istd::shared_ptr
std::string
,std::vector
,std::map
, ... wszystko wewnętrznie zarządzać dynamicznie przydzielane pamięci przejrzyścieMówiąc o
shared_ptr
, istnieje ryzyko: jeśli powstanie cykl odniesień, a nie zostanie on przerwany, może dojść do wycieku pamięci. To do dewelopera należy uniknięcie tej sytuacji, najprostszym sposobem jestshared_ptr
całkowite uniknięcie, a drugim najprostszym jest uniknięcie cykli na poziomie typu.W rezultacie wycieki pamięci nie stanowi problemu w C ++ , nawet dla początkujących użytkowników, jak długo one powstrzymać się od używania
new
,delete
albostd::shared_ptr
. Jest to inaczej niż w przypadku C, gdzie konieczna jest dyscyplina i ogólnie niewystarczająca.Jednak odpowiedź ta nie byłaby kompletna bez wspomnienia o siostrze bliźniaczki wycieków pamięci: zwisających wskaźników .
Zwisający wskaźnik (lub zwisające odniesienie) jest zagrożeniem tworzonym przez trzymanie wskaźnika lub odniesienia do martwego obiektu. Na przykład:
Użycie wiszącego wskaźnika lub odwołania jest zachowaniem niezdefiniowanym . Ogólnie na szczęście jest to natychmiastowa awaria; niestety często powoduje to najpierw uszkodzenie pamięci ... i od czasu do czasu pojawia się dziwne zachowanie, ponieważ kompilator emituje naprawdę dziwny kod.
Niezdefiniowane zachowanie jest największym problemem z C i C ++ do dziś, pod względem bezpieczeństwa / poprawności programów. Możesz sprawdzić Rust dla języka bez Garbage Collectora i bez niezdefiniowanego zachowania.
źródło
new
,delete
ishared_ptr
”; beznew
ishared_ptr
masz bezpośrednią własność, więc nie ma wycieków. Oczywiście prawdopodobnie będziesz miał zwisające wskaźniki itp., Ale obawiam się, że musisz opuścić C ++, aby się ich pozbyć.C ++ ma tę funkcję o nazwie RAII . Zasadniczo oznacza to, że śmieci są czyszczone podczas pracy, zamiast pozostawiać je w stosie i pozwolić sprzątaczowi posprzątać po tobie. (wyobraźcie sobie mnie w swoim pokoju oglądającym piłkę nożną - gdy piję puszki piwa i potrzebuję nowych, sposób C ++ polega na zabraniu pustej puszki do kosza w drodze do lodówki, sposób C # polega na zrzuceniu jej na podłogę i poczekaj, aż pokojówka je odbierze, kiedy przyjdzie zrobić sprzątanie).
Teraz można przeciekać pamięć w C ++, ale aby to zrobić, musisz pozostawić zwykłe konstrukcje i powrócić do sposobu robienia rzeczy w C - przydzielanie bloku pamięci i śledzenie, gdzie ten blok jest bez pomocy języka. Niektóre osoby zapominają o tym wskaźniku i dlatego nie mogą usunąć bloku.
źródło
Należy zauważyć, że w przypadku C ++ powszechne jest błędne przekonanie, że „trzeba ręcznie zarządzać pamięcią”. W rzeczywistości zwykle nie wykonuje się zarządzania pamięcią w kodzie.
Obiekty o stałym rozmiarze (z okresem użytkowania zakresu)
W zdecydowanej większości przypadków, gdy potrzebujesz obiektu, obiekt będzie miał określony czas życia w twoim programie i zostanie utworzony na stosie. Działa to dla wszystkich wbudowanych prymitywnych typów danych, ale także dla instancji klas i struktur:
Obiekty stosu są automatycznie usuwane po zakończeniu funkcji. W Javie obiekty są zawsze tworzone na stercie i dlatego muszą być usuwane przez jakiś mechanizm, taki jak wyrzucanie elementów bezużytecznych. Nie dotyczy to obiektów stosu.
Obiekty zarządzające danymi dynamicznymi (z okresem istnienia zakresu)
Korzystanie z miejsca na stosie działa dla obiektów o stałym rozmiarze. Gdy potrzebujesz zmiennej ilości miejsca, na przykład tablicy, stosuje się inne podejście: Lista jest umieszczana w obiekcie o stałej wielkości, który zarządza pamięcią dynamiczną. Działa to, ponieważ obiekty mogą mieć specjalną funkcję czyszczenia, destruktor. Jest gwarantowane, że zostanie wywołany, gdy obiekt wykracza poza zasięg i robi coś przeciwnego do konstruktora:
W kodzie, w którym pamięć jest używana, nie ma żadnego zarządzania pamięcią. Jedyne, co musimy upewnić się, to to, że obiekt, który napisaliśmy, ma odpowiedni destruktor. Bez względu na to, jak opuszczymy zakres
listTest
, czy to poprzez wyjątek, czy po prostu przez powrót z niego, destruktor~MyList()
zostanie wywołany i nie będziemy musieli zarządzać żadną pamięcią.(Wydaje mi się, że użycie binarnego operatora NOT w
~
celu wskazania niszczyciela jest zabawną decyzją projektową . Gdy jest stosowany w liczbach, odwraca bity; tutaj analogicznie wskazuje, że to, co zrobił konstruktor, jest odwrócone.)Zasadniczo wszystkie obiekty C ++, które potrzebują pamięci dynamicznej, używają tej enkapsulacji. Nazywa się to RAII („pozyskiwanie zasobów to inicjalizacja”), co jest dość dziwnym sposobem wyrażenia prostej idei, że obiekty dbają o własną zawartość; to, co nabywają, należy do posprzątania.
Obiekty polimorficzne i żywotność poza zasięgiem
Teraz oba te przypadki dotyczyły pamięci, która ma jasno określony czas życia: czas życia jest taki sam jak zakres. Jeśli nie chcemy, aby obiekt wygasał po opuszczeniu zakresu, istnieje trzeci mechanizm, który może zarządzać dla nas pamięcią: inteligentny wskaźnik. Wskaźniki inteligentne są również używane, gdy masz instancje obiektów, których typ zmienia się w czasie wykonywania, ale które mają wspólny interfejs lub klasę podstawową:
Istnieje inny rodzaj inteligentnego wskaźnika
std::shared_ptr
do współdzielenia obiektów między kilkoma klientami. Usuwają zawarty obiekt tylko wtedy, gdy ostatni klient wykracza poza zakres, więc można ich używać w sytuacjach, w których nie wiadomo, ilu będzie klientów i jak długo będą używać obiektu.Podsumowując, widzimy, że tak naprawdę nie wykonuje się żadnego ręcznego zarządzania pamięcią. Wszystko jest hermetyzowane, a następnie załatwiane za pomocą całkowicie automatycznego, opartego na zakresie zarządzania pamięcią. W przypadkach, gdy to nie wystarczy, używane są inteligentne wskaźniki, które zamykają surową pamięć.
Uznaje się za bardzo złą praktykę używanie surowych wskaźników jako właścicieli zasobów w dowolnym miejscu w kodzie C ++, surowe alokacje poza konstruktorami i surowe
delete
wywołania poza destrukterami, ponieważ są one prawie niemożliwe do zarządzania, gdy wystąpią wyjątki, i generalnie są trudne do bezpiecznego użycia.Najlepsze: działa dla wszystkich rodzajów zasobów
Jedną z największych zalet RAII jest to, że nie ogranicza się do pamięci. W rzeczywistości zapewnia bardzo naturalny sposób zarządzania zasobami, takimi jak pliki i gniazda (otwieranie / zamykanie) oraz mechanizmy synchronizacji, takie jak muteksy (blokowanie / odblokowywanie). Zasadniczo każdy zasób, który można uzyskać i musi zostać zwolniony, jest zarządzany w C ++ dokładnie w ten sam sposób i żadne z tych zarządzania nie jest pozostawione użytkownikowi. Wszystko jest zamknięte w klasach, które nabywają w konstruktorze i uwalniają w destruktorze.
Na przykład funkcja blokująca muteks jest zwykle napisana w C ++ w następujący sposób:
Inne języki sprawiają, że jest to o wiele bardziej skomplikowane, wymagając od ciebie zrobienia tego ręcznie (np. W
finally
klauzuli) lub rodzą wyspecjalizowane mechanizmy, które rozwiązują ten problem, ale nie w szczególnie elegancki sposób (zwykle w późniejszym okresie życia, gdy wystarczająca liczba ludzi ma cierpiał z powodu wady). Takimi mechanizmami są try-with-resources w Javie i instrukcja using w C #, które są przybliżeniami RAII C ++.Podsumowując, wszystko to było bardzo powierzchownym kontem RAII w C ++, ale mam nadzieję, że pomoże czytelnikom zrozumieć, że zarządzanie pamięcią, a nawet zasobami w C ++ nie jest zwykle „ręczne”, ale w rzeczywistości w większości automatyczne.
źródło
delete
a nie żyjesz” odpowiedzi gwałtownie przekraczają 30 punktów i zostają zaakceptowane, podczas gdy ten ma pięć. Czy ktoś faktycznie używa tutaj C ++?W szczególności w odniesieniu do języka C język nie zapewnia narzędzi do zarządzania pamięcią przydzielaną dynamicznie. Jesteś absolutnie odpowiedzialny za upewnienie się, że każdy
*alloc
mafree
gdzieś odpowiedni .Sprawy stają się naprawdę paskudne, gdy alokacja zasobów nie powiedzie się w połowie; czy spróbujesz ponownie, czy wycofujesz się i zaczynasz od początku, czy wycofujesz się i wychodzisz z błędem, czy po prostu wpłacasz kaucję i pozwalasz systemowi operacyjnemu sobie z tym poradzić?
Na przykład, tutaj jest funkcja przydzielania nieciągłej tablicy 2D. Zachowanie polega na tym, że jeśli błąd alokacji wystąpi w połowie procesu, przywracamy wszystko do tyłu i zwracamy wskazanie błędu za pomocą wskaźnika NULL:
Ten kod jest brzydki z tymi kodami
goto
, ale przy braku jakiegokolwiek strukturalnego mechanizmu obsługi wyjątków, jest to właściwie jedyny sposób radzenia sobie z problemem bez po prostu całkowitego ratowania, szczególnie jeśli kod alokacji zasobów jest zagnieżdżony bardziej głębiej niż jedna pętla. Jest to jeden z niewielu przypadków, kiedygoto
jest to rzeczywiście atrakcyjna opcja; w przeciwnym razie korzystasz z wielu flag i dodatkowychif
instrukcji.Możesz ułatwić sobie życie, pisząc dedykowane funkcje alokatora / dezalokatora dla każdego zasobu, coś w rodzaju
źródło
goto
stwierdzeń. Jest to zalecana praktyka w niektórych obszarach. Jest to powszechnie stosowany schemat ochrony przed odpowiednikiem wyjątków w C. Spójrz na kod jądra Linuksa, który jest pełengoto
instrukcji - i który nie przecieka.goto
jest obcy. Byłoby bardziej czytelne, jeśli zmieniłeś sięgoto done;
nareturn arr;
iarr=NULL;done:return arr;
nareturn NULL;
. Chociaż w bardziej skomplikowanych przypadkach może istnieć wielegoto
s, zaczynają się rozwijać na różnych poziomach gotowości (co można zrobić poprzez odwijanie stosu wyjątków w C ++).Nauczyłem się klasyfikować problemy z pamięcią na kilka różnych kategorii.
Raz kapie. Załóżmy, że program przecieka 100 bajtów podczas uruchamiania, ale już nigdy nie wycieknie. Ściganie i eliminowanie jednorazowych wycieków jest przyjemne (lubię mieć czysty raport dzięki możliwości wykrywania wycieków), ale nie jest konieczne. Czasami są większe problemy, które należy zaatakować.
Powtarzające się wycieki. Funkcja, która jest wywoływana w sposób powtarzalny w trakcie trwania programu, która regularnie przecieka pamięć, stanowi duży problem. Te krople będą torturować program, a być może i system operacyjny, na śmierć.
Wzajemne odniesienia Jeśli obiekty A i B odwołują się do siebie za pośrednictwem wspólnych wskaźników, musisz zrobić coś specjalnego, albo w projektowaniu tych klas, albo w kodzie, który implementuje / używa tych klas w celu przerwania cykliczności. (Nie jest to problem w przypadku języków, w których śmieci są gromadzone).
Za dużo pamiętam. To zły kuzyn wycieków śmieci / pamięci. RAII tu nie pomoże, podobnie jak zbieranie śmieci. Jest to problem w dowolnym języku. Jeśli jakaś aktywna zmienna ma ścieżkę, która łączy ją z jakąś losową porcją pamięci, ta losowa porcja pamięci nie jest śmieciem. Sprawienie, by program stał się zapomniany, aby mógł działać przez kilka dni, jest trudne. Stworzenie programu, który może działać przez kilka miesięcy (np. Do momentu awarii dysku) jest bardzo, bardzo trudne.
Od dłuższego czasu nie miałem poważnego problemu z wyciekami. Korzystanie z RAII w C ++ bardzo pomaga rozwiązać problem kapania i wycieków. (Trzeba jednak uważać na wspólne wskaźniki.) Co ważniejsze, miałem problemy z aplikacjami, których użycie pamięci stale rośnie i rośnie i rośnie z powodu nieprzerwanego połączenia z pamięcią, która nie jest już używana.
źródło
Do programisty C ++ należy zaimplementowanie własnej formy odśmiecania, jeśli to konieczne. Nieprzestrzeganie tego spowoduje „wyciek pamięci”. Języki „wysokiego poziomu” (takie jak Java) mają dość wbudowane funkcje wyrzucania elementów bezużytecznych, ale języki „niskiego poziomu”, takie jak C i C ++, nie.
źródło