Was passiert mit Müll in C ++?

51

Java hat eine automatische GC, die hin und wieder die Welt stoppt, sich aber um den Müll auf einem Haufen kümmert. Jetzt haben C / C ++ - Anwendungen diese STW-Einfrierungen nicht mehr, und auch die Speichernutzung nimmt nicht unendlich zu. Wie wird dieses Verhalten erreicht? Wie werden die toten Gegenstände gepflegt?

Ju Shua
quelle
38
Hinweis: stop-the-world ist eine Implementierungsoption einiger Garbage Collectors, aber sicherlich nicht aller. Es gibt zum Beispiel gleichzeitige GCs, die gleichzeitig mit dem Mutator ausgeführt werden (so nennen GC-Entwickler das eigentliche Programm). Ich glaube, Sie können eine kommerzielle Version von IBMs Open-Source-JVM J9 kaufen, die gleichzeitig einen pausenlosen Sammler hat. Azul Zing hat einen „pauseless“ Sammler , die nicht ist eigentlich pauseless aber extrem schnell , so dass es keine sind spürbar Pausen (seine GC Pausen sind auf der gleichen Reihenfolge wie ein Betriebssystem Thread - Kontextschalter, der in der Regel nicht als Pause zu sehen) .
Jörg W Mittag
14
Die meisten der von mir verwendeten (lang laufenden) C ++ - Programme haben eine Speicherauslastung, die mit der Zeit unbegrenzt zunimmt. Ist es möglich, dass Sie es nicht gewohnt sind, Programme länger als ein paar Tage offen zu lassen?
Jonathan Cast
12
Berücksichtigen Sie, dass Sie mit modernem C ++ und seinen Konstrukten den Speicher nicht mehr manuell löschen müssen (es sei denn, Sie benötigen eine spezielle Optimierung), da Sie den dynamischen Speicher über intelligente Zeiger verwalten können. Offensichtlich erhöht sich der Aufwand für die C ++ - Entwicklung, und Sie müssen ein wenig vorsichtiger sein, aber es ist keine ganz andere Sache. Sie müssen nur daran denken, das Smart Pointer-Konstrukt zu verwenden, anstatt nur manual aufzurufen new.
Andy
9
Beachten Sie, dass es in einer müllsammelnden Sprache immer noch zu Speicherverlusten kommen kann. Ich bin mit Java nicht vertraut, aber Speicherverluste sind in der verwalteten GC-Welt von .NET leider weit verbreitet. Objekte, auf die indirekt durch ein statisches Feld verwiesen wird, werden nicht automatisch erfasst. Ereignishandler sind eine sehr häufige Quelle für Lecks, und die nicht deterministische Art der Speicherbereinigung macht es nicht möglich, die Notwendigkeit der manuellen Freigabe von Ressourcen (die zum IDisposable führt) vollständig zu umgehen Muster). Alles in allem ist das C ++ - Speicherverwaltungsmodell der Garbage Collection weit überlegen.
Cody Grey
26
What happens to garbage in C++? Wird es normalerweise nicht in eine ausführbare Datei kompiliert?
BJ Myers

Antworten:

100

Der Programmierer ist dafür verantwortlich, dass Objekte, die er über erstellt hat, über newgelöscht werden delete. Wenn ein Objekt erstellt, aber nicht zerstört wird, bevor der letzte Zeiger oder Verweis auf dieses Objekt den Gültigkeitsbereich verlässt, fällt es durch die Risse und wird zu einem Speicherleck .

Unglücklicherweise häufen sich C, C ++ und andere Sprachen, die keinen GC enthalten, mit der Zeit an. Dies kann dazu führen, dass einer Anwendung oder dem System der Speicher ausgeht und neue Speicherblöcke nicht zugeordnet werden können. Zu diesem Zeitpunkt muss der Benutzer auf das Beenden der Anwendung zurückgreifen, damit das Betriebssystem den verwendeten Speicher zurückfordern kann.

