Zum Inhalt springen

C++-Programmierung/ Nützliches/ Lebensdauer und Sichtbarkeit von Objekten

Aus Wikibooks


Die Lebensdauer einer Variable beschreibt die Zeit, in der die Variable im Speicher existiert. Die Sichtbarkeit einer Variable ist von ihrer Lebensdauer zu unterscheiden. Sie beschreibt, wie der Name schon vermuten lässt, wann man auf eine Variable über ihren Namen zugreifen kann, beziehungsweise wann dies nicht möglich ist. Wir werden in diesem Kapitel Variablen nach ihrer Lebensdauer sortiert betrachten und dabei auch immer auf ihren Sichtbarkeitsbereich eingehen.

Statische Variablen

[Bearbeiten]

Als statische Variablen werden alle Variablen bezeichnet, deren Lebensdauer mit der Laufzeit des Programms übereinstimmt. Sie belegen somit während der gesamten Ausführungszeit Speicherplatz, können andererseits aber auch während der gesamten Laufzeit zugegriffen werden.

Lokale statische Variablen

[Bearbeiten]

Variablen, die innerhalb einer Funktion als static deklariert wurden, werden als lokale statische Variablen bezeichnet, da sie bezüglich der Funktion lokal sind. Solche Variablen sind nur innerhalb der jeweiligen Funktion sichtbar. Da sie jedoch permanent existieren, können Sie über eine entsprechende Referenz oder einen Zeiger auch außerhalb der Funktion auf sie zugreifen. Die Initialisierung solcher Variablen erfolgt beim ersten Aufruf der Funktion.

Nicht-lokale statische Variablen

[Bearbeiten]

Alle statischen Variablen, die nicht in die Kategorie „lokal“ fallen, werden entsprechend als „nicht-lokal“ bezeichnet. Alle Variablen dieser Art sind überall sichtbar, mit Ausnahme von Variablen, die in anonymen Namensräumen deklariert wurden. Diese sind nur innerhalb der aktuellen Übersetzungseinheit sichtbar. Auch diese können natürlich dennoch mittels einer entsprechenden Referenz oder eines Zeigers auch aus anderen Übersetzungseinheiten zugegriffen werden. Als Übersetzungseinheit bezeichnet man das Kompilat einer cpp-Datei, welches der Compiler erstellt, bevor es vom Linker zum endgültigen Programm gebunden wird. Aus Quelltextsicht entspricht sie somit einer cpp-Datei inklusive aller mit #include eingebundenen Header. Um eine Variable in einer anderen Übersetzungseinheit sichtbar zu machen, muss sie dort deklariert werden. Beachten Sie, dass die Variable dort nicht ein zweites Mal definiert werden darf.

Globale Variablen

[Bearbeiten]

Variablen, die direkt im globalen Namensraum deklariert wurden, heißen globale Variablen. Im globalen Namensraum heißt, außerhalb von jeder Funktion, Klasse und jedem Namensraum. Ein geeignetes Indiz zum Überprüfen, ob eine Variable global deklariert ist, ist den Zugriffsoperator (::) ohne Namensraum zu benutzen. Folgendes Beispiel zeigt diesen einfachen Test.

extern int globale; // explizite Deklaration
int global;         // Definition

int main(){
    int lokal;

    ::global; // geht -> globale Variable
    ::lokal;  // Fehler -> nicht globale Variable
}

Zum erfolgreichen Kompilieren muss die Fehlerzeile auskommentiert werden

Auch hierbei gibt es natürlich wieder eine Ausnahme, denn auch Variablen aus einem anonymen Namensraum lassen sich auf diese Weise zugreifen. Da sich die Nicht-lokalen, statischen Variablen aus interner Sicht jedoch ohnehin alle sehr ähnlich verhalten, spielt ihre Unterscheidung in der Praxis ohnehin eine untergeordnete Rolle. Allgemein sollten Sie globale Variablen komplett vermeiden, da sie in größeren Projekten mit mehreren Programmierern sehr schnell zu Namenskonflikten führen.

