C++-Programmierung/ Speicherverwaltung/ Zusammenfassung

Aus Wikibooks
Zur Navigation springen Zur Suche springen


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 Objekten 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.

Crystal Project Tutorials.png
Syntax:
new «Datentyp» »(«Argumente»)«
delete «Speicheradresse»
«Nicht-C++-Code», »optional«

Auf das Objekt kann dann mit dem Dereferenzierungsoperator

*

, auf Klassenmember mit

->

zugegriffen werden. Beispiel:

Nuvola-inspired-terminal.svg
 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 struct MyData {
 6     int getCurrentYear() { return 2013; }
 7 };
 8 
 9 int main() {
10     int* val = new int;   // Speicheranforderung
11     *val = 42;   // eine einfache Zuweisung
12     cout << *val << endl;   // Ausgabe des Objekts
13     delete val;   // Freigabe des Speichers
14 // auskommentiert, doppeltes Löschen führt zum Programmabsturz
15 //  delete val;
16 
17     MyData* data = new MyData;   // Erstellung eines komplexeren Objekts
18     cout << data->getCurrentYear() << endl;   // Aufruf einer Membermethode
19     delete data;   // Speicherfreigabe
20     data = NULL;
21 // ab C++ 11 alternativ auch
22 //  data = nullptr;
23     delete data;   // okay, Nullpointer dürfen gelöscht werden
24 
25     return 0;
26 }
Symbol opinion vote.svg
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:

Nuvola-inspired-terminal.svg
1 int main() {
2     // Platz für 100 ints reservieren
3     int *myArray = new int[100];
4     // Zugriff erfolgt mittels Index-Operator
5     myArray[13] = 27;
6     // angeforderten Speicher abschließend löschen
7     delete[] myArray;
8 }

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:

Nuvola-inspired-terminal.svg
1 #include <string>
2 
3 int main() {
4     char* buffer = new char[1000];
5     std::string *p = new (buffer) string("Auf dem vorreservierten Buffer");
6 }

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.
Symbol opinion vote.svg
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:

Nuvola-inspired-terminal.svg
1 #include <memory>
2 
3 int main() {
4      std::auto_ptr<int> p1 (new int);
5     *p1.get()=10;
6 
7     std::auto_ptr<int> p2 (p1);
8 }

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:

Nuvola-inspired-terminal.svg
1 auto int i = 1;
2 int j = 1;

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

Symbol opinion vote.svg
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:
Nuvola-inspired-terminal.svg
1 auto i = 1;   // identisch zu int i = 1;
2 auto ptr = std::make_shared<MainWindow>(new MyMainWindow());
3 // identisch zu
4 // 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.