Um dieses Problem zu lindern, gibt es verschiedene Dinge, die einem Programmierer das Leben wesentlich erleichtern. Diese werden in erster Linie durch die Art des Geltungsbereichs gestützt .

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

    delete variableThatIsAPointer;
}

Hier haben wir zwei Variablen erstellt. Sie sind im Blockbereich vorhanden , wie durch die {}geschweiften Klammern definiert. Wenn die Ausführung diesen Bereich verlässt, werden diese Objekte automatisch gelöscht. In diesem Fall ist variableThatIsAPointer, wie der Name schon sagt, ein Zeiger auf ein Objekt im Speicher. Wenn der Gültigkeitsbereich überschritten wird, wird der Zeiger gelöscht, das Objekt, auf das er zeigt, bleibt jedoch erhalten. Hier haben wir deletedieses Objekt, bevor es aus dem Rahmen geht, um sicherzustellen, dass es keinen Speicherverlust gibt. Wir hätten diesen Zeiger jedoch auch an anderer Stelle übergeben und damit gerechnet, dass er später gelöscht wird.

Diese Art des Geltungsbereichs erstreckt sich auf Klassen:

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

Hier gilt das gleiche Prinzip. Wir müssen uns keine Sorgen machen, barwann Foogelöscht wird. Es wird jedoch otherBarnur der Zeiger gelöscht. Wenn otherBarder einzige gültige Zeiger auf das Objekt ist, auf das er verweist, sollten wir ihn wahrscheinlich deleteim FooDestruktor haben. Dies ist das Antriebskonzept von RAII

Die Ressourcenzuweisung (Erfassung) erfolgt während der Objekterstellung (insbesondere der Initialisierung) durch den Konstruktor, während die Freigabe (Freigabe) der Ressource während der Objektzerstörung (insbesondere der Finalisierung) durch den Destruktor erfolgt. Auf diese Weise wird garantiert, dass die Ressource zwischen dem Abschluss der Initialisierung und dem Beginn der Finalisierung gehalten wird (das Halten der Ressourcen ist eine Klasseninvariante) und nur gehalten wird, wenn das Objekt lebt. Wenn es also keine Objektlecks gibt, gibt es keine Ressourcenlecks.

RAII ist auch die typische treibende Kraft hinter Smart Pointern . In dem C ++ Standard Library, diese sind std::shared_ptr, std::unique_ptrund std::weak_ptr; obwohl ich andere shared_ptr/ weak_ptrImplementierungen gesehen und verwendet habe , die den gleichen Konzepten folgen. Bei diesen verfolgt ein Referenzzähler, wie viele Zeiger auf ein bestimmtes Objekt vorhanden sind, und deletes das Objekt automatisch, wenn keine Referenzen mehr vorhanden sind.

Darüber hinaus kommt es darauf an, dass ein Programmierer die richtigen Methoden und Disziplinen anwendet, um sicherzustellen, dass sein Code Objekte ordnungsgemäß verarbeitet.

Thebluefish
quelle
4
gelöscht über delete- das ist was ich gesucht habe. Genial.
Ju Shua
3
Möglicherweise möchten Sie Informationen zu den in c ++ bereitgestellten Gültigkeitsbereichsmechanismen hinzufügen, mit denen ein Großteil der neuen und gelöschten Elemente weitgehend automatisch erstellt werden kann.
Whatsisname
9
@whatsisname es ist nicht so, dass Neu und Löschen automatisch erfolgen, es ist, dass sie in vielen Fällen überhaupt nicht auftreten
Caleth
10
Die deletewird von Smart Pointern automatisch aufgerufen, wenn Sie sie verwenden. Sie sollten sie daher jedes Mal verwenden, wenn ein automatischer Speicher nicht verwendet werden kann.
Marian Spanik
11
@JuShua Beachten Sie, dass Sie beim Schreiben von modernem C ++ nicht unbedingt deletein Ihrem Anwendungscode enthalten sein müssen (und ab C ++ 14 dasselbe mit new), sondern stattdessen intelligente Zeiger und RAII verwenden müssen, um Heap-Objekte löschen zu lassen. std::unique_ptrTyp und std::make_uniqueFunktion sind das direkte, einfachste Ersetzen von newund deleteauf Anwendungscodeebene.
Hyde
82

