RAII als Muster
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 102 von 104.
RAII (Resource Acquisition Is Initialization) ist mehr als nur ein C++-Idiom – es ist ein leistungsstarkes Entwurfsmuster, das die Ressourcenverwaltung an die Lebensdauer von Objekten bindet. Du hast RAII bereits anhand von Smart-Pointern gesehen, aber das Muster lässt sich auf jede Ressource anwenden: Dateihandles, Netzwerk-connections, Mutexes oder Datenbanktransaktionen.
Die Grundidee ist einfach: acquire Ressourcen im constructor, sie im Destruktor freigeben. Da C++ garantiert, dass Destruktoren ausgeführt werden, wenn Objekte ihren Gültigkeitsbereich verlassen, geschieht die Bereinigung automatically – selbst wenn Ausnahmen auftreten:
#include <iostream>
#include <fstream>
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& filename) : file(filename) {
if (!file.is_open()) {
std::cout << "Failed to open file\n";
}
}
void write(const std::string& text) {
if (file.is_open()) file << text;
}
~FileGuard() {
if (file.is_open()) {
file.close();
std::cout << "File closed automatically\n";
}
}
};
int main() {
{
FileGuard guard("output.txt");
guard.write("Hello RAII");
} // Destruktor hier aufgerufen - Datei geschlossen
std::cout << "After scope\n";
}RAII glänzt bei der Verwaltung von Sperren in mehrthreadigem Code. Der std::lock_guard der Standardbibliothek folgt diesem Muster – er erwirbt beim Erstellen einen Mutex und gibt ihn beim Zerstören frei, wodurch Deadlocks aufgrund vergessener Entsperrungen verhindert werden.
Denke bei der Implementierung von RAII-Klassen daran, entweder delete zu verwenden oder die copy/move-Operationen ordnungsgemäß zu implementieren (Rule of Five), um Probleme durch Ressourcenduplizierung oder doppelte Freigabe zu verhindern. RAII verwandelt die fehleranfällige manuelle Ressourcenverwaltung in eine sichere, automatische Bereinigung.
Aufgabe
EinfachErstellen wir einen Connection Pool Manager unter Verwendung von RAII, um Datenbankverbindungen sicher zu verwalten. In realen Anwendungen sind Datenbankverbindungen kostspielige Ressourcen, die ordnungsgemäß erworben und freigegeben werden müssen. Du erstellst einen RAII-Wrapper, der garantiert, dass Verbindungen immer an den Pool zurückgegeben werden, selbst wenn Ausnahmen auftreten oder Codepfade kompliziert werden.
Du organisierst deinen Code auf drei Dateien:
ConnectionPool.h: Create einen einfachen Verbindungspool, der eine begrenzte Anzahl von connections verwaltet.Deine
ConnectionPool-class sollte verfolgen, wie viele connections available sind (beginnend mit einer capacity, die an den constructor übergeben wird). Implementiere:acquire()— wenn eine connection available ist, verringere den count und gibConnection acquired (X available)aus, wobei X der verbleibende count ist; gibtruezurück, wenn der Vorgang erfolgreich war, undfalse, wenn keine connections available sindrelease()— erhöhe den available count und gibConnection released (X available)ausavailable()— gibt die aktuelle Anzahl der available connections zurück
ConnectionGuard.h: Erstelle den RAII-Wrapper, der eine einzelne connection sicher verwaltet.Deine
ConnectionGuard-class verkörpert das RAII-Muster. Sie sollte:- in ihrem constructor eine Referenz auf einen
ConnectionPoolübernehmen und versuchen, eine connection zu erwerben - speichern, ob die acquisition erfolgreich war
- eine
isConnected()-Methode bereitstellen, um zu prüfen, ob der Guard eine gültige connection hält - die connection im destructor automatisch an den Pool zurückgeben (nur wenn eine erworben wurde)
- copy constructor und copy assignment löschen, um die Duplizierung von Ressourcen zu verhindern (Rule-of-
Five-Überlegung)
Wenn der destructor ausgeführt wird und eine connection gehalten wurde, gib
Guard releasing connectionaus, bevor release für den Pool aufgerufen wird.- in ihrem constructor eine Referenz auf einen
main.cpp: Demonstriere die automatische Bereinigung durch RAII mithilfe von Gültigkeitsbereichen.Lies zwei Eingaben ein:
- Pool capacity (Ganzzahl)
- Anzahl der anzufordernden connections (Ganzzahl)
Create einen
ConnectionPoolmit der angegebenen capacity. Erstelle dann innerhalb eines verschachtelten Gültigkeitsbereichs (unter Verwendung von geschweiften Klammern) die angeforderte Anzahl vonConnectionGuard-Objekten und speichere sie in einem Vektor. Gib für jeden Guard aus, ob die connection erfolgreich hergestellt wurde:- Wenn connected:
Guard N: Connected - Wenn nicht connected:
Guard N: Failed to connect
(wobei N bei 1 beginnt)
Nachdem der Gültigkeitsbereich endet (die Guards werden zerstört), gib
After scope: X connections availableaus, um den finalen Zustand des Pools anzuzeigen.
Beispielsweise mit den Eingaben 2 und 3:
Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections availableMit den Eingaben 3 und 2:
Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections availableBeachte, wie die connections automatisch freigegeben werden, wenn die Guards ihren Gültigkeitsbereich verlassen – du rufst in deinem main-Code niemals explizit release auf. Die Destruktoren werden in umgekehrter Reihenfolge der Erstellung ausgeführt (der letzte Guard wird zuerst zerstört), und jede erworbene connection wird garantiert zurückgegeben. Das ist die Stärke von RAII: Die Ressourcenbereinigung geschieht automatisch und zuverlässig, unabhängig davon, wie der Gültigkeitsbereich beendet wird.
Probier es selbst
#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"
using namespace std;
int main() {
// Eingaben lesen
int capacity;
int numConnections;
cin >> capacity;
cin >> numConnections;
// TODO: Erstelle einen ConnectionPool mit der gegebenen Kapazität
// TODO: Erstelle einen verschachtelten Gültigkeitsbereich mit geschweiften Klammern
{
// TODO: Erstelle einen vector zum Speichern von ConnectionGuard-Objekten
// Hinweis: Du musst Zeiger oder Smart Pointer verwenden, da ConnectionGuard
// einen gelöschten Kopierkonstruktor hat
// TODO: Schleife zum Erstellen von numConnections Guards
// Für jeden Guard gib entweder aus:
// "Guard N: Connected" or "Guard N: Failed to connect"
// wobei N bei 1 beginnt
}
// Guards werden hier zerstört, wenn der Gültigkeitsbereich endet
// TODO: Print "After scope: X connections available"
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins via CRTPPimpl-IdiomType ErasureEnum-Klassen & starke TypisierungException-Handling in OOPEigene Exception-Hierarchien14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterRAII als Muster3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische ContainerÜbe selbstständig: Online-C++-Compiler