Wie vermeide ich Speicherverluste, wenn ich einen Vektor von Zeigern auf dynamisch zugewiesene Objekte in C ++ verwende?

68

Ich verwende einen Vektor von Zeigern auf Objekte. Diese Objekte werden von einer Basisklasse abgeleitet und dynamisch zugeordnet und gespeichert.

Zum Beispiel habe ich so etwas wie:

vector<Enemy*> Enemies;

und ich werde von der Enemy-Klasse ableiten und dann dynamisch Speicher für die abgeleitete Klasse zuweisen, wie folgt:

enemies.push_back(new Monster());

Was muss ich beachten, um Speicherverluste und andere Probleme zu vermeiden?

akif
quelle
Vielleicht kann ein englischer Muttersprachler entziffern, was Sie sagen möchten, aber ich bin verloren. Zunächst sprechen Sie über Speicherlecks -> sprach- / plattformabhängig; Ich gehe davon aus, dass du C ++ meinst. Das Vermeiden von Speicherlecks wurde bereits ausführlich diskutiert ( stackoverflow.com/search?q=c%2B%2B+raii ). Sie benötigen einen virtuellen Destruktor zum Löschen aus einem Basistyp, um ordnungsgemäß zu funktionieren.
Gimpf
1
Was meinst du mit "Vektoren zu Zeigern"? Meinen Sie "Vektoren von Zeigern"?
Tamás Szelei
Ja, ich benutze C ++. Ja, ich meine Vektoren von Zeigern. Entschuldigung für mein schlechtes Englisch
akif
16
Ich habe versucht, alles neu zu formulieren. Bitte bearbeiten oder kommentieren Sie, wenn ich Informationen entfernt habe oder wenn diese nicht klar sind.
GManNickG
Nur, dass Sie jedes Element des Vektors von Zeigern auf neue Klassen löschen müssen, die innerhalb des Vektors definiert sind. Der Vektorcontainer selbst wird automatisch freigegeben, wenn er den Gültigkeitsbereich verlässt. Beachten Sie, dass Sie Ihre Destruktoren explizit definieren müssen, wenn Ihre Vererbungshierarchie virtuell ist, da dies auch zu Speicherverlusten führen kann.
Eule

Antworten:

150

std::vector verwaltet den Speicher wie immer für Sie, aber dieser Speicher besteht aus Zeigern, nicht aus Objekten.

Dies bedeutet, dass Ihre Klassen im Speicher verloren gehen, sobald Ihr Vektor den Gültigkeitsbereich verlässt. Zum Beispiel:

#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<base*> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(new derived());

} // leaks here! frees the pointers, doesn't delete them (nor should it)

int main()
{
    foo();
}

Sie müssen lediglich sicherstellen, dass Sie alle Objekte löschen, bevor der Vektor den Gültigkeitsbereich verlässt:

#include <algorithm>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<base*> container;

template <typename T>
void delete_pointed_to(T* const ptr)
{
    delete ptr;
}

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(new derived());

    // free memory
    std::for_each(c.begin(), c.end(), delete_pointed_to<base>);
}

int main()
{
    foo();
}

Dies ist jedoch schwierig aufrechtzuerhalten, da wir uns daran erinnern müssen, eine Aktion auszuführen. Noch wichtiger ist, wenn eine Ausnahme zwischen der Zuweisung von Elementen und der Freigabeschleife auftreten würde, würde die Freigabeschleife niemals ausgeführt und Sie stecken trotzdem mit dem Speicherverlust fest! Dies wird als Ausnahmesicherheit bezeichnet und ist ein kritischer Grund, warum die Freigabe automatisch erfolgen muss.

Besser wäre es, wenn sich die Zeiger selbst löschen würden. Diese werden als intelligente Zeiger bezeichnet, und die Standardbibliothek bietet std::unique_ptrund std::shared_ptr.

std::unique_ptrstellt einen eindeutigen Zeiger (nicht freigegeben, Einzelbesitzer) auf eine Ressource dar. Dies sollte Ihr standardmäßiger intelligenter Zeiger sein und die vollständige Verwendung aller Rohzeiger vollständig ersetzen.

auto myresource = /*std::*/make_unique<derived>(); // won't leak, frees itself