C ++ hat keine Garbage Collection.

C ++ - Anwendungen müssen ihren eigenen Müll entsorgen.

C ++ - Anwendungsprogrammierer müssen dies verstehen.

Wenn sie vergessen, wird das Ergebnis als "Speicherverlust" bezeichnet.

John R. Strohm
quelle
22
Sie haben mit Sicherheit sichergestellt, dass Ihre Antwort weder Müll noch
Heizkessel
15
@leftaroundabout: Danke. Ich betrachte das als Kompliment.
John R. Strohm
1
OK, diese müllfreie Antwort enthält ein Schlüsselwort, nach dem gesucht werden muss: Memory Leak. Es wäre auch schön, irgendwie zu erwähnen , newund delete.
Ruslan
4
@Ruslan Das gleiche gilt auch für mallocund freeoder new[]und delete[]oder irgendwelchen anderen Verteilern (wie Windows ist GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ...) und einen Speicher für Sie zugeordnet (zB über fopen).
user253751
43

In C, C ++ und anderen Systemen ohne Garbage Collector werden dem Entwickler durch die Sprache und ihre Bibliotheken Funktionen angeboten, die angeben, wann Speicher zurückgefordert werden kann.

Die grundlegendste Einrichtung ist die automatische Speicherung . Oft stellt die Sprache selbst sicher, dass Gegenstände entsorgt werden:

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;
}

In diesem Fall muss der Compiler wissen, wann diese Werte nicht verwendet werden, und den damit verbundenen Speicherplatz zurückfordern.

Bei Verwendung des dynamischen Speichers wird in C Speicher traditionell mit zugewiesen mallocund mit zurückgefordert free. In C ++ wird Speicher traditionell mit reserviert newund mit zurückgefordert delete.

C hat sich nicht viel über die Jahre verändert, aber moderne C ++ meidet newund deletevollständig und verlässt sich stattdessen auf Bibliothekseinrichtungen (die selbst nutzen newund deleteangemessen):

  • kluge Zeiger sind die bekanntesten: std::unique_ptrundstd::shared_ptr
  • aber Behälter sind viel weiter verbreitet eigentlich: std::string, std::vector, std::map, ... alle intern verwalten dynamisch zugewiesenen Speicher transparent

Apropos shared_ptr, es besteht die Gefahr: Wenn ein Referenzzyklus gebildet und nicht unterbrochen wird, kann es zu einem Speicherverlust kommen. Es ist Sache des Entwicklers, diese Situation zu vermeiden, wobei die einfachste Möglichkeit darin besteht, sie shared_ptrinsgesamt zu vermeiden, und die zweitgrößte darin, Zyklen auf Typebene zu vermeiden.

Als Ergebnis sind Speicherlecks kein Problem in C ++ verwenden, auch für neue Benutzer, solange sie verzichten new, deleteoder std::shared_ptr. Dies ist anders als bei C, wo eine strenge Disziplin erforderlich und im Allgemeinen unzureichend ist.


Diese Antwort wäre jedoch nicht vollständig, ohne die Zwillingsschwester der Speicherlecks zu erwähnen: baumelnde Zeiger .

Ein baumelnder Zeiger (oder eine baumelnde Referenz) ist eine Gefahr, die dadurch entsteht, dass ein Zeiger oder eine Referenz auf ein Objekt, das tot ist, beibehalten wird. Zum Beispiel:

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";
}

Die Verwendung eines baumelnden Zeigers oder einer Referenz ist undefiniertes Verhalten . Im Allgemeinen handelt es sich glücklicherweise um einen sofortigen Absturz. Leider führt dies häufig zu einer Speicherbeschädigung ... und von Zeit zu Zeit tritt seltsames Verhalten auf, weil der Compiler wirklich seltsamen Code ausgibt.