Während sich eine globale Variable jedoch über eine explizite Deklaration aus einer anderen Übersetzungseinheit heraus sichtbar machen lässt, ist dies für eine Variable in einem anonymen Namensraum nicht möglich.

Globaler Namensraum

[Bearbeiten]

Variablen, die innerhalb eines Namensraumes deklariert werden, vermeiden Namenskonflikte und die Zuordnung zu einzelnen Programmteilen ist schneller ersichtlich. Wenn Sie eine globale Variable benötigen, dann verpacken Sie diese in einen geeigneten Namensraum. Auch hier ist natürlich eine explizite Deklaration in einer anderen Übersetzungseinheit möglich, wobei die Deklaration natürlich auch dort im selben Namensraum erfolgen muss.

Anonymer Namensraum

[Bearbeiten]

Der anonyme Namensraum wurde nun schon einige Male angesprochen. Variablen, die in diesem Namensraum definiert wurden, können in der aktuellen Übersetzungseinheit wie globale Variablen behandelt werden. Da ein anonymer Namensraum nur in der aktuellen Übersetzungseinheit sichtbar ist, gibt es keine Möglichkeit, seinen Inhalt aus einer anderen Übersetzungseinheit aus sichtbar zu machen. Der anonyme Namensraum einer anderen Übersetzungseinheit ist entsprechend wieder ein anderer als der in der aktuellen Datei.

Statische Klassenvariablen

[Bearbeiten]

Statische Klassenvariablen verhalten sich im wesentlichen wie Variablen innerhalb eines benannten Namensraumes. Um sie in einer anderen Übersetzungseinheit zu sehen, ist eine Definition der Klasse nötig, in der sie deklariert ist. Dies erfolgt üblicherweise durch Einbinden der entsprechenden Headerdatei der Klasse.

Probleme mit der Initialisierung

[Bearbeiten]

Das große Problem, das sich in Zusammenhang mit statischen, nicht-lokalen Variablen ergibt, ist ihre Initialisierungsreihenfolge bezüglich unterschiedlicher Übersetzungseinheiten. Diese ist nicht definiert, da es für den Compiler beziehungsweise Linker im allgemeinen unmöglich ist, herauszufinden, welche Reihenfolge die richtige ist. Betrachten Sie zur Veranschaulichung das folgende Beispiel mit zwei Übersetzungseinheiten.

//*******************************************************//
//******************** Datei "B.hpp" ********************//
// Definition von B
struct B{
    B(int value):value_(value){}
    int value_;
};

//*******************************************************//
//****************** Datei "file1.cpp" ******************//
#include "B.hpp"

B b(5); // Definition und Initialisierung der globalen Variable b

//*******************************************************//
//****************** Datei "file2.cpp" ******************//
#include <iostream>
#include "B.hpp"

extern B b; // Deklaration der Variable b

struct A{ // Definition von A
    A(B const& value):b_(value){}
    B b_;
};

A a(b); // Globale Variable a, die mit b initialisiert wird

int main(){
    std::cout << a.b_.value_ << std::endl;
}

Ausgabe ist vom Compiler abhängig

Falls die Variable B zuerst initialisiert wird, liefert das Programm wie gewünscht die Ausgabe 5. Sie können ja mal versuchen Ihrem Compiler die beiden cpp-Dateien auf der Kommandozeile in unterschiedlicher Reihenfolge zu übergeben. Wahrscheinlich wird das Programm je nachdem welche Datei zuerst gebunden wird, ein anderes Ergebnis liefern. Sie können sich jedoch keinesfalls darauf verlassen, dass Sie Ihre Wunschinitialisierungsreihenfolge immer mit dieser Try-and-Error-Methode herstellen können. Es ist nicht immer möglich, weshalb es vom C++-Standard auch nicht definiert wurde.

