C++-Programmierung/ Speicherverwaltung/ Zusammenfassung
Stack- und Heapspeicher
[Bearbeiten]In C++ ist der Speicher grob in vier Bereiche aufgeteilt:
- Programmspeicher: hier ist der Programmcode abgelegt
- globaler Speicher: in diesem Teil des Speichers werden globale Variablen gespeichert
- Halden- oder auch Heapspeicher: dynamisch zur Laufzeit erstellte Objekte werden hier abgelegt. Im Gegensatz zum Stackspeicher werden die Speicherplätze nicht geordnet vergeben
- Stapel- oder auch Stackspeicher: arbeitet nach dem LIFO-Prinzip, d. h. das zuletzt abgelegte Objekt wird auch zuerst herunter genommen. Hier werden lokale Variablen gespeichert
Auf dem Stackspeicher wird das zuletzt erstellte Objekt beim Verlassen seines umschließenden Blocks auch zuerst automatisch gelöscht. Dazu legt der Compiler schon zur Übersetzungszeit fest, dass Objekte erzeugt und später auch freigegeben werden sollen (statische Speicherverwaltung).
Bei der dynamischen Speicherverwaltung kann der Programmierer selber festlegen, wie viele und wann neue Objekte erzeugt und zerstört werden sollen. Objekte verlieren beim Zurückkehren aus einer Funktion ihre Gültigkeit nicht und können weiter verwendet werden. Im Gegensatz zum Stackspeicher ist die Größe des Heapspeichers nicht von den Compiler- oder Linkereinstellungen abhängig, sondern nur von der verfügbaren Hauptspeichermenge bzw. dem Betriebssystem. Ein Nachteil ist, dass der Programmierer sich auch um das Freigeben des vorher angeforderten Heapspeichers kümmern muss. Andernfalls können sog. Speicherlecks ("memory leaks") entstehen. Anders als andere Hochsprachen wie z. B. Java oder C# hat C++ keine "eingebaute" Freispeicherverwaltung. Eine Erleichterung stellen "intelligente" Zeiger dar, welche ihren Speicher automatisch freigeben, falls sie nicht mehr gebraucht werden.
Dynamische Freispeicherverwaltung
[Bearbeiten]new
und delete
[Bearbeiten]Die dynamische Speicherverwaltung ist in C++ mit den beiden Operatoren new
(zur Anforderung von Heapspeicher) und delete
(zur Freigabe des Speichers) realisiert. Im Normalfall gibt new
einen Pointer auf den reservierten Speicher zurück. Falls nicht genügend Speicher reserviert werden konnte, da nicht ausreichend Hauptspeicher verfügbar war, wird eine Ausnahme vom Typ std::bad_alloc
geworfen. Für weitere Erklärungen zum Thema Ausnahmen siehe Übersicht.
Auf das Objekt kann dann mit dem Dereferenzierungsoperator *
, auf Klassenmember mit ->
zugegriffen werden. Beispiel:
#include <iostream>
using namespace std;
struct MyData {
int getCurrentYear() { return 2013; }
};
int main() {
int* val = new int; // Speicheranforderung
*val = 42; // eine einfache Zuweisung
cout << *val << endl; // Ausgabe des Objekts
delete val; // Freigabe des Speichers
// auskommentiert, doppeltes Löschen führt zum Programmabsturz
// delete val;
MyData* data = new MyData; // Erstellung eines komplexeren Objekts
cout << data->getCurrentYear() << endl; // Aufruf einer Membermethode
delete data; // Speicherfreigabe
data = NULL;
// ab C++ 11 alternativ auch
// data = nullptr;
delete data; // okay, Nullpointer dürfen gelöscht werden
return 0;
}
Löschen Sie niemals einen bereits freigegebenen Speicher ein zweites Mal. Mit großer Sicherheit wird ihr Programm mit einem Speicherzugriffsfehler abstürzen. Umgehen lässt sich dieses Problem, indem Sie nach dem Löschen des Speicherbereichs den Zeiger auf NULL bzw. auf nullptr
(ab C++ 11) setzen. Das Löschen von Nullzeigern hat nämlich keinen Effekt.
Array-new
und Array-delete
[Bearbeiten]Das Anfordern von Speicher für Arrays erfolgt prinzipiell ähnlich. In eckigen Klammern wird die Größe des Arrays mit angegeben. Beim Löschen muss darauf geachtet werden statt delete
delete[]
zu schreiben. Beispiel:
int main() {
// Platz für 100 ints reservieren
int *myArray = new int[100];
// Zugriff erfolgt mittels Index-Operator
myArray[13] = 27;
// angeforderten Speicher abschließend löschen
delete[] myArray;
}
Placement new
[Bearbeiten]Mithilfe des Platzierungsoperators new
lassen sich Objekte auf einem vorher vorbereiteten Speicher platzieren. Analog funktioniert das Placement delete
. Hier ein kurzes Beispiel:
#include <string>
int main() {
char* buffer = new char[1000];
std::string *p = new (buffer) string("Auf dem vorreservierten Buffer");
}
Verwendet wird dieses Konzept, wenn die automatische Speicherbereinigung Thema ist, ein Speicherpool angelegt werden soll, man einfach die Leistung des Programms oder die Sicherheit bei Ausnahmen erhöhen will.
Smart Pointer
[Bearbeiten]Smart Pointer verhalten sich wie Zeiger, sie unterstützen die Dereferenzierung über *
und den indirekten Zugriff über ->
. Ein Vorteil ist, dass die von ihm verwaltete Ressource automatisch freigegeben wird, so dass kein Speicherleck entsteht. Die C++-Standardbibliothek (STL) stellt im Header memory
mehrere Arten von "intelligenten" Zeigern bereit:
std::auto_ptr
(bis C++11): ein einfacher Zeiger, mit Vorsicht zu genießen: Es darf immer nur einstd::auto_ptr
auf einen Speicherbereich zeigen, da es ansonsten zu Problemen führt. Wenn der verwaltete Speicher von einem Smart Pointer gelöscht wird verweist der andere Zeiger immer noch auf den Bereich, der möglicherweise schon wieder vergeben wurde. Bessere Alternativen bieten die mit dem C++11-Standard eingeführten, neuen Smart Pointerstd::unique_ptr
(ab C++11): ein "egoistischer" Smart Pointer. Es kann immer nur ein intelligenter Zeiger auf ein Speicherobjekt verweisenstd::shared_ptr
(ab C++11): umgeht die Beschränkungen des veraltetenstd::auto_ptr
: mehrere Zeiger teilen sich dasselbe Objekt, ohne dass es "versehentlich" gelöscht wird. Gelöscht wird das Objekt erst, wenn der letzte verweisende Smart Pointer gelöscht wirdstd::weak_ptr
(ab C++11): löscht das verwaltete Objekt erst, wenn die übrigen Referenzen nurstd::weak_ptr
sind.
Möglicherweise unterstützt ihr Compiler in den Standardeinstellungen keinen C++11-konformen Code. Mit dem Compilerflag -std=c++11 umgehen Sie dieses Problem. Voraussetzung ist ein C++11-konformer Compiler.
Beispiel:
#include <memory>
int main() {
std::auto_ptr<int> p1 (new int);
*p1.get()=10;
std::auto_ptr<int> p2 (p1);
}
Besondere Speicherklassenqualifizierer
[Bearbeiten]auto
[Bearbeiten]Das Schlüsselwort auto
in einer Variablendeklaration hat normalerweise keine Auswirkungen auf die Variable, da bei einer "normalen" Deklaration eine Variable implizit auto
ist. Somit sind diese beiden Deklarationen gleichwertig:
Die Lebenszeit der Variablen ist bis zum Ende ihres umgebenden Blocks, beim Verlassen werden sie gelöscht.
Neuerungen im C++11-Standard:
Mit dem C++11-Standard ändert sich die Bedeutung von auto
: es weist den Compiler an, selbstständig aus der Zuweisung den Variablentyp zu ermitteln (Typinferenz). Beispiel:
auto i = 1; // identisch zu int i = 1;
auto ptr = std::make_shared<MainWindow>(new MyMainWindow());
// identisch zu
// std::shared_ptr<MainWindow> ptr(new MyMainWindow());
Der Vorteil wird vor allem bei komplizierteren Deklarationen deutlich: man muss viel weniger tippen.
register
[Bearbeiten]Variablen, die mit register
gekennzeichnet wurden, sind Vorschläge für den Compiler, sie in den viel schnelleren Prozessorcache zu speichern anstatt in den langsameren Arbeitsspeicher. Vorteil ist natürlich die viel geringere Speicherzugriffszeit, jedoch sollten Sie dieses Schlüsselwort nur verwenden, wenn Sie ihren Compiler und ihr Betriebssystem sehr gut kennen.
static
[Bearbeiten]Statische Variablen und Funktionen stehen die gesamte Laufzeit zur Verfügung. Das hat verschiedene Folgen:
- Statische Variablen innerhalb von Funktionen verlieren nicht ihren Gültigkeit, wenn die Funktion wieder aufgerufen wird. Die Adresse und der Wert bleiben erhalten
- Die Deklaration ist in der gesamten, aktuellen Übersetzungseinheit gültig. Dadurch wird quasi ein Binärobjekt innerhalb eines Programmmoduls zur Speicherbereichsgrenze im gesamten Programm
- Bei Programmen mit mehreren Ausführungsfäden ("Threads") muss der Zugriff synchronisiert werden
- Mit
static
markierte Membervariablen sind auch ohne gültige Instanz zugreifbar. Allerdings sollten sie auch einmalig definiert werden, damit keine komischen Bugs entstehen
extern
[Bearbeiten]Mit extern
deklarierte Variablen bekommen anderen Übersetzungseinheiten einen Wert zugewiesen und wird eher selten gebraucht. Es wird allgemein empfohlen, auf dieses Schlüsselwort zu verzichten.