Undefiniertes Verhalten ist bis heute das größte Problem bei C und C ++ im Hinblick auf die Sicherheit / Korrektheit von Programmen. Vielleicht möchten Sie Rust nach einer Sprache ohne Garbage Collector und ohne undefiniertes Verhalten durchsuchen.

Matthieu M.
quelle
17
Betreff: "Das Verwenden eines baumelnden Zeigers oder einer Referenz ist undefiniertes Verhalten . Im Allgemeinen ist dies glücklicherweise ein sofortiger Absturz." Wirklich? Das passt überhaupt nicht zu meiner Erfahrung; Im Gegenteil, ich habe die Erfahrung gemacht, dass die Verwendung eines baumelnden Zeigers fast nie zu einem sofortigen Absturz führt. . .
Ruakh
9
Ja, da ein Zeiger, um zu "baumeln", einen zuvor zugewiesenen Speicher an einer Stelle haben muss und dieser Speicher normalerweise nicht vollständig aus dem Prozess entfernt wurde, so dass auf ihn überhaupt nicht mehr zugegriffen werden kann, da dies der Fall ist guter Kandidat für die sofortige Wiederverwendung ... in der Praxis verursachen baumelnde Zeiger keine Abstürze, sondern Chaos.
Leushenko
2
"Als Ergebnis sind Speicherlecks in C ++ kein Problem." Natürlich müssen immer C-Bindungen an Bibliotheken sowie rekursive shared_ptrs oder sogar rekursive unique_ptrs und andere Situationen überprüft werden.
Mooing Duck
3
"Kein Problem in C ++, auch für neue Benutzer" - Ich würde dies als "neue Benutzer , die nicht aus einer Java-ähnlichen Sprache oder C stammen " qualifizieren.
links um ca.
3
@leftaroundabout: Es ist qualifiziert, "solange sie es unterlassen new, deleteund shared_ptr"; ohne newund shared_ptrdu hast direkt besitz also keine lecks. Natürlich haben Sie wahrscheinlich baumelnde Zeiger usw., aber ich fürchte, Sie müssen C ++ verlassen, um diese loszuwerden.
Matthieu M.
27

