C++-Programmierung/ Speicherverwaltung

Speicherverwaltung

Zielgruppe:

Anfänger

Lernziel:
Die Speicherverwaltung in C++ kennen lernen.


Stack und Heap [Bearbeiten]

C++ unterteilt den verfügbaren Speicher in vier Bereiche. Diese sind der

  1. Programmspeicher,
  2. globale Speicher für globale Variable,
  3. Haldenspeicher für die dynamische Speicherverwaltung, und
  4. Stapelspeicher (statische Speicherverwaltung).[1]

Der Programmspeicher beinhaltet, wie der Name schon verrät, das Programm. In zahlreichen Sprachen ist er strikt vom Datenspeicher getrennt. Manche Sprachen erlauben aus programmiertechnischen Gründen keine globalen Variablen. Bei C++ sind diese zwar erlaubt, es wird aber zwischen Programm- und Datenspeicher grundsätzlich unterschieden.

Neben dem Speicher für globale Variable bleiben noch zwei Bereiche für die Daten. Einer dieser Bereiche wird als Stapelspeicher oder kurz Stapel (stack) bezeichnet und wir haben ihn schon häufig in Anspruch genommen. Den zweiten Speicherbereich bezeichnet man als Haldenspeicher oder kurz als Halde (heap). Er dient der dynamischen Speicherverwaltung und wird in diesem Abschnitt umfassend behandelt.

Für den Stapelspeicher gilt immer: Was zuletzt angefordert wurde, muss auch als erstes wieder freigegeben werden (LIFO: Last In – First Out). Wenn Sie innerhalb eines Blocks {;;;} also Variablen anlegen, werden diese auf dem Stack angelegt. Am Ende des Blocks verliert die Variable ihre Gültigkeit und der Speicher wird wieder freigegeben. Wenn Sie nun eine Funktion aufrufen, wird die aktuelle Programmadresse (also die Stelle im Programm, an der die Funktion aufgerufen wird, die sog. „Rücksprungadresse“) auf dem Stapel abgelegt. Innerhalb der Funktionen werden möglicherweise Variablen angelegt, die wiederum auf dem Stapel landen. Dass dies so geschehen soll, wird vom Compiler zur Übersetzungszeit festgelegt und ist somit eine statische Speicherverwaltung. Am Ende der Funktion werden die Speicherbereiche der Variablen wieder freigegeben und das Programm springt zur Rücksprungadresse, die jetzt wieder oben auf dem Stapel liegt. Somit befindet es sich jetzt wieder an der Stelle, an der die Funktion aufgerufen wurde.

Speicher aus der Halde wird nicht geordnet vergeben. Sie können ihn zu einem beliebigen Zeitpunkt anfordern und müssen ihn auch selbst wieder freigeben. Somit kann innerhalb einer Funktion Haldenspeicher angefordert, und nach Beendigung der Funktion ein Objekt, das auf der Halde liegt, weiterhin genutzt werden. Es wird also nicht mit Beendigung der Funktion ungültig. Versucht ein Objekt so, Speicher für sich zu reservieren, wird dieser im Rahmen der sogenannten dynamischen Speicherverwaltung zur Laufzeit festgelegt.

In den folgenden Kapiteln lernen Sie in erster Linie, wie man in C++ mit Haldenspeicher arbeitet.

Objekte Erstellen und Zerstören

Bearbeiten

Im Stack (deutsch: Stapel)

Bearbeiten

Effektive Objekte können nur für den aktuellen Gültigkeitsbereich auf dem sog. Stack erstellt werden. Der Stapelspeicher ist ein Speicherbereich für lokale Variablen eines Moduls (statische Speicherverwaltung). Beim Verlassen eines Gültigkeitsbereichs werden diese Objekte automatisch zerstört. Alle vorigen Beispiele in diesem Abschnitt zeigen, wie Objekte auf dem Stack erstellt und zerstört werden. Größere Objekte wie große Speicherblöcke sollten nur in Beispielanwendungen auf dem Stack erstellt werden, denn dieser Bereich ist stark begrenzt und ist ausschließlich für lokale und temporäre Daten gedacht. Moderne Übersetzer begrenzen diesen Bereich auf 1 Megabyte. Wenn Sie größere Objekte auf den Stack legen wollen, müssen Sie die maximale Stackgröße modifizieren! Tun Sie dies nicht, erhalten Sie höchst bemerkenswerte Meldungen von Laufzeitumgebung, Betriebssystem oder Programmabbrüchen.

Auf dem Heap (deutsch: Halde)

