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 (Ausnahmen siehe nachfolgenden Abschnitt Speicherklassen und Qualifizierer).
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 mit 0 initialisiert.
#include <print>
void Function(); // Fkt-Deklaration
short global1; // global1 ab hier gültig
int main ()
{
global1 = 10;
std::println("global1: {}",global1);
Function();
}
short global2; // global1 und global2 ab hier gültig
void Function ()
{
global2 = global1;
std::println("global1: {}, global2: {}",global1,global2);
}
global1: 10
global1: 10, global2: 10
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 global2 in Zeile 14 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).
#include <print>
short var; // Globales Datum
void Func1 ()
{
short var; // verdeckt globales var
var = 10; // lokales var setzen
std::println("Func1::var {}",var);
}
void Func2 ()
{
short lVar = 3.14; // lokale Variable
// Ausgabe lokales lVar und globales var
std::println("Func2::var {}, Func2::lVar {}",var, lVar);
}
int main ()
{
var = 1; // globales var
std::println("main::var {}",var);
Func1();
Func2();
}
main::var 1
Func1::var 10
Func2::var 1, Func2::lVar 3
Es ist keine gute Idee, mehrere Daten mit gleichem Namen in überlappenden Gültigkeitsbereichen zu definieren. Das Beispiel dient einzig und allein zur Demonstration.
Definieren Sie Daten im kleinstmöglichen Gültigkeitsbereich. Dies fördert zum einen die Übersichtlichkeit und zum anderen die Wartbarkeit.
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.
#include <print>
short var1 = 1; // globales var1
int main ()
{
short var2 = 2; // lokales var2
// Eingeschobener Block
{
short var1 = 11; // verdeckt globales var1
short var2 = 22; // verdeckt main() lokales var2
// globales und lokales var1 ausgeben
std::println("::var1 {}, var1 {}",::var1,var1);
// Aber das geht nicht!
// Kein Zugriff auf main() lokales var2
//::var2 = var2;
}
}
::var1 1, var1 11
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.
extern char *pText;
static bool first;
int static second;
Außer Spezifizierer können Daten zusätzlich einen sogenannten Qualifizierer 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.
// Datei file1.cpp
#include <print>
// Funktionsdeklaration
void PrintIt (const char* const pText);
// Definition einer globalen Variable
short counter;
// Definition der Funktion
void PrintIt (const char* const pText)
{
std::println("Text: {}",pText);
std::println("counter: {}",counter);
}
// Datei main.cpp
#include <print>
// Verweis auf Fkt. in Datei file1.cpp
extern void PrintIt (const char* const);
// Verweis auf Variable in Datei file1.cpp
extern short counter;
int main()
{
counter = 11;
PrintIt("Text von main()");
}
Text: Text von main()
counter: 11
Besteht eine Anwendung aus mehreren Quelldateien, kann es bei der Initialisierung von Daten mit statischer Lebensdauer (i.d.R. sind dies die globalen Daten) zum sogenannten 'Static Initialization Order Fiasco' kommen. Der Grund 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 erfolgen kann.
Im nachfolgenden Beispiel wird in main.cpp die Variable var mit dem Inhalt der Variable extVar aus 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.
// Datei main.cpp
#include <print>
extern int extVar;
// Hier wird var mit der externen
// Variable extVar initialisiert
int var = extVar;
int main()
{
std::println("{}",var);
var++;
std::println("{}",var);
}
// Datei data.cpp
// Funktion wird zur Initialisierung des
// globalen Datums extVar aufgerufen
int GetVal()
{
return 10;
}
// Globales Datum extVar initialisieren
int extVar = GetVal();
0
1
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.
// Datei data.cpp
// Funktion wird zur Initialisierung des
// globalen Datums extVar aufgerufen
// Fkt. muss nun constexpr sein da sie durch
// eine constinit Definition aufgerufen wird
constexpr int GetVal()
{
return 10;
}
// Globales Datum extVar initialisieren
constinit int extVar = GetVal();
10
11
Werden globale Variablen mit Literalen und nicht über den Aufruf einer Funktion initialisiert, ist immer sichergestellt, dass diese zur Compilezeit initialisiert werden.
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.
// Datei main.cpp
#include <print>
// Verweis auf externe Konstante
extern const int MAX;
int main()
{
std::println("{}",MAX);
}
// Datei data.cpp
// Definition der Konstante
extern const int MAX=10;
Die Speicherklasse extern wird ebenfalls verwendet, um C Funktionen in ein C++ Programm einzubinden. Wie dies geht, erfahren Sie im Anhang J: extern "C" Speicherklasse.
static Speicherklasse
Globale Daten und Funktionen der Speicherklasse static sind nur in der Quellcode-Datei sichtbar 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.
#include <print>
bool DoAnything()
{
// Statisches Datum zum Zaehlen
// der Aufruf der Funktion
static unsigned int count = 0;
// Anzahl Aufrufe inkr.
count++;
std::println("Dies ist der {}. Aufruf",count);
// Weniger als 4-mal aufgerufen?
return count<4;
}
int main()
{
// Solange die Funktion true zurueckgibt
while (DoAnything())
std::print("Nochmal? ");
// Fertig
std::println("Fertig");
}
Dies ist der 1. Aufruf
Nochmal? Dies ist der 2. Aufruf
Nochmal? Dies ist der 3. Aufruf
Nochmal? Dies ist der 4. Aufruf
Fertig
mutable Speicherklasse
Die mutable Speicherklasse spielt nur im Zusammenhang mit const-Memberfunktionen einer Klasse 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 (Klassen und Objekte).
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.
// Datum welches die Systemzeit enthaelt
// Wird i.d.R. vom Betriebssystem aktualisiert
volatile extern unsigned long SysTicks;
int main()
{
// Lokale Variable zum Zwischenspeichern der Systemzeit
unsigned long var;
var = SysTicks; // Systemzeit auslesen
// ... weitere Anweisung
var = SysTicks; // Systemzeit erneut auslesen
}
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.
Die Definition eines Datums mittels auto entfernt die Qualifizierer const und volatile.
// volatile Variable definieren
volatile unsigned long ticks;
...
// Datentyp von actTicks ist unsigned long
// (ohne volatile)
auto actTicks = ticks;
Übungen
skla_01:
Es ist ein Programm zu erstellen, das aus 10 Zufallszahlen im Bereich von 0 bis 9 den gleitenden Mittelwert berechnet. Die Berechnung des Mittelwerts soll innerhalb einer Funktion erfolgen, die die erzeugte Zufallszahl als Parameter erhält und den jeweils aktuellen Mittelwert berechnet und zurückliefert.
Die Zufallszahl sowie der aktuelle Mittelwert (mit 2 Nachkommastellen) sind auszugeben.
Neuer Wert: 1 Neuer Mittelwert: 1.00
Neuer Wert: 7 Neuer Mittelwert: 4.00
Neuer Wert: 4 Neuer Mittelwert: 4.00
Neuer Wert: 0 Neuer Mittelwert: 3.00
Neuer Wert: 9 Neuer Mittelwert: 4.20
Neuer Wert: 4 Neuer Mittelwert: 4.17
Neuer Wert: 8 Neuer Mittelwert: 4.71
Neuer Wert: 8 Neuer Mittelwert: 5.12
Neuer Wert: 2 Neuer Mittelwert: 4.78
Neuer Wert: 4 Neuer Mittelwert: 4.70
skla_02:
In einer Produktion für die Produkte TV, Smartphone und Computer sind für den jeweiligen Produkttyp fortlaufende Seriennummern zu vergeben.
Die Seriennummern für TVs beginnen bei 1, für Smartphone bei 10001 und für Computer bei 20001.
Erstellen Sie eine Funktion für die Vergabe der fortlaufenden Seriennummern aller Produkte.
In main() sind dann 15 Produkte zu 'produzieren' die zufällig auf die Produkttypen verteilt sind. Geben Sie das produzierte Produkt und seine Seriennummer aus.
Erzeuge nun Seriennummern:
Produkt Computer , Seriennummer: 20001
Produkt Computer , Seriennummer: 20002
Produkt Smartphone, Seriennummer: 10001
Produkt Smartphone, Seriennummer: 10002
Produkt Computer , Seriennummer: 20003
Produkt Smartphone, Seriennummer: 10003
Produkt TV , Seriennummer: 00001
Produkt TV , Seriennummer: 00002
Produkt Smartphone, Seriennummer: 10004
Produkt Computer , Seriennummer: 20004
Produkt Computer , Seriennummer: 20005
Produkt Computer , Seriennummer: 20006
Produkt Smartphone, Seriennummer: 10005
Produkt TV , Seriennummer: 00003
Produkt Smartphone, Seriennummer: 10006