C ++ hat dieses Ding namens RAII . Im Grunde bedeutet dies, dass der Müll beim Gehen aufgeräumt wird, anstatt ihn auf einem Stapel liegen zu lassen und den Reiniger nach Ihnen aufräumen zu lassen. (Stellen Sie sich vor, ich sehe in meinem Zimmer Fußball, während ich Bierdosen trinke und neue brauche. Der C ++ - Weg ist, die leere Dose auf dem Weg zum Kühlschrank zum Mülleimer zu bringen. Der C # -Weg ist, sie auf den Boden zu werfen und warten Sie, bis die Magd sie abgeholt hat, wenn sie zum Putzen kommt).

Jetzt ist es möglich, Speicher in C ++ zu verlieren, aber dazu müssen Sie die üblichen Konstrukte verlassen und zur C-Methode zurückkehren - einen Speicherblock zuweisen und verfolgen, wo sich dieser Block ohne Sprachunterstützung befindet. Einige Leute vergessen diesen Zeiger und können den Block nicht entfernen.

gbjbaanb
quelle
9
Gemeinsame Zeiger (die RAII verwenden) bieten eine moderne Möglichkeit, Lecks zu erzeugen. Angenommen, die Objekte A und B verweisen über gemeinsame Zeiger aufeinander, und nichts anderes verweist auf Objekt A oder Objekt B. Das Ergebnis ist ein Leck. Diese gegenseitige Referenzierung ist in Sprachen mit Garbage Collection kein Problem.
David Hammen
@DavidHammen sicher, aber auf Kosten von fast jedem Objekt zu überqueren, um sicherzugehen. In Ihrem Beispiel für die intelligenten Zeiger wird die Tatsache ignoriert, dass der intelligente Zeiger selbst den Gültigkeitsbereich verlässt und die Objekte dann freigegeben werden. Sie nehmen an, ein intelligenter Zeiger ist wie ein Zeiger, nicht wie ein Objekt, das wie die meisten Parameter auf dem Stapel herumgereicht wird. Dies unterscheidet sich nicht wesentlich von Speicherverlusten, die in GC-Sprachen verursacht werden. ZB die berühmte, bei der ein Ereignishandler aus einer UI-Klasse entfernt wird, ohne dass auf ihn verwiesen wird und daher ein Leck auftritt.
Gbjbaanb
1
@gbjbaanb im Beispiel mit dem Smart - Pointer, weder Smart - Pointer immer geht out of scope, das ist, warum ein Leck gibt. Da beide Smart-Pointer-Objekte in einem dynamischen Bereich, nicht in einem lexikalischen, zugeordnet sind, versuchen sie jeweils, vor dem Zerstören auf den anderen zu warten. Die Tatsache, dass intelligente Zeiger in C ++ echte Objekte und nicht nur Zeiger sind, ist genau das, was das Leck hier verursacht - die zusätzlichen intelligenten Zeigerobjekte in Stapelbereichen, die auch auf die Containerobjekte verweisen, können ihre Zuordnung nicht aufheben, wenn sie sich selbst zerstören, da die Nachzählung erfolgt nicht Null.
Leushenko
2
Der .NET-Weg ist, es nicht auf den Boden zu werfen . Es hält es einfach dort, wo es war, bis das Dienstmädchen vorbeikommt. Und aufgrund der Art und Weise, wie .NET in der Praxis Speicher zuweist (nicht vertraglich), ähnelt der Heap eher einem Stapel mit wahlfreiem Zugriff. Es ist so, als hätte man einen Stapel Verträge und Papiere und würde hin und wieder durchgehen, um diejenigen zu verwerfen, die nicht mehr gültig sind. Und um dies zu vereinfachen, werden diejenigen, die jeden Abwurf überstehen, in einen anderen Stapel befördert, so dass Sie die meiste Zeit vermeiden können, alle Stapel zu durchlaufen. Wenn der erste Stapel nicht groß genug wird, berührt das Dienstmädchen die anderen nicht.
Luaan
@Luaan, es war eine Analogie ... Ich denke, Sie wären glücklicher, wenn ich sagen würde, dass Dosen auf dem Tisch liegen bleiben, bis die Magd kommt, um aufzuräumen.
gbjbaanb
26

Es sollte beachtet werden, dass es im Fall von C ++ ein weit verbreitetes Missverständnis ist, dass "Sie manuelle Speicherverwaltung durchführen müssen". Tatsächlich führen Sie normalerweise keine Speicherverwaltung in Ihrem Code durch.

Objekte mit fester Größe (mit Gültigkeitsdauer des Bereichs)

In den allermeisten Fällen, in denen Sie ein Objekt benötigen, hat das Objekt eine definierte Lebensdauer in Ihrem Programm und wird auf dem Stapel erstellt. Dies funktioniert für alle integrierten primitiven Datentypen, aber auch für Instanzen von Klassen und Strukturen:

class MyObject {
    public: int x;
};

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

Stapelobjekte werden nach Beendigung der Funktion automatisch entfernt. In Java werden Objekte immer auf dem Heap erstellt und müssen daher durch einen Mechanismus wie Garbage Collection entfernt werden. Dies ist für Stapelobjekte kein Problem.

Objekte, die dynamische Daten verwalten (mit Gültigkeitsdauer des Bereichs)

Die Verwendung von Speicherplatz auf dem Stapel funktioniert für Objekte fester Größe. Wenn Sie eine variable Menge an Speicherplatz benötigen, z. B. ein Array, wird ein anderer Ansatz verwendet: Die Liste ist in einem Objekt fester Größe gekapselt, das den dynamischen Speicher für Sie verwaltet. Dies funktioniert, weil Objekte eine spezielle Bereinigungsfunktion haben können, den Destruktor. Es wird garantiert aufgerufen, wenn das Objekt den Gültigkeitsbereich verlässt und das Gegenteil des Konstruktors bewirkt:

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

In dem Code, in dem der Speicher verwendet wird, gibt es überhaupt keine Speicherverwaltung. Das einzige, was wir sicherstellen müssen, ist, dass das Objekt, das wir geschrieben haben, einen geeigneten Destruktor hat. Egal, wie wir den Gültigkeitsbereich verlassen listTest, sei es über eine Ausnahme oder einfach durch Zurückkehren, der Destruktor ~MyList()wird aufgerufen, und wir müssen keinen Speicher verwalten.

(Ich halte es für eine witzige Designentscheidung, den binären NOT- Operator zu verwenden, ~um den Destruktor anzugeben. Bei Verwendung mit Zahlen werden die Bits invertiert. In Analogie dazu wird hier angegeben, dass der Konstruktor invertiert wurde.)

Grundsätzlich verwenden alle C ++ - Objekte, die dynamischen Speicher benötigen, diese Kapselung. Es wurde RAII ("Ressourcenbeschaffung ist Initialisierung") genannt, was eine ziemlich seltsame Art ist, die einfache Idee auszudrücken, dass sich Objekte um ihren eigenen Inhalt kümmern. was sie erwerben, ist ihr, um aufzuräumen.

Polymorphe Objekte und eine Lebensdauer, die den Rahmen sprengt

Beide Fälle betrafen Speicher mit einer klar definierten Lebensdauer: Die Lebensdauer entspricht dem Gültigkeitsbereich. Wenn wir nicht möchten, dass ein Objekt verfällt, wenn wir den Gültigkeitsbereich verlassen, gibt es einen dritten Mechanismus, der den Speicher für uns verwalten kann: einen intelligenten Zeiger. Intelligente Zeiger werden auch verwendet, wenn Sie Instanzen von Objekten haben, deren Typ zur Laufzeit variiert, die jedoch eine gemeinsame Schnittstelle oder Basisklasse haben:

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

Es gibt eine andere Art von intelligentem Zeiger, std::shared_ptrmit dem Objekte von mehreren Clients gemeinsam genutzt werden können. Sie löschen ihr enthaltenes Objekt nur, wenn der letzte Client den Gültigkeitsbereich verlässt, sodass sie in Situationen verwendet werden können, in denen völlig unbekannt ist, wie viele Clients vorhanden sind und wie lange sie das Objekt verwenden werden.

Zusammenfassend stellen wir fest, dass Sie keine manuelle Speicherverwaltung durchführen. Alles ist gekapselt und wird dann durch eine vollautomatische, bereichsbezogene Speicherverwaltung erledigt. In den Fällen, in denen dies nicht ausreicht, werden intelligente Zeiger verwendet, die den Rohspeicher einkapseln.

Es wird als äußerst schlechte Praxis angesehen, Rohzeiger als Ressourcenbesitzer im gesamten C ++ - Code, Rohzuweisungen außerhalb von Konstruktoren und Rohaufrufe deleteaußerhalb von Destruktoren zu verwenden, da sie im Ausnahmefall kaum zu verwalten sind und im Allgemeinen nur schwer sicher zu verwenden sind.

Das Beste: Dies funktioniert für alle Arten von Ressourcen

Einer der größten Vorteile von RAII ist, dass es nicht nur auf das Gedächtnis beschränkt ist. Tatsächlich bietet es eine sehr natürliche Möglichkeit, Ressourcen wie Dateien und Sockets (Öffnen / Schließen) und Synchronisationsmechanismen wie Mutexe (Sperren / Entsperren) zu verwalten. Grundsätzlich wird jede Ressource, die erworben werden kann und freigegeben werden muss, in C ++ genauso verwaltet, und nichts von dieser Verwaltung bleibt dem Benutzer überlassen. Es ist alles in Klassen gekapselt, die im Konstruktor akquirieren und im Destruktor freigeben.

Zum Beispiel wird eine Funktion, die einen Mutex sperrt, normalerweise so in C ++ geschrieben:

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

Andere Sprachen erschweren dies erheblich, indem Sie dies entweder manuell ausführen müssen (z. B. in einer finallyKlausel) oder spezialisierte Mechanismen erzeugen, die dieses Problem lösen, jedoch nicht auf besonders elegante Weise (in der Regel später in ihrem Leben, wenn genügend Menschen vorhanden sind) litt unter dem Mangel). Solche Mechanismen sind Try-with-Resources in Java und die using- Anweisung in C #, die beide Annäherungen an C ++ 's RAII sind.