Bearbeiten

Effektive Objekte können dynamisch und permanent bis zum Ende der Laufzeit des Moduls erstellt werden. Dies erfolgt im sog. Heap. Der Heap entspricht meistens dem nicht vorgespeicherten Datensegment für das gesamte Programm (dynamische Speicherverwaltung). Dazu verwendet man den Operator new. Wenn ein Objekt nicht mehr benötigt wird, muss es bei dieser Variante manuell zerstört werden und zwar mit dem Operator delete. Weiterführende Konzepte wie Smart-Pointer können das Zerstören beim Verlassen von Gültigkeitsbereichen automatisieren. Diese Operatoren kann man auch für Felder verwenden. Dann muss allerdings bei der Zerstörung der Operator delete [] verwendet werden.

Der Heap hat den eklatanten Vorteil, dass die Grenzen des zuteilbaren Speichers nur vom Betriebssystem und der physikalischen Speichermenge gezogen werden und nicht von Compiler- und Linkereinstellungen. Ein weiterer Vorteil ist, dass alle Elemente einer Klasse dann auch auf dem Heap liegen.

Wir verwenden Klasse 'a' aus vorigem Beispiel:

int main(){
  A *pObjekt(0); // Zeiger auf ein A-Objekt
  pObjekt = new A; // Instanziieren auf dem Heap, Standardkonstruktor verwenden
  delete pObjekt; // Zerstören
  char *pszMemory = new char[0x100000]; // 1 Megabyte auf dem Heap allozieren
  delete [] pszMemory; // Speicherblock wieder freigeben
  A *ar_Objekte = new A[50]; // 50 Objekte von A anlegen
  delete [] ar_Objekte;

  return 0;
}

Weitere Optionen zur Verwendung von new, delete, new [] und delete [] gibt es auch.

Referenzen

Bearbeiten
  1. 7.9 — The stack and the heap (Zugriff 2011-06-17)

new und delete [Bearbeiten]

C++ bietet die Möglichkeit der sogenannten Freispeicherverwaltung. Das heißt, Sie können zu jedem Zeitpunkt eine beliebige Menge an Speicher anfordern (vom Betriebssystem). Allerdings müssen Sie diesen Speicher später auch selbst wieder frei geben (ans Betriebssystem zurückgeben), was Gefahren nach sich zieht: Sie können vergessen, nicht mehr benötigten Speicher zurückzugeben - das Betriebssystem kann ohne einen expliziten Hinweis (also das Freigeben des Speichers durch Ihr Programm) nicht wissen, ob der Speicher noch benötigt wird. Das unnötige Vorhandensein nicht mehr benötigten Speichers bezeichnet man als Speicherleck (engl. „Memory Leak“) oder auch Speicherleiche. Umgekehrt wird auch das versehentliche Freigeben von eigentlich noch benötigtem Speicher als Speicherleck bezeichnet (es führt zu "dangling pointers").

Andere Sprachen, wie etwa Java, rücken diesem Problem mit einer sogenannten „Garbage Collection“ (englisch für Müllabfuhr; in diesem Fall für „Automatische Speicherbereinigung“) zu Leibe. Dabei wird im Hintergrund periodisch nach vom Programm angeforderten Speicherbereichen gesucht, auf die keine Variable/Zeiger im Programm mehr verweist. C++ besitzt kein derartiges Werkzeug, was zwar zu einer höheren Performance führt, aber andererseits auch eine besondere Sorgfalt von Ihnen als Programmierer verlangt. Im Lauf dieses Kapitels werden Sie jedoch Hilfsmittel kennenlernen, die Sie tatkräftig bei der Vermeidung von Speicherlecks unterstützen.

Anforderung und Freigabe von Speicher

Bearbeiten

Angefordert wird Speicher in C++ über den Operator new. Im Gegensatz zur C-Variante wird in C++ immer Speicher für einen bestimmten Datentyp angefordert. Dieser darf allerdings nicht const oder volatile und auch keine Funktion sein. Der new-Operator erhält als Argument also einen Datentyp, anhand dessen er die Menge des benötigten Speichers selbstständig bestimmt. An den Konstruktor des Datentyps können, wenn nötig, Argumente übergeben werden, denn new gibt nicht nur einen Speicherbereich zurück, sondern legt in diesem Speicherbereich auch gleich noch ein Objekt des übergebenen Typs an. Zu beachten ist hierbei, dass für einen Aufruf ohne zusätzliche Konstruktorargumente keine leeren Klammern angegeben werden dürfen.

