Warum verursacht die Verwendung von "neu" Speicherlecks?


131

Ich habe zuerst C # gelernt und jetzt beginne ich mit C ++. Soweit ich weiß, newähnelt der Operator in C ++ nicht dem in C #.

Können Sie den Grund für den Speicherverlust in diesem Beispielcode erklären?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

Antworten:


464

Was ist los

Wenn Sie schreiben T t;, erstellen Sie ein Objekt vom Typ Tmit automatischer Speicherdauer . Es wird automatisch bereinigt, wenn es außerhalb des Gültigkeitsbereichs liegt.

Beim Schreiben new T()erstellen Sie ein Objekt vom Typ Tmit dynamischer Speicherdauer . Es wird nicht automatisch aufgeräumt.

neu ohne aufräumen

Sie müssen einen Zeiger darauf übergeben, deleteum es zu bereinigen:

Neu mit Löschen

Ihr zweites Beispiel ist jedoch schlechter: Sie dereferenzieren den Zeiger und erstellen eine Kopie des Objekts. Auf diese Weise verlieren Sie den Zeiger auf das mit erstellte Objekt new, sodass Sie es niemals löschen können, selbst wenn Sie möchten!

Neuheit mit deref

Was du machen solltest

Sie sollten die automatische Speicherdauer bevorzugen. Benötigen Sie ein neues Objekt, schreiben Sie einfach:

A a; // a new object of type A
B b; // a new object of type B

Wenn Sie eine dynamische Speicherdauer benötigen, speichern Sie den Zeiger auf das zugewiesene Objekt in einem Objekt mit automatischer Speicherdauer, das es automatisch löscht.

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

Neuheit mit automatischem Zeiger

Dies ist eine gebräuchliche Redewendung, die unter dem nicht sehr beschreibenden Namen RAII ( Resource Acquisition Is Initialization ) geführt wird. Wenn Sie eine Ressource erwerben, die bereinigt werden muss, stecken Sie sie in ein Objekt mit automatischer Speicherdauer, damit Sie sich nicht um die Bereinigung kümmern müssen. Dies gilt für jede Ressource, sei es Speicher, geöffnete Dateien, Netzwerkverbindungen oder was auch immer Sie möchten.

Dieses automatic_pointerDing existiert bereits in verschiedenen Formen, ich habe es nur bereitgestellt, um ein Beispiel zu geben. Eine sehr ähnliche Klasse existiert in der Standardbibliothek namens std::unique_ptr.

Es gibt auch eine alte (vor C ++ 11) mit dem Namen auto_ptr, die jetzt jedoch veraltet ist, da sie ein seltsames Kopierverhalten aufweist.

Und dann gibt es einige noch intelligentere Beispiele, std::shared_ptrdie mehrere Zeiger auf dasselbe Objekt zulassen und es erst bereinigen, wenn der letzte Zeiger zerstört wird.


4
@ user1131997: froh, dass du dies eine andere Frage gestellt hast. Wie Sie sehen können, ist es nicht sehr einfach, in Kommentaren zu erklären :)
R. Martinho Fernandes

@ R.MartinhoFernandes: ausgezeichnete Antwort. Nur eine Frage. Warum haben Sie in der Funktion operator * () return by reference verwendet?
Zerstörer

@ Zerstörer späte Antwort: D. Wenn Sie als Referenz zurückkehren, können Sie den Pointee so ändern, dass Sie dies beispielsweise *p += 2wie mit einem normalen Zeiger tun können . Wenn es nicht als Referenz zurückgegeben würde, würde es das Verhalten eines normalen Zeigers nicht nachahmen, was hier beabsichtigt ist.
R. Martinho Fernandes

Vielen Dank für den Hinweis, "den Zeiger auf das zugewiesene Objekt in einem Objekt mit automatischer Speicherdauer zu speichern, das es automatisch löscht". Wenn es nur eine Möglichkeit gäbe, Codierer zu verpflichten, dieses Muster zu lernen, bevor sie C ++ kompilieren können!
Andy

35

Eine schrittweise Erklärung:

// 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());

Am Ende haben Sie also ein Objekt auf dem Heap ohne Zeiger darauf, sodass es nicht gelöscht werden kann.

Das andere Beispiel:

A *object1 = new A();

ist nur dann ein Speicherverlust, wenn Sie deleteden zugewiesenen Speicher vergessen haben :

delete object1;

In C ++ gibt es Objekte mit automatischem Speicher, auf dem Stapel erstellte Objekte, die automatisch entsorgt werden, und Objekte mit dynamischem Speicher auf dem Heap, denen Sie zuordnen newund mit denen Sie sich befreien müssen delete. (das ist alles grob ausgedrückt)

Denken Sie, dass Sie deletefür jedes Objekt eine zuweisen sollten new.

BEARBEITEN

Kommen Sie, um darüber nachzudenken, object2muss kein Speicherverlust sein.

Der folgende Code dient nur dazu, einen Punkt zu verdeutlichen. Es ist eine schlechte Idee, Code wie diesen niemals zu mögen:

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

In diesem Fall ist es, da otheres als Referenz übergeben wird, das genaue Objekt, auf das von gezeigt wird new B(). Daher &otherwürde das Abrufen der Adresse durch und das Löschen des Zeigers den Speicher freigeben.

Aber ich kann das nicht genug betonen, tu das nicht. Es ist nur hier, um einen Punkt zu machen.


2
Ich dachte das Gleiche: Wir können es hacken, um nicht zu lecken, aber das würden Sie nicht wollen. object1 muss auch nicht auslaufen, da sich sein Konstruktor an eine Datenstruktur anhängen könnte, die ihn irgendwann löscht.
CashCow

2
Es ist immer so verlockend, die Antworten "Es ist möglich, dies zu tun, aber nicht" zu schreiben! :-) Ich kenne das Gefühl
Kos

11

Gegeben zwei "Objekte":

obj a;
obj b;

Sie werden nicht denselben Speicherort belegen. Mit anderen Worten,&a != &b

Wenn Sie den Wert des einen dem anderen zuweisen, ändert sich nicht der Speicherort, sondern der Inhalt:

obj a;
obj b = a;
//a == b, but &a != &b

Intuitiv funktionieren Zeiger- "Objekte" auf die gleiche Weise:

obj *a;
obj *b = a;
//a == b, but &a != &b

Schauen wir uns nun Ihr Beispiel an:

A *object1 = new A();

Dies weist den Wert von zu new A() zu object1. Der Wert ist ein Zeiger, was bedeutet object1 == new A(), aber &object1 != &(new A()). (Beachten Sie, dass dieses Beispiel kein gültiger Code ist, sondern nur zur Erläuterung dient.)

Da der Wert des Zeigers erhalten bleibt, können wir den Speicher freigeben, auf den er verweist: delete object1;Aufgrund unserer Regel verhält sich dies genauso wie delete (new A());bei keinem Leck.


In Ihrem zweiten Beispiel kopieren Sie das Objekt, auf das Sie zeigen. Der Wert ist der Inhalt dieses Objekts, nicht der eigentliche Zeiger. Wie in jedem anderen Fall&object2 != &*(new A()) .

B object2 = *(new B());

Wir haben den Zeiger auf den zugewiesenen Speicher verloren und können ihn daher nicht freigeben. delete &object2;mag scheinen, als würde es funktionieren, aber weil &object2 != &*(new A())es nicht gleichwertig delete (new A())und so ungültig ist.


9

In C # und Java verwenden Sie new, um eine Instanz einer Klasse zu erstellen, und müssen sich dann nicht mehr darum kümmern, sie später zu zerstören.

C ++ hat auch ein Schlüsselwort "new", das ein Objekt erstellt. Im Gegensatz zu Java oder C # ist dies jedoch nicht die einzige Möglichkeit, ein Objekt zu erstellen.

C ++ verfügt über zwei Mechanismen zum Erstellen eines Objekts:

  • automatisch
  • dynamisch

Bei der automatischen Erstellung erstellen Sie das Objekt in einer Umgebung mit Gültigkeitsbereich: - in einer Funktion oder - als Mitglied einer Klasse (oder Struktur).

In einer Funktion würden Sie es folgendermaßen erstellen:

int func()
{
   A a;
   B b( 1, 2 );
}

Innerhalb einer Klasse würden Sie es normalerweise so erstellen:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