Zusammenfassend war dies alles eine sehr oberflächliche Darstellung von RAII in C ++, aber ich hoffe, dass es den Lesern hilft, zu verstehen, dass das Speichern und sogar das Ressourcenmanagement in C ++ normalerweise nicht "manuell", sondern hauptsächlich automatisch ist.

Felix Dombek
quelle
7
Dies ist die einzige Antwort, die weder Menschen falsch informiert noch C ++ schwieriger oder gefährlicher malt, als es wirklich ist.
Alexander Revo
6
Übrigens wird es nur als schlechte Praxis angesehen, Rohzeiger als Ressourceneigentümer zu verwenden. Es ist nichts Falsches daran, sie zu verwenden, wenn sie auf etwas zeigen, das den Zeiger selbst garantiert überlebt.
Alexander Revo
8
Ich bin zweiter Alexander. Ich bin verblüfft zu sehen, dass "C ++ keine automatisierte Speicherverwaltung hat, vergiss ein deleteund du bist tot" Antworten, die über 30 Punkte schießen und akzeptiert werden, während diese fünf hat. Verwendet hier tatsächlich jemand C ++?
Quentin
8

In Bezug auf C bietet die Sprache keine Tools zum Verwalten des dynamisch zugewiesenen Speichers. Sie sind absolut dafür verantwortlich, dass jeder irgendwo *alloceine Entsprechung hat free.