Glücklicherweise gibt es einen einfachen Ausweg aus diesem Dilemma. Eine kleine Designänderung wird Ihr Problem sofort beheben. Wie Ihnen inzwischen bekannt ist, werden lokale, statische Variablen erst beim ersten Funktionsaufruf initialisiert. Wenn Sie also derartige Initialisierungsprobleme befürchten müssen, dann können Sie statt einer nicht-lokalen, statischen Variable eine Funktion verwenden, die eine Referenz auf eine lokale statische Variable zurückgibt. Die Initialisierungsreihenfolge ergibt sich dann automatisch und ohne Zutun von Compiler und Linker zur Laufzeit des Programms.

//*******************************************************//
//******************** Datei "B.hpp" ********************//
// Definition von B
struct B{
    B(int value):value_(value){}
    int value_;
};

//*******************************************************//
//****************** Datei "file1.cpp" ******************//
#include "B.hpp"

// Definition und Initialisierung einer statischen
// lokalen Variable die durch einen Funktionsaufruf
// zugegriffen werden kann
B& b(){
    static B _b(5); // Initialisierung beim ersten Aufruf
    return _b;
}

//*******************************************************//
//****************** Datei "file2.cpp" ******************//
#include <iostream>
#include "B.hpp"

B& b(); // Deklaration der Funktion b

struct A{ // Definition von A
    A(B const& value):b_(value){}
    B b_;
};

// Kapselungsfunktion
A& a(){
    static A _a(b()); // Initialisierung beim ersten Aufruf
    return _a;
}

int main(){
    std::cout << a().b_.value_ << std::endl;
}
Ausgabe:
5

Diese Variante erfordert zwar einen minimal höheren Schreibaufwand, im Gegenzug erhalten Sie aber ein klar definiertes und sinnvolles Verhalten und als kleinen Bonus bekommen Sie noch einen möglicherweise hilfreichen Effekt dazu. Da die Initialisierung der Variablen nun erst dann stattfindet, wenn Sie diese tatsächlich verwenden, startet ihr Programm etwas schneller und Variablen, die während der Laufzeit nicht genutzt werden, verschwenden keine Zeit mehr mit einer unnötigen Initialisierung. Natürlich belegen sie trotzdem immer noch Speicherplatz, also gehen Sie trotz der gewonnen Laufzeitvorteile mit bedacht vor, wenn Sie statische Variablen verwenden.

Alter Text

[Bearbeiten]

Der Rest des Kapitels muss noch einmal überarbeitet werden, die aktuelle Version bietet jedoch schon einen grundlegenden Überblick. Ich möchte die Überarbeitung selbst vornehmen, da ich eine etwas andere Struktur vorgesehen habe. Der aktuelle Text wird natürlich in meine Version mit einfließen. ;) --Prog 16:19, 6. Dez. 2010 (CET)

Lokale Variablen

[Bearbeiten]

Im Gegensatz zu globalen, werden lokale Variablen in einem bestimmten Anweisungsblock (z.B. Schleifen, if-Abfragen oder Funktionen) deklariert. Ihre Existenz endet, wenn dieser Block wieder verlassen wird.

void foo(int a) {
    int lok1 = 0; // lok1 ist eine Variable im Anweisungsblock von void foo(int a)

    if (a < 0) {
        int lok2 = 1; // lok2 ist eine Variable im if-Block
    } // hier wird lok2 aus dem Speicher gelöscht...
} // ...und hier lok1
Hinweis

Bei Schleifen ist zu beachten, dass Variablen, deren Deklaration im Schleifenrumpf steht, bei jedem Durchlauf neu initialisiert werden.

while (1) {
    int var = 0;
    std::cout << var << std::endl; // die Ausgabe ist hier immer 0
    ++var;
}

Statische Variablen

[Bearbeiten]

Statische Variablen (auch statische Klassenmember) werden wie globale zu Beginn des Programms im Speicher angelegt und bei seinem Ende wieder daraus entfernt. Der Unterschied zu einer globalen Variable wird weiter unten auf dieser Seite im Teil über Sichtbarkeit geklärt.

Dynamisch erzeugte Variablen

[Bearbeiten]

Eine Variable, die mittels dem new Operator angefordert wird, ist dynamisch. Sie existiert so lange, bis sie durch einen Aufruf von delete wieder gelöscht wird.

Hinweis

Der Aufruf von delete ist die einzige Möglichkeit, den von einer dynamisch erzeugten Variable belegten Speicher wieder frei zu geben. Geschieht dies nicht, so kann es leicht zu einem Speicherleck kommen.

Lesen Sie bitte auch das Kapitel über den new und delete um Informationen über deren Verwendung zu erhalten.

Objekte und Membervariablen

[Bearbeiten]

Objekte werden wie normale Variablen gehandhabt, d. h. sie können global, lokal, statisch oder dynamisch erzeugt sein. Ihre Member haben die gleiche Lebensdauer wie sie selbst. Eine Ausnahme bilden statische Klassenvariablen, die von Anfang bis Ende des Programmablaufes im Speicher vorhanden sind.

Sichtbarkeit

[Bearbeiten]

Allgemein

[Bearbeiten]

Um überhaupt die Chance zu haben mit einer Variablen zu arbeiten, muss diese im Quelltext bereits deklariert worden sein. Folgendes Codestück ist also falsch und führt zu einem Fehler.

// ...
var = 34; // Fehler z.B. "symbol var not found"
int var;
// ...
Thema wird später näher erläutert…

Das Prinzip der Datenkapselung in der objektorientierten Programmierung finden Sie im Abschnitt über Klassen.

Gültigkeitsbereiche und deren Schachtelung

[Bearbeiten]

Jede Variable gehört zu einem bestimmten Gültigkeitsbereich (engl. scope). Dieser legt fest, wann eine Variable von uns „gesehen“ und damit benutzt werden kann. Vereinfacht gesagt bildet jedes Paar aus geschweiften Klammern ({}) einen eigenen Definitionsbereich. Dazu gehören beispielsweise if, else, Schleifen und Funktionen. Diese unterschiedlichen Bereiche sind nun ineinander geschachtelt ähnlich wie die berühmten Matrjoschka-Puppen mit dem Unterschied, dass die Definitionsbereiche nicht „kleiner“ werden und dass es mehrere „nebeneinander“ geben kann.

// das hier gehört zum globalen Bereich

int func() { // hier beginnt der Bereich der Funktion func...
    return 0;
    // ... und ist hier auch schon wieder beendet
}

int bar(int val) {  // val gehört zum Definitionsbereich "bar"
    if (val == 7) { // diese if-Anweisung hat auch ihren eigenen Gültigkeitsbereich...
       int ich_gehoer_zum_if;
    } // ... der hier zu Ende ist
} // bar ende

Welche Variablen sind sichtbar?

[Bearbeiten]

Jetzt ist es leicht zu bestimmen mit welchen Variablen wir an einer bestimmten Stelle im Programmcode arbeiten können: Es sind diejenigen, die entweder dem derzeitigen oder einem Gültigkeitsbereich auf einer höheren Ebene angehören. Wenn wir uns erneut das Beispiel der Puppen vor Augen halten, so wird klar was hiermit gemeint ist.

Beispiel:

int out;
{
    int inner_1;
}

{   
    int inner_2;
    // an dieser Stelle könnten wir sowohl auf out als auch auf inner_2 zugreifen, nicht jedoch auf inner_1;
}

Zusätzlich gilt noch, dass von zwei Variablen gleichen Namens nur auf die weiter innen liegende zugegriffen werden kann. Außerdem wird die gleichnamige innere Variable im selben Speicherbereich agieren.

int var=9;

{   
    int var=3;
    std::cout << var << std::endl; // die Ausgabe ist 3
}