Im ersten Fall werden die Objekte beim Verlassen des Bereichsblocks automatisch zerstört. Dies kann eine Funktion oder ein Scope-Block innerhalb einer Funktion sein.

Im letzteren Fall wird das Objekt b zusammen mit der Instanz von A zerstört, in der es Mitglied ist.

Objekte werden mit new zugewiesen, wenn Sie die Lebensdauer des Objekts steuern müssen. Anschließend muss es gelöscht werden, um es zu zerstören. Mit der als RAII bekannten Technik kümmern Sie sich um das Löschen des Objekts an dem Punkt, an dem Sie es erstellen, indem Sie es in ein automatisches Objekt einfügen, und warten, bis der Destruktor des automatischen Objekts wirksam wird.

Ein solches Objekt ist ein shared_ptr, das eine "Löscher" -Logik aufruft, jedoch nur dann, wenn alle Instanzen des shared_ptr, die das Objekt gemeinsam nutzen, zerstört werden.

Während Ihr Code möglicherweise viele Aufrufe von new enthält, sollten Sie im Allgemeinen nur begrenzte Aufrufe zum Löschen haben und immer sicherstellen, dass diese von Destruktoren aufgerufen oder Objekte "gelöscht" werden, die in Smart-Pointer eingefügt werden.

Ihre Destruktoren sollten auch niemals Ausnahmen auslösen.

Wenn Sie dies tun, treten nur wenige Speicherlecks auf.


4
Es gibt mehr als automaticund dynamic. Es gibt auch static.
Mooing Duck

9
B object2 = *(new B());

Diese Leitung ist die Ursache für das Leck. Lassen Sie uns dies ein wenig auseinander nehmen ..

Objekt2 ist eine Variable vom Typ B, die beispielsweise an Adresse 1 gespeichert ist (Ja, ich wähle hier beliebige Zahlen aus). Auf der rechten Seite haben Sie nach einem neuen B oder einem Zeiger auf ein Objekt vom Typ B gefragt. Das Programm gibt Ihnen dies gerne und weist Ihr neues B der Adresse 2 zu und erstellt auch einen Zeiger in der Adresse 3. Nun Der einzige Weg, auf die Daten in Adresse 2 zuzugreifen, ist über den Zeiger in Adresse 3. Als nächstes haben Sie den Zeiger mit dereferenziert* , um die Daten auf die der Zeiger zeigt (die Daten in Adresse 2). Dadurch wird effektiv eine Kopie dieser Daten erstellt und Objekt2 zugewiesen, das in Adresse 1 zugewiesen ist. Denken Sie daran, dass es sich um eine KOPIE handelt, nicht um das Original.

Hier ist das Problem:

Sie haben diesen Zeiger nie an einem Ort gespeichert, an dem Sie ihn verwenden können! Sobald diese Zuweisung abgeschlossen ist, ist der Zeiger (Speicher in Adresse3, über den Sie auf Adresse2 zugegriffen haben) außerhalb des Bereichs und außerhalb Ihrer Reichweite! Sie können delete nicht mehr aufrufen und daher den Speicher in Adresse2 nicht bereinigen. Was Ihnen bleibt, ist eine Kopie der Daten von Adresse2 in Adresse1. Zwei der gleichen Dinge, die in Erinnerung bleiben. Auf eines können Sie zugreifen, auf das andere nicht (weil Sie den Pfad dorthin verloren haben). Deshalb ist dies ein Speicherverlust.

Ich würde vorschlagen, dass Sie aus Ihrem C # -Hintergrund viel darüber lesen, wie Zeiger in C ++ funktionieren. Sie sind ein fortgeschrittenes Thema und können einige Zeit in Anspruch nehmen, aber ihre Verwendung wird für Sie von unschätzbarem Wert sein.


8

Wenn es einfacher ist, stellen Sie sich den Computerspeicher wie ein Hotel vor, und Programme sind Kunden, die Zimmer mieten, wenn sie diese benötigen.

Dieses Hotel funktioniert so, dass Sie ein Zimmer buchen und dem Portier mitteilen, wann Sie abreisen.

