C++-Programmierung/ 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
- Programmspeicher,
- globale Speicher für globale Variable,
- Haldenspeicher für die dynamische Speicherverwaltung, und
- 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]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.
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.
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:
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
- eine automatische Speicherbereinigung eingesetzt werden soll;
- ein Speicherpool verwendet werden soll;
- es kann die Leistung des Programms erhöhen;
- die Sicherheit vor Ausnahmen kann erhöht werden.
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
die kürzere Form
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:
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
:
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]auto
[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.
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:
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.
static
[Bearbeiten]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.
extern
[Bearbeiten]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.
mutable
[Bearbeiten]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]const
[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:
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]Heapspeicher
[Bearbeiten]Verwendung
[Bearbeiten]Beispiele
[Bearbeiten]Zusammenfassung [Bearbeiten]
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.