std::make_uniquefehlt im C ++ 11-Standard durch Versehen, aber Sie können selbst einen erstellen. Gehen Sie folgendermaßen vor, um ein direkt zu erstellen unique_ptr(nicht empfohlen, make_uniquewenn Sie können):

std::unique_ptr<derived> myresource(new derived());

Eindeutige Zeiger haben nur eine Bewegungssemantik. Sie können nicht kopiert werden:

auto x = myresource; // error, cannot copy
auto y = std::move(myresource); // okay, now myresource is empty

Und das ist alles, was wir brauchen, um es in einem Container zu verwenden:

#include <memory>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<std::unique_ptr<base>> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(make_unique<derived>());

} // all automatically freed here

int main()
{
    foo();
}

shared_ptrhat eine Referenzzählkopiesemantik; Es ermöglicht mehreren Eigentümern, das Objekt gemeinsam zu nutzen. Es verfolgt, wie vieleshared_ptr s für ein Objekt existieren, und wenn das letzte nicht mehr existiert (diese Anzahl geht auf Null), gibt es den Zeiger frei. Durch das Kopieren wird lediglich die Referenzanzahl erhöht (und durch das Verschieben wird das Eigentum zu geringeren, fast kostenlosen Kosten übertragen). Sie erstellen sie mit std::make_shared(oder direkt wie oben gezeigt, aber da shared_ptrinterne Zuweisungen vorgenommen werden müssen, ist die Verwendung im Allgemeinen effizienter und technisch ausnahmsicherer make_shared).

#include <memory>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<std::shared_ptr<base>> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(std::make_shared<derived>());

} // all automatically freed here

int main()
{
    foo();
}

Denken Sie daran, dass Sie im Allgemeinen std::unique_ptrstandardmäßig verwenden möchten, da es leichter ist. Zusätzlich std::shared_ptrkann konstruiert aus einem werden std::unique_ptr(aber nicht umgekehrt), so dass es in Ordnung ist , klein zu beginnen.

Alternativ können Sie einen Container verwenden, der zum Speichern von Zeigern auf Objekte erstellt wurde, z boost::ptr_container :

#include <boost/ptr_container/ptr_vector.hpp>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

// hold pointers, specially
typedef boost::ptr_vector<base> container;

void foo()
{
    container c;

    for (int i = 0; i < 100; ++i)
        c.push_back(new Derived());

} // all automatically freed here

int main()
{
    foo();
}

Während boost::ptr_vector<T> dies in C ++ 03 offensichtlich verwendet wurde, kann ich jetzt nicht über die Relevanz sprechen, da wir es std::vector<std::unique_ptr<T>>mit wahrscheinlich geringem bis keinem vergleichbaren Overhead verwenden können, aber diese Behauptung sollte getestet werden.

Ungeachtet, niemals explizit Dinge in Ihrem Code frei . Packen Sie alles zusammen, um sicherzustellen, dass das Ressourcenmanagement automatisch erledigt wird. Sie sollten keine rohen Besitzzeiger in Ihrem Code haben.

Als Standard in einem Spiel würde ich wahrscheinlich mit gehen std::vector<std::shared_ptr<T>>. Wir erwarten sowieso, dass das Teilen schnell genug ist, bis die Profilerstellung etwas anderes sagt, es sicher ist und einfach zu verwenden ist.

GManNickG
quelle
2
Wenn er tatsächlich Gamecode schreibt (wie das Beispiel andeutet), ist ein Zeiger mit Ref-Zählung (oder wie auch immer der gemeinsam genutzte Zeiger durch Boost implementiert wird) wahrscheinlich zu teuer. Ein konstanter Speicherbedarf (insbesondere für KI-Objekte) ist ein höheres Designziel als Entfernen einer for-Schleife zum Aufheben der Zuordnung.
Dan O
Welches sollte ich s / w Zeiger enthält und freigegebene Zeiger wählen und warum?
Akif
5
@Dan: Auf die eine oder andere Weise müssen Sie die Bereinigung durchführen, und wenn das zu langsam ist, ist die Frage nicht, wie Sie es tun sollen, sondern wie Sie vermeiden müssen, dass Sie es überhaupt tun müssen. Wenn Sie es nicht umgehen können, verwenden Sie zuerst den saubersten Weg, messen Sie dann und versuchen Sie erst danach, sich zu verbessern. Boost bedeutet mehrere tausend Paare scharfer Augen, die den Code verbessern. Kaum zu übertreffen: Ich habe gesehen, dass Boost shared_ptreinen benutzerdefinierten Smart Pointer mit einem speziellen Allokator in CPU / GPU-intensiven 3D-Anwendungen übertrifft. Bis Sie messen, wissen Sie nie ...
sbi
Meine Antwort wurde aktualisiert. Zum Glück stimmten unsere 'Antworten' diesmal überein, sbi. : P (Profil!)
GManNickG
1
@sbi Ich befürworte kein anderes shared_ptr, ich befürworte einen anderen Ansatz für die Speicherverwaltung. Geteilte Zeiger sind im Fall des Spielcodes sehr wahrscheinlich unangemessen. Tatsächlich sind sie für das Beispiel des Originalplakats völlig ungeeignet. Die meisten meiner Argumente sind hier zusammengefasst: office14.fr/blogea/2009/08/smart-pointers-are-overused
Dan O
10

