Einleitung
auto Speicherklasse
register Speicherklasse
extern Speicherklasse
static Speicherklasse
mutable Speicherklasse
const Qualifizierer
volatile Qualifizierer
Beispiel und Übung
Wo im Speicher eine Variable abgelegt wird und/oder wie lange sie erhalten bleibt, wird durch die Speicherklasse der Variable festgelegt. C++ kennt folgende Speicherklassen:
auto, register, extern, static, mutable
Um eine Variable einer bestimmten Speicherklasse zuzuordnen, wird vor dem Datentyp der Variablen die entsprechende Speicherklasse angegeben. Zudem können Variablen, bedingt durch die Stellung ihrer Definition im Programm, implizit einer bestimmten Speicherklasse angehören.
|
auto short var; register int count; extern char *pText; static bool first; |
Außer Speicherklassen können Variablen zusätzlich noch einen so genannten Qualifizierer (Qualifier) besitzen. Folgende Qualifizierer stehen zur Verfügung:
const, volatile
Die Angabe des zusätzlichen Qualifizierers erfolgt vor dem Datentyp der Variablen, aber nach einer eventuell vorhandenen Speicherklasse.
|
const int NUMBERELEMENTS; volatile unsigned long ticks; extern volatile bool portReady; |
Variablen der Speicherklasse auto sind immer lokale Variablen. Ihr Gültigkeitsbereich wird durch den sie umschließenden Block {...} begrenzt. Diese Speicherklasse wird jedoch so gut wie nie angegeben, da lokale Variablen standardmäßig dieser Speicherklasse angehören. auto Variablen werden nicht automatisch initialisiert, d.h. sie haben nach ihrer Definition einen zufälligen Wert.
|
|
Variablen der Speicherklasse register können ebenfalls nur lokale Variablen sein oder aber Funktionsparameter. Der Compiler versucht Variablen dieser Speicherklasse, innerhalb eines Prozessor-Registers abzulegen anstatt im Speicher. Aber wie gesagt, der Compiler versucht nur die Variable in einem Prozessor-Register unterzubringen. Sie können durchaus mehrere Dutzend Variablen dieses Typs definieren ohne einen Übersetzungsfehler zu erhalten, obwohl der Prozessor selbst z.B. nur 8 Register besitzt.
|
// Funktion mit register Parameter void PrintIt(register char *pText); // Nicht erlaubt, da register nur auf lokale Variablen register short count; int main() { register int loop; ... } void PrintIt(register char *pText) { ... } |
Wenn Sie versuchen, von einer register Variable die Adresse zu bilden, legt der Compiler die Variable allerdings automatisch im Speicher ab, da C++ davon ausgeht, dass von Registern keine Adresse gebildet werden kann.
|
|
Variablen oder Funktionen der Speicherklasse extern teilen dem Compiler mit, dass ihre Definition in einem anderen Modul (Quellcode-Datei) erfolgt als im aktuellen. D.h. als externe deklarierte Daten belegen zunächst einmal keinen Speicherplatz, sondern erst bei ihrer Definition in einem anderen Modul.
|
// Datei FILE1.CPP // Funktionsdeklaration void PrintIt (const char* const pText); // Definition einer globalen Variable short counter; ... // Definition der Funktion void PrintIt (const char* const pText) { ... } |
|
// Datei FILE2.CPP // Verweis auf Fkt. im Modul FILE1.CPP extern void PrintIt (const char* const); // Verweis auf Variable im Modul FILE1.CPP extern short counter; ... |
|
|
Eine etwas andere Wirkung besitzt die Speicherklasse extern bei benannten Konstanten. Wie schon in der Lektion über Konstanten erwähnt, sind benannten Konstanten standardmäßig modulglobal, d.h. nur in der Quellcode-Datei gültig, in der sie definiert sind. Soll eine benannte Konstante so definiert werden, dass sie in verschiedenen Modulen gültig ist, so muss sie sowohl bei ihrer Definition als auch bei den Verweisen darauf als externe Konstante definiert werden. Beachten Sie dabei aber bitte, dass die Konstante nur einmal initialisiert werden darf; ansonsten erhalten Sie beim Linken des Programms einen Fehler in der Form, dass die Konstante mehrfach definiert ist.
|
// Datei FILE2.CPP // Definition der Konstanten extern const int MAX = 5; ... |
|
// Datei FILE2.CPP // Verweis auf benannte Konstante extern const int MAX; ... |
|
|
Kommen wir jetzt zur Speicherklasse static. Globale Variablen und Funktionen dieser Speicherklasse sind nur in dem Modul sichtbar (und damit gültig), in dem Sie definiert sind. Eine extern-Referenz auf eine Variable oder Funktion dieses Typs führt zu einer Fehlermeldung beim Linken des Programms.
Lokale Variablen der Speicherklasse static behalten ihren letzten Wert auch dann noch bei, wenn ihr Gültigkeitsbereich verlassen wird, d.h. sie werden beim Verlassen ihres Gültigkeitsbereichs nicht gelöscht. Beim nächsten Eintritt in den Gültigkeitsbereich kann dann mit dem zuletzt abgespeicherten Wert weiter gearbeitet werden. Wohl gemerkt, dies betrifft nur die Erhaltung des Wertes, der Gültigkeitsbereich der Variable bleibt weiterhin auf den Block begrenzt, in dem sie definiert ist. Im Beispiel unten wird die Variable count dazu verwendet, die Anzahl der Funktionsaufrufe zu zählen. Da lokale Variablen aber nicht automatisch initialisiert werden, sollten sie (wie im Beispiel) bei ihrer Definition mit einem Startwert versehen werden. Diese Initialisierung wird aber nur ein einziges Mal ausgeführt, nämlich beim Reservieren des Speicherplatzes für die static-Variable.
|
// Funktionsdeklaration static void PrintIt (const char *pText); ... // Funktionsdefinition void PrintIt(const char *pText) { static short count = 0; ... count++; } |
Die mutable Speicherklasse spielt nur im Zusammenhange mit Klassen eine Rolle und ist nur der Vollständigkeit halber hier erwähnt. Mehr dazu später bei der Einführung von Klassen.
Den const Qualifizierer im Zusammenhang mit Variablen (einfachen Variablen, Zeiger und Felder) haben Sie im Verlaufe des Kurses schon mehrfach kennen gelernt. Später im Kurs werden wir uns diesen Qualifizierer nochmals ansehen, und zwar ebenfalls im Zusammenhang mit Klassen und den dazugehörigen Konstruktoren.
Kommen wir jetzt zum Qualifizierer volatile. volatile wird in der Regel nur für Variablen verwendet. Eine als volatile definierte Variable kann auch außerhalb des normalen Programmablaufs, und damit auf eine nicht vom Compiler feststellbare Art und Weise, ihren Zustand (Wert) verändern. Ursachen für solche asynchronen Zustandsänderungen von Variablen sind z.B. das Betriebssystem, die Hardware (Interrupts) oder ein quasi parallel laufender Thread (vereinfacht: ein Programm das parallel zu einem anderen läuft und mit diesem Daten austauschen kann). Eine typische volatile Variable ist z.B. die Systemzeit. Die Systemzeit wird in der Regel nicht durch die Anwendung verändert, sondern durch das Betriebssystem.
|
// Variable welche die Systemzeit enthält // Wird vom Betriebssystem aktualisiert volatile unsigned long ticks; ... // Beliebige Funktion void DoAnything(...) { // Lokale Variable zum Zwischenspeichern der Systemzeit unsigned long var; ... var = ticks; ... var = ticks; } |
Da die heutigen Compiler in der Regel sehr gut optimieren, könnte ohne volatile im Beispiel oben die zweite Zuweisung unter gewissen Bedingungen durch den Compiler entfernt werden, da sowohl var wie auch ticks zwischen diesen beiden Anweisung augenscheinlich nicht verändert wird. Durch die Definition von ticks als volatile wird dem Compiler mitgeteilt, dass diese Variable auch außerhalb des normalen Programmablaufs (z.B. in einer Interrupt-Routine) verändert werden kann und damit keinerlei Optimierung bezüglich ticks vorgenommen werden dürfen. Bei jedem Lesezugriff auf ticks wird immer der aktuelle Wert aus dem Speicher ausgelesen und jeder Schreibzugriff führt zur sofortigen Ablage des neuen Werts im Speicher.
Zum Schluss noch der Hinweis, dass es auch möglich ist, Memberfunktion als volatile zu kennzeichnen.
|
|
Neuer Wert: 1 Neuer Mittelwert: 1 |
|
// Beispiel zu Speicherklassen // Dateien einbinden #include <iostream> #include <iomanip> #include <cstdlib> using std::cout; using std::endl; // Funktionsdeklarationen double Mittelwert (const double wert); // main() Funktion int main () { // Schleifenindex im Register ablegen register short index; // 10 Zufallszahlen erzeugen for (index=0; index<10; index++) { // Zufallszahl im Bereich 0..9 erzeugen double zahl = rand() % 10; // Zufallszahl ausgeben cout << "Neuer Wert: " << zahl; // Mittelwert berechnen und ausgeben cout << " Neuer Mittelwert: " << Mittelwert(zahl) << endl; } } // Funktion zur Berechnung des Mittelwerts // Eingabe : Neuer hinzuaddierender Wert // Ausgabe : Neuer Mittelwert double Mittelwert(const double wert) { // Summe über alle bisherigen Werte static double summe = 0.0; // Zähler für Anzahl der Werte static double anzahl = 0.0; // Summe und Anzahl der Werte erhöhen summe += wert; anzahl++; // Mittelwert zurückgeben return summe/anzahl; } |
Schreiben Sie eine Funktion, die fortlaufende Seriennummern für 3 verschiedene 'Gerätetypen' erzeugen kann. Der Einfachheit halber werden die Gerätetypen mit den Nummern 0, 1 und 2 gekennzeichnet.
Die Seriennummern der Geräte des Typs '0' sollen ab 0 beginnen, die des Gerätetyps '1' ab 100 und die des Gerätetyps '2' ab 1000.
Erzeugen Sie in main() 15 Geräte, die zufällig auf die Gerätetypen 0, 1 oder 2 verteilt werden. Rufen Sie dann die Funktion zur Erzeugung der jeweiligen Seriennummer auf und geben Sie diese einschließlich des Gerätetyps aus.
Wenn Sie die Aufgabe richtig gelöst haben, sollten Sie eine Ausgabe in etwa der nachfolgenden Form erhalten.
|
Erzeuge nun Seriennummern: Gerätetyp: 2 erhält Seriennummer 1001 |