Lebenszeit und -ort eines Objekts
Content |
Objekte können global bzw. statisch, lokal auf dem Stack oder auf dem Heap leben. Ihre Lebenszeit ist entsprechend. Zusätzlich gibt es auch noch thread_local-Deklarationen, mit denen wir uns hier aber nicht näher beschäftigen.
Globale bzw. statische Variablen
Bei globalen bzw. statischen Variablen steht die Adresse beim Zusammenbau des Programms bereits fest (daher statisch im Gegensatz zu dynamisch, d.h. zur Laufzeit). Entsprechende Objekte werden vor dem Aufruf von main konstruiert und nach dem Ende von main bzw. dem Aufruf von exit abgebaut. Eine Ausnahme davon sind mit static deklarierte Objekte innerhalb einer Funktion oder Methode. Diese werden erst konstruiert, wenn die Ausführung die entsprechende Deklaration das erste Mal erreicht. Dies ist seit C++11 auch thread-safe.
Alle Deklarationen sind im folgenden Beispiel global bzw. statisch:
#include <iostream> class X { private: const char* name; public: X(const char* name) : name(name) { std::cout << "X \"" << name << "\" constructed" << std::endl; } ~X() { std::cout << "X \"" << name << "\" destructed" << std::endl; } }; class Y { static X x; // static member, defined below }; X Y::x = "static member of Y"; // definition of Y::x X x = "global variable"; int main() { std::cout << "main starts" << std::endl; static X x = "static variable within main"; std::cout << "main ends" << std::endl; }
theon$ g++ -Wall -o global global.cpp theon$ ./global X "static member of Y" constructed X "global variable" constructed main starts X "static variable within main" constructed main ends X "static variable within main" destructed X "global variable" destructed X "static member of Y" destructed theon$
Es ist hier wiederum zu beobachten, dass alle Objekte in umgekehrter Reihenfolge abgebaut werden. Das ist bei innerhalb einer Funktion oder Methode statisch deklarierten Variablen nicht trivial umzusetzen, da die Reihenfolge der Konstruktionen nicht vorhersehbar ist:
#include <iostream> class X { private: const char* name; public: X(const char* name) : name(name) { std::cout << "X \"" << name << "\" constructed" << std::endl; } ~X() { std::cout << "X \"" << name << "\" destructed" << std::endl; } }; void foo(int selector) { if (selector) { static X x1 = "x1 within foo"; } static X x2 = "x2 within foo"; } int main() { std::cout << "main starts" << std::endl; int selector; if (std::cin >> selector) { foo(selector); foo(1); } std::cout << "main ends" << std::endl; }
theon$ g++ -Wall -o static-can-be-dynamic static-can-be-dynamic.cpp theon$ echo 0 | ./static-can-be-dynamic main starts X "x2 within foo" constructed X "x1 within foo" constructed main ends X "x1 within foo" destructed X "x2 within foo" destructed theon$ echo 1 | ./static-can-be-dynamic main starts X "x1 within foo" constructed X "x2 within foo" constructed main ends X "x2 within foo" destructed X "x1 within foo" destructed theon$
Lokale Variablen
Wenn die Programmausführung eine lokale Variablendeklaration erreicht, wird diese konstruiert. Am Ende des umgebenden Blocks (das ist das umgebende compound statement) werden die lokalen Variablen in umgekehrter Reihenfolge wieder abgebaut. Alle lokalen Variablen leben in dem entsprechenden Stack-Segment, das dem Funktionsaufruf zugeordnet ist.
#include <iostream> class X { private: const char* name; public: X(const char* name) : name(name) { std::cout << "X \"" << name << "\" constructed" << std::endl; } ~X() { std::cout << "X \"" << name << "\" destructed" << std::endl; } }; int main() { for (int i = 0; i < 2; ++i) { X x1 = "x1 within for loop"; { X x2 = "x1 in the inner block"; X x3 = "x2 in the inner block"; } X x4 = "x4 at the end of the for loop"; } }
theon$ g++ -Wall -o local local.cpp theon$ ./local X "x1 within for loop" constructed X "x1 in the inner block" constructed X "x2 in the inner block" constructed X "x2 in the inner block" destructed X "x1 in the inner block" destructed X "x4 at the end of the for loop" constructed X "x4 at the end of the for loop" destructed X "x1 within for loop" destructed X "x1 within for loop" constructed X "x1 in the inner block" constructed X "x2 in the inner block" constructed X "x2 in the inner block" destructed X "x1 in the inner block" destructed X "x4 at the end of the for loop" constructed X "x4 at the end of the for loop" destructed X "x1 within for loop" destructed theon$
Variablen auf dem Heap
Daten können auf dem Heap mit dem new-Operator angelegt werden. Mit delete werden sie zunächst dekonstruiert und danach wird der belegte Speicher freigegeben. Bei einem Speicherleck, d.h. dem Fehlen von delete kommt es nicht zum Abbau der entsprechenden Objekte.
#include <iostream> class X { private: const char* name; public: X(const char* name) : name(name) { std::cout << "X \"" << name << "\" constructed" << std::endl; } ~X() { std::cout << "X \"" << name << "\" destructed" << std::endl; } }; int main() { X* x1p = new X{"*x1p"}; X* x2p = new X{"*x2p"}; delete x1p; }
theon$ g++ -Wall -Wno-unused-variable -o heap heap.cpp theon$ ./heap X "*x1p" constructed X "*x2p" constructed X "*x1p" destructed theon$
Aufgaben
-
Was gibt folgendes Programm aus? Ist die Reihenfolge der Abbauten genau umgekehrt zur Reihenfolge der Konstruktionen? Wenn nein: Woran liegt das?
#include <iostream> class X { private: const char* name; public: X(const char* name) : name(name) { std::cout << "X \"" << name << "\" constructed" << std::endl; } ~X() { std::cout << "X \"" << name << "\" destructed" << std::endl; } }; class Y { public: Y(const char* name) : x("within Y"), name(name) { std::cout << "Y \"" << name << "\" constructed" << std::endl; } ~Y() { std::cout << "Y \"" << name << "\" destructed" << std::endl; } private: X x; const char* name; }; X x = "global x"; int main() { X x1 = "local x1"; Y* yp = new Y{"*yp (on heap)"}; X x2 = "local x2"; delete yp; }
-
Ergänzen Sie die folgende Vorlage mit der Klasse Chain, deren Konstruktor einen Namen und einen Zeiger auf ein nachfolgendes Mitglied erhält. Beim Abbau sollte die Klasse zuerst das nachfolgende Mitglied freigeben und dann die entsprechende Meldung ausgeben. Der Nullzeiger wird in C++ mit nullptr bezeichnet und es ist kein Problem, wenn delete mit einem Nullzeiger aufgerufen wird.
#include <iostream> class Chain { /* FIXME */ }; int main() { Chain* c1p = new Chain("chain member #1", nullptr); Chain* c2p = new Chain("chain member #2", c1p); Chain* c3p = new Chain("chain member #3", c2p); delete c3p; }
Erwartete Ausgabe:
Chain "chain member #1" constructed Chain "chain member #2" constructed Chain "chain member #3" constructed Chain "chain member #1" destructed Chain "chain member #2" destructed Chain "chain member #3" destructed
Was wäre in der Klasse Chain zu ändern, damit die Abbau-Meldungen in der umgekehrten Reihenfolge erscheinen?