Das Problem bei der Verwendung vector<T*>besteht darin, dass der Vektor immer dann bereinigt wird, wenn der Vektor unerwartet den Gültigkeitsbereich verlässt (z. B. wenn eine Ausnahme ausgelöst wird). Dadurch wird jedoch nur der Speicher freigegeben, den er für das Halten des Zeigers verwaltet , nicht der von Ihnen zugewiesene Speicher für was sich die Zeiger beziehen. Also GMans delete_pointed_toFunktion ist nur von begrenztem Wert, da es funktioniert nur , wenn nichts schief geht.

Was Sie tun müssen, ist die Verwendung eines intelligenten Zeigers:

vector< std::tr1::shared_ptr<Enemy> > Enemies;

(Wenn Ihre Standardbibliothek ohne TR1 geliefert wird, verwenden Sie boost::shared_ptrstattdessen.) Mit Ausnahme sehr seltener Eckfälle (Zirkelverweise) werden die Probleme der Objektlebensdauer einfach beseitigt.

Bearbeiten : Beachten Sie, dass GMan in seiner ausführlichen Antwort dies ebenfalls erwähnt.

sbi
quelle
1
@ GMan: Ich habe Ihre Antwort vollständig gelesen und gesehen. Ich hätte die delete_pointer_toMöglichkeit nur erwähnt , ohne darauf einzugehen, da sie so viel minderwertiger ist. Ich hatte das Bedürfnis, die Standardlösung in eine kurze, einfache "Do-it-this-way" -Antwort umzuwandeln. (Die Zeigercontainer von Boost sind jedoch eine gute Alternative, und ich habe eine positive Bewertung abgegeben, weil ich sie erwähnt habe.) Es tut mir leid, wenn Sie sich falsch verstanden haben.
sbi
2
Ich denke, Ihr Standpunkt ist eigentlich sehr gut. Soll ich es bearbeiten? Ich bin mir an dieser Stelle immer unsicher. Wenn ich meine Antwort so bearbeite, dass sie vollständiger ist, habe ich das Gefühl, dass ich anderen Leuten einen Repräsentanten "stehle".
GManNickG
3
@GMan: Verbessere die Antwort, die oben auf dem Stapel liegt. Ihre Antwort ist gut und detailliert und verdient es definitiv, dort zu sein. Zum Teufel mit dem Repräsentanten, wenn es einen Programmierer weniger gibt, der solche Dinge tut, hilft uns das viel mehr als alle Wiederholungspunkte. :)
sbi
und vielleicht helfen
wir
2
Mein Wort! Freundlicher und kooperativer Diskurs, geschweige denn Einigung in einer Online-Diskussion? Völlig unbekannt!
Gute
9

Ich gehe davon aus:

  1. Sie haben einen Vektor wie den Vektor <base *>
  2. Sie verschieben die Zeiger auf diesen Vektor, nachdem Sie die Objekte auf dem Heap zugewiesen haben
  3. Sie möchten einen Push_back des abgeleiteten * Zeigers in diesen Vektor ausführen.

Folgende Dinge kommen mir in den Sinn:

  1. Der Vektor gibt den Speicher des Objekts, auf das der Zeiger zeigt, nicht frei. Sie müssen es selbst löschen.
  2. Nichts spezifisches für den Vektor, aber der Destruktor der Basisklasse sollte virtuell sein.
  3. Vektor <Basis *> und Vektor <abgeleitet *> sind zwei völlig unterschiedliche Typen.