Wirklich schlimm wird es, wenn eine Ressourcenzuweisung auf halbem Weg fehlschlägt. Versuchen Sie es erneut, führen Sie einen Rollback durch und beginnen Sie von vorne. Führen Sie einen Rollback durch und beenden Sie das Programm mit einem Fehler.

Hier ist beispielsweise eine Funktion zum Zuweisen eines nicht zusammenhängenden 2D-Arrays. Das Verhalten hier ist, dass, wenn ein Zuordnungsfehler in der Mitte des Prozesses auftritt, wir alles zurücksetzen und eine Fehleranzeige mit einem NULL-Zeiger zurückgeben:

/**
 * 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;
}

Dieser Code ist stumpf hässlich mit jenen gotos, aber in Abwesenheit jede Art einer strukturierten Ausnahmebehandlung, das so ziemlich der einzige Weg ist , völlig ohne nur die Rettung mit dem Problem zu befassen, vor allem , wenn Ihr Code Ressourcenzuweisung ist verschachtelt mehr als eine Schleife tief. Dies ist eines der wenigen Male, bei denen gotoes sich tatsächlich um eine attraktive Option handelt. Andernfalls verwenden Sie eine Reihe von Flags und zusätzlichen ifAnweisungen.

Sie können sich das Leben erleichtern, indem Sie für jede Ressource dedizierte Allokator- / Freigabefunktionen schreiben, wie z

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
quelle
1
Dies ist eine gute Antwort, auch mit den gotoAussagen. Dies wird in einigen Bereichen empfohlen. Es ist ein weit verbreitetes Schema zum Schutz gegen das Äquivalent von Ausnahmen in C. Schauen Sie sich den Linux-Kernel-Code an, der voll von gotoAnweisungen ist - und der nicht leckt.
David Hammen
"ohne nur komplett auszusteigen" -> Wenn Sie fairerweise über C sprechen möchten, ist dies wahrscheinlich eine gute Übung. C ist eine Sprache, die am besten für die Verarbeitung von Speicherblöcken verwendet wird, die von einem anderen Ort stammen, oder für die Verteilung kleiner Speicherblöcke auf andere Prozeduren, vorzugsweise jedoch nicht für die gleichzeitige Ausführung beider Aufgaben in verschachtelter Form. Wenn Sie in C klassische "Objekte" verwenden, werden Sie die Sprache wahrscheinlich nicht zu ihren Stärken nutzen.
Leushenko
Der zweite gotoist irrelevant. Es wäre besser lesbar, wenn Sie goto done;zu return arr;und arr=NULL;done:return arr;zu wechseln würden return NULL;. Obwohl es in komplizierteren Fällen tatsächlich mehrere gotos geben kann, beginnt das Abwickeln mit unterschiedlichen Bereitschaftsstufen (was durch das Abwickeln des Ausnahmestapels in C ++ geschehen würde).
Ruslan
2

Ich habe gelernt, Speicherprobleme in verschiedene Kategorien einzuteilen.

  • Einmal tropft. Angenommen, ein Programm verliert 100 Bytes zum Startzeitpunkt, nur um nie wieder zu verlieren. Das Aufspüren und Beseitigen dieser einmaligen Lecks ist nett (ich habe gerne einen sauberen Bericht mit einer Leckerkennungsfunktion), aber nicht unbedingt erforderlich. Manchmal gibt es größere Probleme, die angegriffen werden müssen.

  • Wiederholte Undichtigkeiten. Eine Funktion, die im Laufe einer Programmlebensdauer wiederholt aufgerufen wird und bei der regelmäßig Speicherplatz verloren geht, ist ein großes Problem. Diese Tropfen werden das Programm und möglicherweise das Betriebssystem zu Tode quälen.

  • Gegenseitige Referenzen. Wenn die Objekte A und B über gemeinsame Zeiger aufeinander verweisen, müssen Sie etwas Besonderes tun, entweder im Design dieser Klassen oder im Code, der diese Klassen implementiert / verwendet, um die Zirkularität zu durchbrechen. (Dies ist kein Problem für Sprachen, die mit Datenmüll gesammelt wurden.)

  • Sich zu sehr erinnern. Dies ist der böse Cousin von Müll / Speicherlecks. RAII wird hier nicht helfen, noch wird Müllabfuhr. Dies ist in jeder Sprache ein Problem. Wenn eine aktive Variable einen Pfad hat, der sie mit einem zufälligen Speicherblock verbindet, ist dieser zufällige Speicherblock kein Müll. Es ist schwierig, ein Programm vergesslich zu machen, damit es mehrere Tage laufen kann. Es ist sehr, sehr schwierig, ein Programm zu erstellen, das mehrere Monate laufen kann (z. B. bis die Festplatte ausfällt).

Ich habe lange, lange kein ernstes Problem mit Undichtigkeiten gehabt. Die Verwendung von RAII in C ++ hilft sehr dabei, diese Tropfen und Undichtigkeiten zu beseitigen. (Mit gemeinsam genutzten Zeigern muss man jedoch vorsichtig sein.) Viel wichtiger ist, dass ich Probleme mit Anwendungen hatte, deren Speichernutzung immer größer und größer wird, da nicht mehr benötigte Verbindungen zum Arbeitsspeicher bestehen.

David Hammen
quelle
-6

Es ist Sache des C ++ - Programmierers, bei Bedarf seine eigene Form der Garbage Collection zu implementieren. Andernfalls tritt ein sogenannter "Memory Leak" auf. Es ist durchaus üblich, dass 'High-Level'-Sprachen (wie Java) eine Garbage Collection eingebaut haben,' Low-Level'-Sprachen wie C und C ++ jedoch nicht.

xDr_Johnx
quelle