it-swarm.com.de

RAII und Smart Pointer in C ++

In der Praxis mit C++, was ist RAII , was sind intelligente Zeiger , wie werden diese in einem Programm implementiert und welche Vorteile ergeben sich daraus von RAII mit intelligenten Zeigern?

189
Rob Kam

Ein einfaches (und möglicherweise überlastetes) Beispiel für RAII ist eine File-Klasse. Ohne RAII könnte der Code ungefähr so ​​aussehen:

File file("/path/to/file");
// Do stuff with file
file.close();

Mit anderen Worten, wir müssen sicherstellen, dass wir die Datei schließen, sobald wir damit fertig sind. Dies hat zwei Nachteile: Erstens müssen wir, wo immer wir File verwenden, File :: close () aufrufen. Wenn wir dies vergessen, halten wir uns länger an die Datei als nötig. Das zweite Problem ist, was passiert, wenn eine Ausnahme ausgelöst wird, bevor wir die Datei schließen?

Java löst das zweite Problem mit einer finally-Klausel:

try {
    File file = new File("/path/to/file");
    // Do stuff with file
} finally {
    file.close();
}

oder seit Java 7, eine Try-with-Resource-Anweisung:

try (File file = new File("/path/to/file")) {
   // Do stuff with file
}

C++ löst beide Probleme mit RAII - das heißt, das Schließen der Datei im Destruktor von File. Solange das File-Objekt zum richtigen Zeitpunkt zerstört wird (was es auch sein sollte), ist das Schließen der Datei für uns erledigt. Unser Code sieht nun ungefähr so ​​aus:

File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us

Dies ist in Java nicht möglich, da nicht garantiert werden kann, wann das Objekt zerstört wird. Daher können wir nicht garantieren, wann eine Ressource wie eine Datei freigegeben wird.

Bei intelligenten Zeigern erstellen wir häufig nur Objekte auf dem Stapel. Zum Beispiel (und ein Beispiel aus einer anderen Antwort zu stehlen):

void foo() {
    std::string str;
    // Do cool things to or using str
}

Das funktioniert gut - aber was ist, wenn wir str zurückgeben wollen? Wir könnten dies schreiben:

std::string foo() {
    std::string str;
    // Do cool things to or using str
    return str;
}

Also, was ist daran falsch? Nun, der Rückgabetyp ist std :: string - das heißt, wir geben nach Wert zurück. Dies bedeutet, dass wir str kopieren und die Kopie tatsächlich zurückgeben. Dies kann teuer sein, und wir möchten möglicherweise die Kosten für das Kopieren vermeiden. Daher könnten wir auf die Idee kommen, per Referenz oder Zeiger zurückzukehren.

std::string* foo() {
    std::string str;
    // Do cool things to or using str
    return &str;
}

Leider funktioniert dieser Code nicht. Wir geben einen Zeiger auf str zurück - aber str wurde auf dem Stack erstellt, sodass wir gelöscht werden, sobald wir foo () beenden. Mit anderen Worten, wenn der Aufrufer den Zeiger erhält, ist er unbrauchbar (und vermutlich schlimmer als unbrauchbar, da die Verwendung allerlei irre Fehler verursachen könnte).

Also, was ist die Lösung? Wir könnten mit new str auf dem Heap erzeugen - wenn foo () abgeschlossen ist, wird str nicht zerstört.

std::string* foo() {
    std::string* str = new std::string();
    // Do cool things to or using str
    return str;
}

Natürlich ist diese Lösung auch nicht perfekt. Der Grund ist, dass wir str erstellt haben, es aber niemals löschen. Dies ist möglicherweise kein Problem in einem sehr kleinen Programm, aber im Allgemeinen möchten wir sicherstellen, dass es gelöscht wird. Wir könnten einfach sagen, dass der Anrufer das Objekt löschen muss, wenn er damit fertig ist. Der Nachteil ist, dass der Aufrufer den Speicher verwalten muss, was die Komplexität erhöht und zu Fehlern führen kann, was zu einem Speicherverlust führt, d. H., Dass Objekte nicht gelöscht werden, obwohl sie nicht mehr benötigt werden.

