C++ Tutorial

Lebensdauer und Sichtbarkeit von Daten

Die Lebensdauer und Sichtbarkeit eines Datums, d.h. die Zeit, in der es Speicher belegt und in der darauf zugegriffen werden kann, wird im Prinzip durch die Position seiner Definition bestimmt.

Globale Daten

Daten welche nicht innerhalb eines Blocks {...} definiert sind, werden als globale Daten bezeichnet. Der Zugriff darauf ist standardmäßig nur in der Quellcode-Datei möglich, in der sie definiert sind. Sie sind ab der Stelle im Programm gültig, an der sie definiert sind und sie werden beim Programmstart mit 0 initialisiert.

1: short modul1; // modul1 ab hier gültig
2:
3: int main ()
4: {
5:    modul1 = 10;
6:    ...
7: }
8: short modul2; // modul1 und modul2 ab hier gültig
9: void Function ()
10: {
11:    modul2 = modul1;
12:    ...
13: }

Obwohl globale Daten an beliebiger Stelle definiert werden können, sollten sie der besseren Lesbarkeit und Wartbarkeit wegen am Dateianfang definiert werden. D.h., die Definition der Variable modul2 in Zeile 8 im obigen Beispiel fördert nicht die Wartbarkeit des Programms.

Lokale Daten

Daten die innerhalb eines Blocks {...}, z.B. in einer Funktion oder in einem if-Zweig, definiert werden, werden als lokale Daten bezeichnet. Sie sind ebenfalls ab der Stelle im Programm gültig, an der sie definiert sind und ihre Gültigkeit endet am Blockende. Lokale Daten werden nicht automatisch initialisiert und haben zu Beginn einen zufälligen Inhalt. Eine Ausnahme davon bilden statische Daten, die anschließend erklärt werden.

Besitzt ein lokales Datum den gleichen Namen wie ein globales Datum, verdeckt das lokale Datum das globale Datum (siehe nachfolgendes Beispiel).

1: short var;
2:
3: int main ()
4: {
5:    var = 1;   // globales var
6:    ...
7: }
8: void Func1 ()
9: {
10:    short var;   // verdeckt globales var
11:    var = 10;    // lokales var setzen
12:    ...
13: }
14: void Func2 ()
15: {
16:    short lVar = 3.14;   // lokale Variable
17:    // Ausgabe lokales lVar und globales var
18:    std::cout << std::format("{} {}\n",var, lVar);
19:    ...
20: }

Zugriff auf globale Daten (Gültigkeitsbereichsoperator)

Wird ein globales Datum durch ein lokales verdeckt, kann mithilfe des Gültigkeitsbereichsoperators :: (scope resolution operator, das sind zwei Doppelpunkte!) vor dem Variablennamen auf das globale Datum zugegriffen werden. Dieser Zugriff funktioniert nur auf globale Daten und nicht, wie im Beispiel anhand von var2 dargestellt, auf 'übergeordnete' lokale Daten.

1: short var1; // globales var1
2: int main ()
3: {
4:    short var2; // lokales var2
5:    ...
6:    {
7:        short var1;     // lokale Daten
8:        short var2;
9:        var1 = 10;      // lokale Daten setzen
10:       ::var1 = 0;     // globales Datum setzen
11:       ::var2 = 0;     // nicht erlaubt!
12:    }
13: }

Speicherklassen und Qualifizierer

Um eine vom Standard abweichende Lebensdauer und Sichtbarkeit von Daten (und Funktionen) zu definieren, stehen folgende Spezifizierer (Speicherklassen) zur Verfügung:

extern, static, mutable

Um ein Datum einer Speicherklasse zuzuordnen, wird vor dem Namen des Datums der Spezifizierer angegeben.

const int NUMBERELEMENTS = 10;
volatile unsigned long ticks;
extern volatile bool portReady

Außer Spezifizierer können Daten zusätzlich einen sogenannten Qualifizierer (Qualifier) besitzen:

const, volatile

Die Angabe des Qualifizierers erfolgt ebenfalls vor dem Namen des Datums.

const int NUMBERELEMENTS = 10;
volatile unsigned long ticks;
extern volatile bool portReady

extern Speicherklasse

Daten und Funktionen der Speicherklasse extern teilen dem Compiler mit, dass ihre Definition in einer anderen Quellcode-Datei erfolgt als in der aktuellen.

1: // Datei file1.cpp
2: // Funktionsdeklaration
3: void PrintIt (const char* const pText);
4: // Definition einer globalen Variable
5: short counter;
6: // Definition der Funktion
7: void PrintIt (const char* const pText)
8: {
9:    ...
10: }
1: // Datei file2.cpp
2: // Verweis auf Fkt. in Datei file1.cpp
3: extern void PrintIt (const char* const);
4: // Verweis auf Variable in Datei file1.cpp
5: extern short counter;6: ...

Besteht eine Anwendung aus mehreren Quelldateien, kann es bei der Initialisierung von statischen Daten zum sogenannten ' Static Initialization Order Fiasco' kommen. Die Ursache dafür ist, dass zum einen die Reihenfolge der Initialisierungen der Daten zwischen den Quelldateien nicht eindeutig definiert ist, und zum anderen die Initialisierung erst zur Laufzeit vorgenommen werden kann.

Im nachfolgenden Beispiel wird in main.cpp die Variable var mit dem Inhalt der Variable extVar in der Datei data.cpp initialisiert. Da die Reihenfolge der Initialisierungen in diesem Fall nicht definiert ist, besteht eine 50:50 Change, dass var den erwarteten Wert 10 enthält.