Syntax:
new «Datentyp» »(«Konstruktorargumente»)«
«Nicht-C++-Code», »optional«

new gibt bei erfolgreicher Durchführung einen Zeiger auf den entsprechenden Datentyp zurück. Diesen Zeiger müssen Sie dann (beispielsweise in einer Variable) speichern. Wenn Sie das Objekt nicht mehr benötigen, müssen Sie es mittels delete-Operator wieder freigeben. delete erwartet zum Löschen den Zeiger, den new Ihnen geliefert hat. Analog zum Konstruktoraufruf bei new ruft delete zunächst den Destruktor des Objekts auf und gibt anschließend den Speicher an das Betriebssystem zurück.

Syntax:
delete «Speicheradresse»
«Nicht-C++-Code», »optional«
Hinweis

Lassen Sie niemals eine Speicheradresse zweimal durch delete löschen, denn dies führt fast immer zum Programmabsturz. Am einfachsten vermeiden Sie dies, indem Sie Ihre Zeigervariable nach dem Löschen auf 0 setzen, denn das Löschen eines Nullzeigers durch delete ist gestattet.

Die Situation, dass ein Speicherbereich zweimalig freigegeben werden soll, ist zudem meist ein Hinweis auf eine inkonsistente Programmstruktur - etwas stimmt mit der Programmlogik nicht.

Im Folgenden sehen Sie noch mal ein kleines Beispiel, bei dem ein double dynamisch angelegt wird. Dabei wird beim zweiten Anlegen ein Argument zur Initialisierung angegeben, was einem Konstruktorargument entspricht.

#include <iostream>

int main(){
    double* zeiger = new double;       // Anfordern eines doubles

    std::cout << *zeiger << std::endl; // gibt den (zufälligen) Wert des doubles aus

    delete zeiger;                     // Löschen der Variable
    zeiger = 0;                        // Zeiger auf 0 setzen
    delete zeiger;                     // ok, weil Zeiger 0 ist (nur zur Vorführung)

    zeiger = new double(7.5);          // Anfordern eines doubles, initialisieren mit 7.5

    std::cout << *zeiger << std::endl; // gibt 7.5 aus

    delete zeiger;                     // Löschen der Variable
}

Falls beim Anfordern des Speichers durch new etwas schief geht, etwa weil nicht mehr genug Speicher zur Verfügung steht, wird eine std::bad_alloc-Ausnahme geworfen. Sie können also nach einem new-Aufruf davon ausgehen, dass dieser erfolgreich war, müssen aber mittels eines try-Blocks eventuell geworfene Ausnahmen abfangen. Mehr Informationen hierzu finden Sie im Kapitel „Übersicht“ zur Ausnahmebehandlung.

Array-new und Array-delete [Bearbeiten]

Besonders nützlich ist diese Art, Speicher zu reservieren, beim Erstellen dynamischer Datenfelder (engl. arrays) oder bei Listen - denn deren Umfang/Länge ergibt sich häufig erst während des Programmlaufs. Hierzu wird nach der Angabe des Datentyps die Anzahl als int angegeben:

Syntax:
new «Datentyp»[«Menge»];
«Nicht-C++-Code», »optional«

Das Freigeben des Speichers, wenn die Daten nicht mehr benötigt werden, funktioniert für einfache Datenfelder ähnlich wie für einzelne Variablen mit delete[].

Dies kann im Code zum Beispiel so aussehen:

#include <iostream>

int main(){
    int *dyn_array = 0;
    int groesse;

    std::cout  << "Wie gross soll das Datenfeld werden?" << std::endl;
    std::cin >> groesse;
    dyn_array = new int[groesse];            // Anfordern eines Datenfeld aus int-Werten

    for (int i = 0; i < groesse; ++i){
        dyn_array[i] = i;                    // Wiederholung aus Kapitel "Zeigerarithmetik":
                                             //  dyn_array[i] entspricht *(dyn_array + i)
    }

    for (int i = 0; i < groesse; ++i){
        std::cout << dyn_array[i] << std::endl;
    }

    delete[] dyn_array;                      // Das Freigeben des Speichers funktioniert 
                                             // wie für einzelne Werte
    dyn_array = 0;                           // Pointer definiert auf Null zeigen lassen
}

Erwähnenswert ist an dieser Stelle noch, dass sich Arrays beim Anlegen nicht initialisieren lassen. Man kann aber natürlich, wie im Beispiel, nach dem Anlegen das gesamte Array mit Werten belegen, beispielsweise mit einer for-Schleife.