Hier kommen intelligente Zeiger ins Spiel. Im folgenden Beispiel wird shared_ptr verwendet. Ich schlage vor, Sie sehen sich die verschiedenen Arten intelligenter Zeiger an, um zu erfahren, was Sie tatsächlich verwenden möchten.

shared_ptr<std::string> foo() {
    shared_ptr<std::string> str = new std::string();
    // Do cool things to or using str
    return str;
}

Shared_ptr zählt nun die Anzahl der Verweise auf str. Zum Beispiel

shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;

Jetzt gibt es zwei Verweise auf dieselbe Zeichenfolge. Sobald keine weiteren Verweise auf str mehr vorhanden sind, werden diese gelöscht. Sie müssen sich also nicht mehr um das Löschen selbst kümmern.

Schnelle Bearbeitung: Wie in einigen Kommentaren bereits erwähnt, ist dieses Beispiel aus (zumindest!) Zwei Gründen nicht perfekt. Erstens ist das Kopieren einer Zeichenfolge aufgrund der Implementierung von Zeichenfolgen in der Regel kostengünstig. Zweitens ist die Rückgabe nach Wert aufgrund der so genannten Rückgabewertoptimierung möglicherweise nicht teuer, da der Compiler eine gewisse Cleverness anwenden kann, um die Dinge zu beschleunigen.

Versuchen wir also ein anderes Beispiel mit unserer File-Klasse.

Angenommen, wir möchten eine Datei als Protokoll verwenden. Dies bedeutet, dass wir unsere Datei nur im Anhänge-Modus öffnen möchten:

File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log

Legen wir nun unsere Datei als Protokoll für einige andere Objekte fest:

