C++-Programmierung/ Speicherverwaltung/ Zusammenfassung

Aus Wikibooks


Stack- und Heapspeicher[Bearbeiten]

In C++ ist der Speicher grob in vier Bereiche aufgeteilt:

  1. Programmspeicher: hier ist der Programmcode abgelegt
  2. globaler Speicher: in diesem Teil des Speichers werden globale Variablen gespeichert
  3. Halden- oder auch Heapspeicher: dynamisch zur Laufzeit erstellte Objekte werden hier abgelegt. Im Gegensatz zum Stackspeicher werden die Speicherplätze nicht geordnet vergeben
  4. 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.

Syntax:
new «Datentyp» »(«Argumente»)«
delete «Speicheradresse»
«Nicht-C++-Code», »optional«

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;
}
Hinweis

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 ein std::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 Pointer
  • std::unique_ptr (ab C++11): ein "egoistischer" Smart Pointer. Es kann immer nur ein intelligenter Zeiger auf ein Speicherobjekt verweisen
  • std::shared_ptr (ab C++11): umgeht die Beschränkungen des veralteten std::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 wird
  • std::weak_ptr (ab C++11): löscht das verwaltete Objekt erst, wenn die übrigen Referenzen nur std::weak_ptr sind.
Hinweis

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:

auto int i = 1;
int j = 1;

Die Lebenszeit der Variablen ist bis zum Ende ihres umgebenden Blocks, beim Verlassen werden sie gelöscht.

Hinweis

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.