Weiterhin ist zu beachten, dass (bei mehrdimensionalen Arrays) nur die erste Dimension dynamisch erzeugt werden kann. Für weitere Dimensionen können nur Konstanten eingesetzt werden. (Allerdings kann man mit Kniffen jedes mehrdimensionale Array als eindimensionales mit entsprechendem Zugriff darstellen.)

Placement new [Bearbeiten]

Neben dem in C++ verwendeten Operator new, der auf dem Haldenspeicher Platz reserviert, unterstützt Standard-C++ auch den sogenannten Platzierungsoperator new (engl. placement new), der es ermöglicht, ein neues Objekt einem vorbereiteten Puffer zuzuordnen. Hier der Vergleich der unterschiedlichen Speicherreservierungen:

void placement() {
	string *q = new string("hi");		// Normale Speicherreservierung auf dem Haldenspeicher
	char *buf  = new char[1000];		// In voraus reservierter Pufferspeicher

	string *p = new (buf) string("hi");	// Placement new
}

Verwendet wird dieses Konzept, wenn

Nachteil ist die höhere Komplexität, für anzulegende Objekte zusätzlich Buffer vorzuhalten und zu verwalten, sowie auf deren Größenbeschränkungen acht zu geben. Derartige Aufgaben erledigt beim einfachen new die Sprache und das Betriebssystem für das Programm (und den Programmierer).

Smart Pointer [Bearbeiten]

Als smart guy wird auf Englisch jemand bezeichnet, der vornehm und gebildet ist. In Analogie zu diesem Begriff versteht man unter einem smart pointer ein Objekt, das sich wie ein Zeiger verhält, d.h. es muss Zeigeroperationen wie die Dereferenzierung mit * oder den indirekten Zugriff mit -> unterstützen. Zusätzlich zu diesen Eigenschaften geht der Smart pointer besser mit den Ressourcen um. Konkret bedeutet das, dass er darauf aufpasst, kein Speicherleck entstehen zu lassen.

Das einfachste Beispiel eines Smart pointers ist der in der C++-Bibliothek inkludierte auto_ptr, der in der Header-Datei von <memory> definiert wird. Hier ein Einblick in die Implementation des Auto pointers:

template <typename T>
class auto_ptr{
    T* ptr;
public:
    explicit auto_ptr(T* p = 0) : ptr(p) {}
    ~auto_ptr()                 { delete ptr; }
    T& operator*()              { return *ptr; }
    T* operator->()             { return ptr; }
    // ...
};

Man sieht, dass auto_ptr praktisch eine Hülle um einen primitiven Zeiger darstellt. Wichtig ist, dass der Destruktor den Speicher des Zeigers freigibt, den die Klasse als privaten Member beinhaltet. Weil der Destruktor eines Objekts automatisch aufgerufen wird beim Verlassen des Gültigkeitsbereichs (z. B. der Methode), kann ein delete nicht mehr vergessen werden. Nachteile: Die Methode kann das Objekt, auf das ptr verweist, nicht als Ergebnis zurückgeben - es wird ja automatisch freigegeben. Außerdem kann der belegte Speicher nicht vorzeitig freigegeben werden - außer, man ruft explizit den Destructor auf. In Folge kann danach nicht mehr auf den auto_ptr zugegriffen werden - das Problem des dangling pointers ist in diesem Fall verlagert zu einem ungültigen Zugriff(sversuch) auf ein bereits 'destructed' Objekt.

Beispiele

Bearbeiten

Verwendet man Smart pointers, wird an Stelle des Codes

void foo(){
    MyClass* p = new MyClass();

    p->DoSomething();

    delete p;
}

die kürzere Form

void foo(){
    auto_ptr<MyClass> p(new MyClass);

    p->DoSomething();
}

verwendet, und man kann darauf vertrauen, dass der Speicher, auf den der Zeiger p verweist, am Ende der Funktion wieder freigegeben wird.

Smart Pointer mit Referenzzählung

Bearbeiten

Einen Nachteil hat auto_ptr jedoch. Es darf niemals mehr als ein auto_ptr auf den von ihm verwalteten Speicher zeigen. Diese Limitation zeigt sich z.B. beim Kopieren und Zuweisen:

void foo() {
    auto_ptr<MyClass> p(new MyClass);
    auto_ptr<MyClass> p2(p);
    p = p2;
}