Wenn Sie Bücher buchen und einen Raum verlassen, ohne dies dem Portier mitzuteilen, wird der Portier denken, dass der Raum noch genutzt wird, und niemand anderem erlauben, ihn zu benutzen. In diesem Fall liegt ein Raumleck vor.

Wenn Ihr Programm Speicher zuweist und ihn nicht löscht (es verwendet ihn lediglich nicht mehr), glaubt der Computer, dass der Speicher noch verwendet wird, und lässt niemanden zu, ihn zu verwenden. Dies ist ein Speicherverlust.

Dies ist keine exakte Analogie, aber es könnte helfen.


5
Ich mag diese Analogie sehr, sie ist nicht perfekt, aber sie ist definitiv eine gute Möglichkeit, Speicherlecks Menschen zu erklären, die neu darin sind!
AdamM

1
Ich habe dies in einem Interview für einen leitenden Ingenieur bei Bloomberg in London verwendet, um einem HR-Mädchen Gedächtnislecks zu erklären. Ich habe dieses Interview überstanden, weil ich einem Nicht-Programmierer Speicherlecks (und Threading-Probleme) auf eine Weise erklären konnte, die sie verstand.
Stefan

7

Beim Erstellen erstellen object2Sie eine Kopie des Objekts, das Sie mit new erstellt haben, verlieren jedoch auch den (nie zugewiesenen) Zeiger (sodass Sie ihn später nicht mehr löschen können). Um dies zu vermeiden, müssten Sie object2eine Referenz erstellen.


3
Es ist unglaublich schlecht, die Adresse einer Referenz zu verwenden, um ein Objekt zu löschen. Verwenden Sie einen intelligenten Zeiger.
Tom Whittock

3
Unglaublich schlechte Praxis, oder? Was verwenden Ihrer Meinung nach intelligente Zeiger hinter den Kulissen?
Blindy

3
@Blindy Smart Pointer (zumindest anständig implementierte) verwenden Zeiger direkt.
Luchian Grigore

2
Um ganz ehrlich zu sein, ist die ganze Idee nicht so toll, oder? Eigentlich bin ich mir nicht mal sicher, wo das im OP versuchte Muster tatsächlich nützlich wäre.
Mario

7

Nun, Sie erstellen einen Speicherverlust, wenn Sie den mit dem newOperator zugewiesenen Speicher nicht irgendwann freigeben, indem Sie dem Operator einen Zeiger auf diesen Speicher übergeben delete.

In Ihren beiden oben genannten Fällen:

A *object1 = new A();

Hier wird deleteder Speicher nicht freigegeben. Wenn also Ihr object1Zeiger den Gültigkeitsbereich verlässt, tritt ein Speicherverlust auf, da Sie den Zeiger verloren haben und daher den deleteOperator nicht verwenden können.

Und hier

B object2 = *(new B());

Sie verwerfen den von zurückgegebenen Zeiger new B()und können diesen Zeiger daher niemals an übergeben, damit deleteder Speicher freigegeben wird. Daher ein weiterer Speicherverlust.


7

Es ist diese Linie, die sofort leckt:

B object2 = *(new B());

Hier erstellen Sie ein neues BObjekt auf dem Heap und anschließend eine Kopie auf dem Stapel. Auf diejenige, die auf dem Heap zugewiesen wurde, kann nicht mehr zugegriffen werden, und daher das Leck.

Diese Leitung ist nicht sofort undicht:

A *object1 = new A();

Es wäre ein Leck sein , wenn Sie nie deleted object1though.


4
Bitte verwenden Sie keinen Heap / Stack, wenn Sie den dynamischen / automatischen Speicher erklären.
Pubby

2
@Pubby warum nicht verwenden? Wegen dynamischer / automatischer Speicherung ist immer Haufen, nicht Stapel? Und deshalb gibt es keine Notwendigkeit, Details über Stack / Heap zu machen, habe ich Recht?

4
@ user1131997 Heap / Stack sind Implementierungsdetails. Sie sind wichtig zu wissen, aber für diese Frage irrelevant.
Pubby

2
Hmm, ich hätte gerne eine separate Antwort darauf, dh die gleiche wie meine, aber ich würde den Haufen / Stapel durch das ersetzen, was Sie für am besten halten. Es würde mich interessieren, wie Sie es lieber erklären würden.
Mattjgalloway
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.