1: // Datei data.cpp
2: int GetVal()
3: {
4:    return 10;
5: }
6: ...
7: int extVar = GetVal();
1: // Datei main.cpp
2: #include <iostream>
3:
4: extern int extVar;
5: int var = extVar;
6:
7:  int main()
8:  {
9:     std::println("{}",var);
10: }

Um solche Fälle abzufangen, ist der Spezifizierer constinit zum Datentyp der zu initialisierenden Variable hinzuzufügen. Dadurch wird erzwungen, dass die Initialisierung zur Compilezeit durchgeführt wird. Kann die Initialisierung nicht zur Compilezeit durchgeführt werden, meldet der Compiler einen Fehler.

1: // Datei data.cpp
2: constexpr int GetVal()
3: {
4:    return 10;
5: }
6: constinit int extVar = GetVal();

Eine andere Wirkung besitzt die Speicherklasse extern bei benannten Konstanten. Wie im Kapitel über Konstanten erwähnt, sind benannte Konstanten standardmäßig modulglobal, d.h. nur in der Quellcode-Datei gültig, in der sie definiert sind. Soll eine benannte Konstante definiert werden die in verschiedenen Quellcode-Dateien verwendet wird, ist sie sowohl bei ihrer Definition wie auch bei den Verweisen darauf als externe Konstante zu definieren. Dabei ist zu beachten, dass die Konstante nur einmal initialisiert werden darf.

1: // Datei file1.cpp
2: // Definition der Konstanten
3: extern const int MAX = 5;
4: ...
1: // Datei file2.cpp
2: // Verweis auf benannte Konstante>
3: extern const int MAX;
4: ...

static Speicherklasse

Globale Daten und Funktionen der Speicherklasse static sind nur in der Quellcode-Datei sichtbar und damit gültig in der sie definiert sind. Eine extern Referenz auf ein Datum oder eine Funktion dieses Typs führt zu einer Fehlermeldung beim Linken des Programms.

Lokale Daten der Speicherklasse static behalten ihren letzten Wert auch dann bei, wenn ihr Gültigkeitsbereich verlassen wird, d.h., sie werden beim Verlassen ihres Gültigkeitsbereichs nicht gelöscht. Wohl gemerkt, dies betrifft nur die Erhaltung des Wertes. Der Gültigkeitsbereich der Daten bleibt weiterhin auf den Block begrenzt, in dem sie definiert sind.

Da lokale Daten nicht automatisch initialisiert werden, sollten static-Daten bei ihrer Definition mit einem Startwert versehen werden. Diese Initialisierung wird nur ein einziges Mal ausgeführt, beim Reservieren des Speicherplatzes für das Datum. Im Beispiel wird die Variable count dazu verwendet, die Anzahl der Funktionsaufrufe zu zählen.

1: // Funktionsdefinition
2: void PrintIt(const char *pText)
3: {
4:    static auto count = 0;
5:    ...
6:    count++;
7: }

mutable Speicherklasse

Die mutable Speicherklasse spielt nur im Zusammenhang mit const-Memberfunktionen einer Klassen eine Rolle und ist nur der Vollständigkeit halber hier erwähnt. Mehr später bei der Einführung von Klassen.

const Qualifizierer

Der const Qualifizierer im Zusammenhang mit Daten (einfachen Variablen, Zeiger und Felder) definiert deren Inhalt als unveränderlich. Daraus folgt, dass const-Daten bei ihrer Definition initialisiert werden müssen, da eine nachträgliche Änderung nicht mehr möglich ist (siehe dazu Kapitel Konstanten).

Später wird dieser Qualifizierer nochmals betrachtet, und zwar im Zusammenhang mit Klassen.

volatile Qualifizierer

Der volatile Qualifizierer wird der in der Regel nur für Daten verwendet. Ein als volatile definiertes Datum kann außerhalb des normalen Programmablaufs, und damit auf eine nicht vom Compiler feststellbare Art und Weise, seinen Wert ändern. Ursache für eine solche asynchrone Zustandsänderung eines Datums kann z.B. das Betriebssystem, die Hardware (Interrupts) oder eine parallel laufende Task sein. Ein typisches volatile-Datum ist z.B. die Systemzeit. Die Systemzeit wird im Allgemeinen nicht durch die Applikation gesetzt, sondern durch das Betriebssystem.

1: // Variable welche die Systemzeit enthält
2: // Wird vom Betriebssystem aktualisiert
3: volatile extern unsigned long SysTicks;
4: ...
5: // Beliebige Funktion
6: void DoAnything(...)
7: {
8:  // Lokale Variable zum Zwischenspeichern der Systemzeit
9:  unsigned long var;
10: var = SysTicks;     // Systemzeit auslesen
11: ...
12: var = SysTicks;     // Systemzeit erneut auslesen
13: }

Da Compiler sehr gut optimieren, könnte im Beispiel ohne volatile die zweite Zuweisung unter gewissen Bedingungen durch den Compiler entfernt werden, da sowohl var wie auch SysTicks zwischen diesen beiden Anweisung allem Anschein nach nicht verändert werden. Durch die Definition von SysTicks als volatile wird dem Compiler mitgeteilt, dass dieses Datum außerhalb des normalen Programmablaufs (z.B. in einer Interrupt-Routine) verändert werden könnte und damit keinerlei Optimierung bezüglich des zu erwartenden Inhalts von SysTicks vorgenommen werden darf. Bei jedem Lesezugriff auf SysTicks wird immer der aktuelle Wert aus dem Speicher ausgelesen und jeder Schreibzugriff führt zur sofortigen Ablage des neuen Werts im Speicher.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz