Co dzieje się ze śmieciami w C ++?

51

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?

Ju Shua
źródło
38
Uwaga: stop-the-world jest wyborem implementacyjnym niektórych śmieciarek, ale na pewno nie wszystkich. Istnieją na przykład współbieżne GC, które działają równolegle z mutatorem (tak nazywają rzeczywisty program GC). Wierzę, że można kupić komercyjną wersję oprogramowania JVM J9 IBM typu open source, która ma równoległy, nieprzerwany moduł zbierający. Azul Zing ma „pauzujący” kolektor, który w rzeczywistości nie jest pauzujący, ale niezwykle szybki, dzięki czemu nie ma zauważalnych przerw (jego przerwy GC są w tej samej kolejności co przełącznik kontekstowy wątku systemu operacyjnego, który zwykle nie jest postrzegany jako przerwa) .
Jörg W Mittag
14
Większość (długoterminowych) programów C ++, z których korzystam , korzysta z pamięci, która z czasem rośnie bez ograniczeń. Czy to możliwe, że nie masz zwyczaju pozostawiać programów otwartych na dłużej niż kilka dni?
Jonathan Cast
12
Weź pod uwagę, że w nowoczesnym C ++ i jego konstrukcjach nie musisz już ręcznie usuwać pamięci (chyba że masz specjalną optymalizację), ponieważ możesz zarządzać pamięcią dynamiczną za pomocą inteligentnych wskaźników. Oczywiście, dodaje to trochę nakładów na programowanie w C ++ i musisz być trochę bardziej ostrożny, ale to nie jest zupełnie inna rzecz, musisz tylko pamiętać, aby używać inteligentnej konstrukcji wskaźnika zamiast po prostu wywoływać instrukcję new.
Andy
9
Pamiętaj, że nadal można mieć wycieki pamięci w języku, w którym gromadzone są śmieci. Nie znam Java, ale przecieki pamięci są niestety dość powszechne w zarządzanym świecie GC .NET. Obiekty, do których pośrednio odwołuje się pole statyczne, nie są automatycznie gromadzone, procedury obsługi zdarzeń są bardzo częstym źródłem wycieków, a niedeterministyczny charakter odśmiecania sprawia, że ​​nie jest w stanie całkowicie uniknąć konieczności ręcznego zwalniania zasobów (co prowadzi do IDisposable wzór). Podsumowując, właściwie stosowany model zarządzania pamięcią C ++ jest znacznie lepszy niż wyrzucanie elementów bezużytecznych.
Cody Gray
26
What happens to garbage in C++? Czy zwykle nie jest kompilowany do pliku wykonywalnego?
BJ Myers

Odpowiedzi:

100

Programista jest odpowiedzialny za zapewnienie, że obiekty, które utworzyli, newzostaną usunięte przez delete. 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 .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

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, variableThatIsAPointerjak 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 my deleteten 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:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

Tutaj obowiązuje ta sama zasada. Nie musimy się martwić, barkiedy Foozostanie usunięty. Jednak otherBartylko wskaźnik jest usuwany. Jeśli otherBarjest to jedyny poprawny wskaźnik do dowolnego obiektu, na który wskazuje, prawdopodobnie powinniśmy deletego użyć w Foodestruktorze. To jest koncepcja prowadzenia RAII

alokacja zasobów (pozyskiwanie) odbywa się podczas tworzenia obiektu (konkretnie inicjalizacji) przez konstruktora, podczas gdy dezalokacja zasobów (uwalnianie) odbywa się podczas niszczenia obiektu (konkretnie finalizacji) przez destruktor. W ten sposób gwarantuje się, że zasób będzie przechowywany pomiędzy zakończeniem inicjalizacji a rozpoczęciem finalizacji (utrzymywanie zasobów jest niezmiennikiem klasowym) i będzie przechowywany tylko wtedy, gdy obiekt będzie żył. Zatem jeśli nie ma wycieków obiektów, nie ma wycieków zasobów.

RAII jest także typową siłą napędową Smart Pointers . W C ++ Standard Library, są std::shared_ptr, std::unique_ptri std::weak_ptr; chociaż widziałem i korzystałem z innych shared_ptr/ weak_ptrwdrożeń zgodnych z tymi samymi koncepcjami. W tym przypadku licznik referencyjny śledzi liczbę wskaźników dla danego obiektu i automatycznie deletes 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.