Naveen
quelle
Ihre Annahmen sind absolut richtig. Entschuldigung, ich konnte es nicht richtig erklären. Gibt es noch etwas?
akif
1
Vermeiden Sie nach Möglichkeit rohe Zeiger und verwenden Sie die in der Antwort von GMan beschriebenen Methoden.
Naveen
-1

Eine Sache, die sehr vorsichtig sein sollte, ist, WENN es zwei von Monster () ABGELEITETE Objekte gibt, deren Inhalt im Wert identisch ist. Angenommen, Sie möchten die DUPLICATE Monster-Objekte aus Ihrem Vektor entfernen (BASE-Klassenzeiger auf DERIVED Monster-Objekte). Wenn Sie die Standardsprache zum Entfernen von Duplikaten verwendet haben (sortieren, eindeutig, löschen: siehe LINK 2), treten Probleme mit Speicherverlusten und / oder doppelten Löschproblemen auf, die möglicherweise zu SEGMENTIERUNGSVERLETZUNGEN führen (ich habe diese Probleme persönlich gesehen) LINUX-Maschine).

Das Problem mit std :: unique () besteht darin, dass die Duplikate im Bereich [duplicatePosition, end) [einschließlich, exklusiv] am Ende des Vektors undefiniert sind als? Was passieren kann, ist, dass diese undefinierten ((?) Elemente möglicherweise ein zusätzliches Duplikat oder ein fehlendes Duplikat sind.

Das Problem ist, dass std :: unique () nicht darauf ausgerichtet ist, einen Zeigervektor richtig zu behandeln. Der Grund dafür ist, dass std :: unique-Kopien vom Ende des Vektors "abwärts" bis zum Anfang des Vektors eindeutig sind. Für einen Vektor von einfachen Objekten ruft dies den COPY CTOR auf, und wenn der COPY CTOR richtig geschrieben ist, gibt es kein Problem von Speicherlecks. Wenn es sich jedoch um einen Zeigervektor handelt, gibt es keinen anderen COPY CTOR als "bitweises Kopieren", sodass der Zeiger selbst einfach kopiert wird.

Es gibt andere Möglichkeiten, um diesen Speicherverlust zu beheben, als einen intelligenten Zeiger zu verwenden. Eine Möglichkeit, Ihre eigene leicht modifizierte Version von std :: unique () als "your_company :: unique ()" zu schreiben. Der grundlegende Trick besteht darin, dass Sie anstelle des Kopierens eines Elements zwei Elemente austauschen würden. Und Sie müssten sicher sein, dass Sie anstatt zwei Zeiger zu vergleichen, BinaryPredicate aufrufen, das den beiden Zeigern auf das Objekt selbst folgt, und den Inhalt dieser beiden von "Monster" abgeleiteten Objekte vergleichen.

1) @SEE_ALSO: http://www.cplusplus.com/reference/algorithm/unique/

2) @SEE_ALSO: Was ist der effizienteste Weg, um Duplikate zu löschen und einen Vektor zu sortieren?

Der zweite Link ist hervorragend geschrieben und funktioniert für einen std :: vector, hat jedoch Speicherlecks und doppelte Freigaben (die manchmal zu Verstößen gegen die SEGMENTIERUNG führen) für einen std :: vector

3) @SEE_ALSO: valgrind (1). Dieses "Memory Leak" -Tool unter LINUX ist erstaunlich, was es finden kann! Ich kann es nur wärmstens empfehlen!

Ich hoffe, in einem zukünftigen Beitrag eine schöne Version von "my_company :: unique ()" veröffentlichen zu können. Im Moment ist es nicht perfekt, da ich möchte, dass die 3-Argumente-Version mit BinaryPredicate nahtlos für einen Funktionszeiger oder einen FUNCTOR funktioniert, und ich habe einige Probleme, beide richtig zu handhaben. WENN ich diese Probleme nicht lösen kann, werde ich veröffentlichen, was ich habe, und die Community versuchen lassen, das zu verbessern, was ich bisher getan habe.

Dennis Bednar
quelle
Dies scheint die Frage überhaupt nicht zu beantworten. Wenn Sie sich nur um die Möglichkeit mehrerer Zeiger auf dasselbe Objekt sorgen, sollten Sie nur einen intelligenten Zeiger mit Referenzzählung verwenden, z boost::smart_ptr.
beldaz