In Zeile 3 wird der Kopierkonstruktor aufgerufen - dieser ist für auto_ptr jedoch so definiert, dass er nach dem Kopieren den internen Zeiger der Vorlage (hier p.ptr) auf null setzt. p2 zeigt nun auf das Objekt, p hingegen ist nun null. Zeile 4 hat die gleichen Auswirkungen (nur umgekehrt - jetzt enthält wieder p das Objekt und p2.ptr ist null).

Eine mögliche Lösung zu diesem Problem ist, Smart Pointer zu benutzen, die anders mit dem Speicher umgehen und somit mehrere Zeiger auf den gleichen Speicher erlauben. Die Standardbibliothek bietet für diesen Zweck den Smart Pointer shared_ptr im Namensraum std::tr1 an. Die Speicherverwaltung bei shared_ptr erfolgt über Referenzzählung. Ein Zähler hält fest, wie viele Objekte auf den Speicher zeigen. Fällt der Zähler auf 0, wird der verwaltete Speicher freigegeben. Die Benutzung erfolgt ganz wie bei auto_ptr:

void foo(){
    tr1::shared_ptr<MyClass> p(new MyClass);
    tr1::shared_ptr<MyClass> p2(p);
}

Die Anweisung in Zeile 3 liefert nun eine Kopie in p2, ohne p auf null zu setzen.

Speicherklassen [Bearbeiten]

Wo werden Variablen angelegt? Im Datenbereich (Datensegment), auf dem Stapelspeicher (Stack), auf der Halde (Heap)? C++ erlaubt, in eingeschränktem Maße selbst zu wählen.

Speicherklassen

Bearbeiten

Diese Speicherklasse zu wählen, ist nicht nötig, da implizit immer auto verwendet wird, wenn innerhalb von Quelltexten eine Variable deklariert wird. Diese Variable bekommt dann automatisch Speicher auf dem Stack zugeteilt. Sobald ein Gültigkeitsbereich verlassen wird, in dem eine Variable mit Speicherklasse auto erzeugt wurde, wird dieser Speicher wieder freigegeben.

Hinweis

Bemerkung zum neuen C++2011-Standard:

Seit dem neuen Standard hat das Schlüsselwort auto eine neue Bedeutung und zwar die der automatischen Typbestimmung von Variablen. Das bedeutet, wenn man vor einer Variablen auto setzt, so wird nicht mehr verlangt, ein Typ wie int explizit davor zu schreiben.

Beispiel:

//x ist automatisch double
auto x = 3.14; // Korrekt nach C++11

Implementiert wurde dies im g++ seit Version 4.4[1]

register

Bearbeiten

Durch die Verwendung der Speicherklasse register wird dem Compiler der Hinweis gegeben, den Speicherbereich für eine Variable in möglichst schnellen Speicher zu legen. In der Vergangenheit, insbesondere bei klassischem C wurde register dafür verwendet, um die Speicherung einer Variablen eines Integraltyps in einem Prozessorregister zu erzwingen. Aktuelle Compiler versuchen heutzutage Variablen jeden Typs, die mit register deklariert wurden, in 'gecachten' Prozessorspeicher zu legen oder teilen bei der Speicherallokation dem Betriebssystem mit, dass die entsprechenden Speicherseiten nicht in virtuellem Speicher ausgelagert werden dürfen. Sie sollten register nur verwenden, wenn Sie Ihren Compiler und die Zielplattform sehr gut kennen.

Die Speicherklasse static weist den Compiler an, dass der Speicherbereich der Variable während der gesamten Laufzeit des Programms zur Verfügung stehen soll und nicht von einem Gültigkeitsbereich bzw. dessen Verlassen abhängt. Dies hat verschiedene Auswirkungen:

  • Eine static-Variable innerhalb einer Funktion bleibt auch nach Ausführung der Funktion bestehen. Der Wert und die Adresse bleiben zwischen zwei Funktionsaufrufen erhalten.
  • Die Deklaration ist in der gesamten Übersetzungseinheit gültig/zugreifbar. Dadurch wird ein Binärobjekt innerhalb eines Programmmoduls zur Speicherbereichsgrenze im gesamten Programm da der C++-Standard verlangt, dass static-Deklarationen bei der Verlinkung nur im aktuellen Modul sichtbar sind.
  • Bei Multithreading muss eine Zugriffssynchronisierung erfolgen.
  • Mitgliedsattribute von Klassen, die mit der Speicherklasse static deklariert wurden, sollten eine zusätzliche Deklaration und Definition erhalten, die vom Compiler nur einmal angewendet wird.