Thebluefish
źródło
4
usunięty przez delete- właśnie tego szukałem. Niesamowite.
Ju Shua
3
Możesz dodać o mechanizmach określania zakresu dostarczonych w c ++, które pozwalają, aby większość nowych i usuwanie były w większości automatyczne.
whatsisname
9
@ Whatsisname nie jest tak, że nowe i usuwane są automatycznie, ale w wielu przypadkach w ogóle nie występują
Caleth
10
deleteJest 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.
Marian Spanik,
11
@JuShua Zauważ, że pisząc nowoczesny C ++, nigdy nie powinieneś mieć potrzeby posiadania deletekodu aplikacji (i począwszy od C ++ 14, to samo z new), ale zamiast tego użyj inteligentnych wskaźników i RAII, aby usunąć obiekty sterty. std::unique_ptrrodzaj i std::make_uniquefunkcja są bezpośrednim, najprostszym zamiennikiem newi deletena poziomie kodu aplikacji.
hyde
82

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”.

John R. Strohm
źródło
22
Na pewno upewniłeś się, że twoja odpowiedź nie zawiera śmieci, ani
płyty kotłowej
15
@leftaroundabout: Dziękuję. Uważam to za komplement.
John R. Strohm,
1
OK, ta pozbawiona śmieci odpowiedź zawiera słowo kluczowe do wyszukania: wyciek pamięci. Byłoby również miło wspomnieć newi delete.
Ruslan
4
@Ruslan To samo odnosi się również do malloci freelub new[]i delete[]lub innych podzielniki (jak windows GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ...) i pamięci przydzielonej dla Ciebie (na przykład za pomocą fopen).
user253751,
43

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:

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

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 malloci odzyskiwana za pomocą free. W C ++ pamięć jest tradycyjnie przydzielana newi odzyskiwana za pomocą delete.

C przez lata niewiele się zmieniło, jednak współczesne C ++ całkowicie unika newi deletecałkowicie opiera się na bibliotekach (które same używają newi deleteodpowiednio):

  • inteligentne wskaźniki są najbardziej znane: std::unique_ptristd::shared_ptr
  • ale pojemniki są znacznie bardziej rozpowszechnione rzeczywiście: std::string, std::vector, std::map, ... wszystko wewnętrznie zarządzać dynamicznie przydzielane pamięci przejrzyście

Mó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 jest shared_ptrcał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, deletealbo std::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:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

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.

Matthieu M.
źródło
17
Re: „Użycie zwisającego wskaźnika lub odwołania jest zachowaniem niezdefiniowanym . Na szczęście na ogół jest to natychmiastowa awaria”: Naprawdę? To w ogóle nie pasuje do mojego doświadczenia; wręcz przeciwnie, moje doświadczenie jest takie, że użycie zwisającego wskaźnika prawie nigdy nie powoduje natychmiastowej awarii. . .
ruakh
9
Tak, ponieważ aby być „wiszącym”, wskaźnik musi celować w wcześniej przydzieloną pamięć w jednym punkcie, a pamięć ta zwykle nie jest całkowicie odwzorowana z procesu, tak że nie jest już w ogóle dostępna, ponieważ będzie to dobry kandydat do natychmiastowego ponownego użycia ... w praktyce zwisające wskaźniki nie powodują awarii, powodują chaos.
Leushenko
2
„W wyniku tego wycieki pamięci nie stanowią problemu w C ++”. Pewnie, że zawsze istnieją powiązania C z bibliotekami do zepsucia, a także rekurencyjne share_ptrs lub nawet rekurencyjne unikalne_ptrs i inne sytuacje.
Mooing Duck
3
„Nie jest to problem w C ++, nawet dla nowych użytkowników” - kwalifikowałbym to do „nowych użytkowników, którzy nie pochodzą z języka Java lub C ”.
lewo wokół
3
@leftaroundabout: kwalifikuje się „dopóki nie będą używać new, deletei shared_ptr”; bez newi shared_ptrmasz 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ć.
Matthieu M.,
27

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.

