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_ptr
mit 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 delete
auß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 finally
Klausel) 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.