Variablen mit der Speicherklasse extern beschreiben Speicher, der in anderen Übersetzungseinheiten zugewiesen wird und in der aktuellen Übersetzungseinheit importiert wird. Es muss ein direkter Zugriff auf den Speicher im Quellbereich vorhanden sein, in der Regel dadurch, dass die Variable dort global oder als öffentliches, statisches Klassenattribut deklariert wurde.

Diese Speicherklasse ist nur sinnvoll, wenn Sie sich absolut sicher sind, dass ein Attribut einer Klasse keine Auswirkung auf deren Zustand bzw. Ihre Programmierlogik hat, oder wenn Sie erlauben wollen, dass konstante Zugriffsmethoden Objekte vor Verwendung initialisieren. Sie wird dafür verwendet, um aus konstant deklarierten Methoden auf ein nichtkonstantes Attribut zuzugreifen und dieses zu ändern. mutable kann beispielsweise einem konstant deklarierten Zugriffsoperator ermöglichen, ein Objekt einmalig auf dem Heap anzulegen, für das im privaten Bereich einer Klasse eine auf null gesetzte Zeigervariable liegt:

#include <string>

struct K : std::string {
	std::string::size_type const len() const {return this->size();}
};

class U {
	mutable K * m_pK;
public:
	U() : m_pK(0) {};
	K const * getK() const {return m_pK==0 ? m_pK=new K() : m_pK;}
};

int main(int argc, char* argv[]) {
	U U1;
	U1.getK()->len();
	return 0;
}

Das Beispiel zeigt, wie hilfs mutable die konstante Methode K const * getK() const die unter m_pK gespeicherte Adresse ändern konnte.

Qualifizierer

Bearbeiten

Variablen mit diesem Qualifizierer sind nach der Definition unveränderbare Konstanten, die - anders als beispielsweise in C - kompilierzeitkonstant sind. Das bedeutet, dass bei der Deklaration einer Konstanten ihr auch ein Wert zugewiesen werden muss. Die Gründe für diese Entscheidung sind klar: Wie könnte eine const-Variable eine Konstante sein, wenn ihr kein Wert zugewiesen wurde? Generell ist es eine gute Idee, auch "normale" Variablen bei ihrer Deklaration zu initialisieren, falls es einen sinnvollen Anfangswert gibt.

const int x = 7;    // Initialisierer - verwendet die =-Syntax
const int x2(9);    // Initialisierer - verwendet die ()-Syntax
const int y;        // Fehler: kein Initialisierer

Das Schlüsselwort const kann aber auch in Verbindung mit (Member-)Funktionen genutzt werden. So wird in dieser Deklaration festgelegt, dass die Methode das Argument nicht verändern darf und kann:

#include <vector>

void print(const std::vector<int>& v); // pass-by-const-reference

Im Zusammenspiel mit Memberfunktionen macht const deutlich, dass die Methode das Objekt, für das es aufgerufen wurde, nicht verändert. Die Funktion kann also auch für const-Objekte aufgerufen werden.

class Car {
public:
    explicit Car() : _km(0) {}
    int getKm() const { return _km; }
private:
    int _km;
};

volatile

Bearbeiten

Mit dem Schlüsselwort volatile vor einem Variablentyp (bei der Deklaration einer Variablen) wird der Compiler angewiesen, die Variable bei jedem Zugriff erneut aus dem Speicher zu laden bzw. bei schreibendem Zugriff die Variable sofort in den Speicher zu schreiben. Ohne volatile würde die Optimierung des Compilers möglicherweise dazu führen, dass die Variable in einem Prozessorregister zwischengespeichert wird. volatile ist somit gewissermaßen das Gegenteil zu register.

volatile wird dann verwendet, wenn zu erwarten ist, dass auf den Wert der Variablen von außerhalb des Programms zugegriffen wird. Solch ein Zugriff könnte beispielsweise durch einen anderen Prozess/ Thread, durch das Betriebssystem oder durch die Hardware stattfinden. Ein typischer Anwendungsfall ist ein Interrupt (also eine Unterbrechung des aktuellen Programms zur Behandlung auftretender Ereignisse). Eine Zwischenspeicherung der Variablen würde dazu führen, dass das Programm nicht mit dem geänderten Wert arbeitet und diesen möglicherweise sogar überschreibt.

Referenzen

Bearbeiten
  1. http://gcc.gnu.org/projects/cxx0x.html

Heapspeicher

Bearbeiten

Verwendung

Bearbeiten

Beispiele

Bearbeiten

Zusammenfassung [Bearbeiten]

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

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.

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

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.