gbjbaanb
źródło
9
Współdzielone wskaźniki (które wykorzystują RAII) zapewniają nowoczesny sposób tworzenia wycieków. Załóżmy, że obiekty A i B odnoszą się do siebie nawzajem za pośrednictwem wspólnych wskaźników i nic innego nie odwołuje się do obiektu A lub obiektu B. W rezultacie powstaje wyciek. To wzajemne odniesienie nie jest problemem w językach z odśmiecaniem.
David Hammen,
@DavidHammen na pewno, ale dla pewności kosztem przemierzenia prawie każdego obiektu. Twój przykład inteligentnych wskaźników ignoruje fakt, że sam inteligentny wskaźnik wykracza poza zasięg, a następnie obiekty zostaną uwolnione. Zakładasz, że inteligentny wskaźnik jest jak wskaźnik, ale nie jest to obiekt, który jest przekazywany na stosie jak większość parametrów. Nie różni się to zbytnio od wycieków pamięci w językach GC. np. słynny, w którym usunięcie procedury obsługi zdarzeń z klasy interfejsu użytkownika pozostawia w niej ciche odniesienie i dlatego jest nieszczelne.
gbjbaanb
1
@ gbjbaanb w przykładzie z inteligentnymi wskaźnikami, żaden inteligentny wskaźnik nigdy nie wykracza poza zakres, dlatego jest przeciek. Ponieważ oba inteligentne obiekty wskaźnikowe są przydzielane w zakresie dynamicznym , a nie leksykalnym, każdy z nich próbuje poczekać na drugi przed zniszczeniem. Fakt, że inteligentne wskaźniki są prawdziwymi obiektami w C ++, a nie tylko wskaźnikami, jest dokładnie tym, co powoduje wyciek - dodatkowe inteligentne obiekty wskaźnikowe w zakresach stosów, które również wskazują na obiekty kontenerowe, nie mogą ich zwolnić, gdy same się zniszczą, ponieważ przeliczenie jest niezerowa.
Leushenko
2
Sposób .NET to nie rzucać go na podłogę. Po prostu utrzymuje to tam, gdzie było, dopóki nie pojawi się pokojówka. A ze względu na sposób, w jaki .NET przydziela pamięć w praktyce (nie kontraktowy), sterta przypomina bardziej stos o swobodnym dostępie. To tak, jakby mieć stos kontraktów i dokumentów i od czasu do czasu przeglądać te, które już nie są ważne. Aby to ułatwić, te, które przetrwają każde odrzucenie, są awansowane na inny stos, dzięki czemu można uniknąć przerzucania wszystkich stosów przez większość czasu - chyba że pierwszy stos jest wystarczająco duży, pokojówka nie dotyka pozostałych.
Luaan,
@Luaan, to była analogia ... Myślę, że byłbyś szczęśliwszy, gdybym powiedział, że zostawia puszki leżące na stole, dopóki pokojówka nie posprząta.
gbjbaanb
26

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:

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

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:

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

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ą:

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

Istnieje inny rodzaj inteligentnego wskaźnika std::shared_ptrdo 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 deletewywoł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:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

Inne języki sprawiają, że jest to o wiele bardziej skomplikowane, wymagając od ciebie zrobienia tego ręcznie (np. W finallyklauzuli) 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.

Felix Dombek
źródło
7
To jedyna odpowiedź, która nie wprowadza w błąd ludzi ani nie maluje C ++ trudniejszego lub bardziej niebezpiecznego niż jest w rzeczywistości.
Alexander Revo,
6
BTW, za surową wskazówkę jako właścicieli zasobów uważa się jedynie złą praktykę. Nie ma nic złego w ich użyciu, jeśli wskazują na coś, co gwarantuje przetrwanie samego wskaźnika.
Alexander Revo,
8
Popieram Aleksandra. Jestem zaskoczony, że „C ++ nie ma zautomatyzowanego zarządzania pamięcią, zapomnij, deletea nie żyjesz” odpowiedzi gwałtownie przekraczają 30 punktów i zostają zaakceptowane, podczas gdy ten ma pięć. Czy ktoś faktycznie używa tutaj C ++?
Quentin,
8

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 *allocma freegdzieś 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:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

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, kiedy gotojest to rzeczywiście atrakcyjna opcja; w przeciwnym razie korzystasz z wielu flag i dodatkowych ifinstrukcji.

Możesz ułatwić sobie życie, pisząc dedykowane funkcje alokatora / dezalokatora dla każdego zasobu, coś w rodzaju

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}
John Bode
źródło
1
To dobra odpowiedź, nawet w przypadku gotostwierdzeń. 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łen gotoinstrukcji - i który nie przecieka.
David Hammen,
„bez po prostu ratowania całkowicie” -> w uczciwości, jeśli chcesz porozmawiać o C, jest to prawdopodobnie dobra praktyka. C jest językiem najlepiej używanym do obsługi bloków pamięci pochodzących z innego miejsca lub rozdzielania małych fragmentów pamięci na inne procedury, ale najlepiej nie wykonując obu jednocześnie w sposób przeplatany. Jeśli używasz klasycznych „obiektów” w C, prawdopodobnie nie używasz języka do jego mocnych stron.
Leushenko
Drugi gotojest obcy. Byłoby bardziej czytelne, jeśli zmieniłeś się goto done;na return arr;i arr=NULL;done:return arr;na return NULL;. Chociaż w bardziej skomplikowanych przypadkach może istnieć wiele gotos, zaczynają się rozwijać na różnych poziomach gotowości (co można zrobić poprzez odwijanie stosu wyjątków w C ++).
Ruslan
2

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.

David Hammen
źródło
-6

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.

xDr_Johnx
źródło