void setLog(const Foo & foo, const Bar & bar) {
    File file("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

Leider endet dieses Beispiel schrecklich - die Datei wird geschlossen, sobald diese Methode endet, was bedeutet, dass foo und bar jetzt eine ungültige Protokolldatei haben. Wir könnten eine Datei auf dem Heap erstellen und einen Zeiger auf eine Datei sowohl an foo als auch an bar übergeben:

void setLog(const Foo & foo, const Bar & bar) {
    File* file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

Aber wer ist dann für das Löschen der Datei verantwortlich? Wenn keine der beiden Dateien gelöscht wird, liegt sowohl ein Speicher- als auch ein Ressourcenleck vor. Wir wissen nicht, ob foo oder bar zuerst mit der Datei fertig werden, daher können wir auch nicht damit rechnen, die Datei selbst zu löschen. Wenn zum Beispiel foo die Datei löscht, bevor der Balken damit fertig ist, hat der Balken jetzt einen ungültigen Zeiger.

Wie Sie vielleicht erraten haben, könnten wir intelligente Zeiger verwenden, um uns zu helfen.

void setLog(const Foo & foo, const Bar & bar) {
    shared_ptr<File> file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

Jetzt muss sich niemand mehr um das Löschen von Dateien kümmern - sobald sowohl foo als auch bar fertig sind und keine Verweise auf Dateien mehr vorhanden sind (wahrscheinlich weil foo und bar zerstört wurden), werden Dateien automatisch gelöscht.

311

Die Prämisse und die Gründe sind im Konzept einfach.

RAII ist das Entwurfsparadigma, um sicherzustellen, dass Variablen die gesamte erforderliche Initialisierung in ihren Konstruktoren und die gesamte erforderliche Bereinigung in ihren Destruktoren verarbeiten. Dies reduziert die gesamte Initialisierung und Bereinigung auf einen einzigen Schritt.

C++ erfordert kein RAII, aber es wird zunehmend akzeptiert, dass die Verwendung von RAII-Methoden robusteren Code erzeugt.

Der Grund, warum RAII in C++ nützlich ist, besteht darin, dass C++ die Erstellung und Zerstörung von Variablen beim Ein- und Verlassen des Gültigkeitsbereichs eigenständig verwaltet, sei es durch normalen Code-Fluss oder durch Stack-Unwinding, ausgelöst durch eine Ausnahme. Das ist ein Werbegeschenk in C++.

Indem Sie die gesamte Initialisierung und Bereinigung mit diesen Mechanismen verknüpfen, stellen Sie sicher, dass C++ diese Arbeit auch für Sie erledigt.

Wenn in C++ über RAII gesprochen wird, werden in der Regel intelligente Zeiger diskutiert, da Zeiger bei der Bereinigung besonders anfällig sind. Bei der Verwaltung von Heap-reserviertem Speicher, der von malloc oder new bezogen wurde, liegt es normalerweise in der Verantwortung des Programmierers, diesen Speicher freizugeben oder zu löschen, bevor der Zeiger zerstört wird. Intelligente Zeiger verwenden die RAII-Philosophie, um sicherzustellen, dass vom Heap zugewiesene Objekte jedes Mal zerstört werden, wenn die Zeigervariable zerstört wird.

32
Drew Dormann

Smart Pointer ist eine Variation von RAII. RAII bedeutet, dass die Ressourcenbeschaffung eine Initialisierung ist. Der intelligente Zeiger erfasst eine Ressource (Speicher) vor der Verwendung und wirft sie dann automatisch in einen Destruktor. Zwei Dinge passieren:

  1. Wir ordnen Speicher zu, bevor wir es verwenden, immer, auch wenn wir keine Lust dazu haben - es ist schwierig, mit einem intelligenten Zeiger einen anderen Weg zu gehen. Wenn dies nicht der Fall ist, versuchen Sie, auf den NULL-Speicher zuzugreifen, was zu einem Absturz führt (sehr schmerzhaft).
  2. Wir geben Speicher frei, auch wenn ein Fehler vorliegt. Es bleibt kein Speicher hängen.

Ein anderes Beispiel ist beispielsweise der Netzwerk-Socket RAII. In diesem Fall:

  1. Wir öffnen Netzwerk-Socket bevor wir es benutzen, immer, auch wenn wir keine Lust haben - mit RAII ist es schwierig, es anders zu machen. Wenn Sie dies ohne RAII versuchen, können Sie einen leeren Socket für MSN-Verbindung öffnen. Dann wird eine Nachricht wie "Lass es uns heute Nacht machen" möglicherweise nicht übertragen, Benutzer werden nicht gelegt und du riskierst möglicherweise, gefeuert zu werden.
  2. Wir schließen Netzwerk-Socket, auch wenn ein Fehler vorliegt. Es bleibt kein Socket hängen, da dies verhindern könnte, dass die Antwortnachricht "sure ill be on bottom" den Absender zurückschlägt.

Nun, wie Sie sehen können, ist RAII in den meisten Fällen ein sehr nützliches Werkzeug, da es den Menschen hilft, sich zu legen.

C++ - Quellen für intelligente Zeiger liegen in Millionenhöhe im Netz, einschließlich der Antworten über mir.

8
mannicken

Boost hat eine Reihe von diesen, einschließlich der in Boost.Interprocess für gemeinsam genutzten Speicher. Dies vereinfacht die Speicherverwaltung erheblich, insbesondere in Situationen, in denen Kopfschmerzen auftreten, z. B. wenn 5 Prozesse dieselbe Datenstruktur gemeinsam nutzen: Wenn alle mit einem Speicherblock fertig sind, möchten Sie, dass dieser automatisch freigegeben wird und Sie nicht mehr dort sitzen müssen, um dies herauszufinden Wer sollte für das Aufrufen von delete für einen Teil des Speichers verantwortlich sein, damit Sie nicht zu einem Speicherverlust kommen oder zu einem Zeiger, der fälschlicherweise zweimal freigegeben wird und möglicherweise den gesamten Heap beschädigt.

2
Jason S
 void foo () 
 {
 std :: string bar; 
 //
 // more code here 
 // 
} 

Egal was passiert, die Leiste wird ordnungsgemäß gelöscht, sobald der Umfang der Funktion foo () verlassen wurde.

Interne std :: string-Implementierungen verwenden häufig Zeiger mit Referenzzählung. Die interne Zeichenfolge muss also nur kopiert werden, wenn eine der Kopien der Zeichenfolgen geändert wurde. Ein intelligenter Zeiger mit Referenzzählung ermöglicht es daher, nur dann etwas zu kopieren, wenn dies erforderlich ist.

Darüber hinaus ermöglicht die interne Referenzzählung, dass der Speicher ordnungsgemäß gelöscht wird, wenn die Kopie der internen Zeichenfolge nicht mehr benötigt wird.

0
Juan