C++ Kurs

Variablen

Die Themen:

Variablendefinition

In diesem Kapitel werden wir uns ansehen, wie Daten innerhalb eines Programms abgelegt werden.

Zur Programmlaufzeit veränderbare Daten werden in sogenannten Variablen abgelegt. Einer Variable können beliebig oft Werte zuweisen werden (siehe dazu später Kapitel Zuweisungen), der bisherige Wert wird dann einfach überschrieben.

Bevor ein Wert in einer Variablen abgelegt werden kann, muss die Variable vorher definiert werden. C++ unterstützt keine automatische Variablendefinition wie z.B. BASIC. Eine Variablendefinition hat folgenden vereinfachten allgemeinen Aufbau:

DTYP NAME;

An erster Stelle steht der Datentyp der Variable und danach folgt der Name (Bezeichner) der Variable. Da eine Variablendefinition auch eine ausführbare Anweisung ist, muss sie mit einem Semikolon abgeschlossen werden.

Der Datentyp bestimmt den Wertebereich der Variable und damit auch indirekt den Speicherplatz (Anzahl Bytes) den eine Variable belegt. Je mehr Speicher die Variable belegt, umso größer kann der in ihr abzulegende Wert prinzipiell sein. Mehr zu den Wertebereichen gleich noch.

Nach dem Datentyp folgt der Name der Variable. Über diesen Namen können dann der Variable Werte zugewiesen oder deren aktueller Wert auslesen werden. Der Name muss eindeutig sein, d.h. es darf keine zweite Variable (oder Funktion und ähnliches) mit gleichem Namen im gleichen Gültigkeitsbereich geben. Was Gültigkeitsbereiche sind und welche es gibt, wird später noch ausführlich behandelt. Bis auf weiteres sollten keine Namen von Variablen mehrfach verwendet werden.

HinweisObwohl die Schreibweise von Variablennamen beliebig ist, sollte stets eine einheitliche Schreibweise verwendet werden. Im Kurs beginnen Variablennamen immer mit einem Kleinbuchstaben und bei Bedarf werden an den Wortgrenzen Großbuchstaben verwendet (z.B. maxTemp oder numberOfTiles).
AchtungC++ unterscheidet streng zwischen Groß- und Kleinschreibung. So ist die Variable max nicht identisch mit der Variable Max. Allerdings sollte vermieden werden, dass sich zwei unterschiedliche Variablen nur durch die Groß-/Kleinschreibung unterscheiden, alleine der guten Lesbarkeit wegen.
PgmHeader
// Variable ausserhalb von main() definieren
int numberOfRanges;

// main() Funktion
int main()
{
   // Variable innerhalb von main() definieren
   int sinusValue;
   ...
   // weitere Variable definieren
   double result;
   ...
}

Für den Augenblick spielt es noch keine Rolle, ob die Definition einer Variable vor main() oder aber innerhalb von main() erfolgt. Worin sich diese beiden Definitionen letztendlich unterscheiden, sehen wir uns später noch bei den erwähnten Gültigkeitsbereichen genauer an. Außerdem können Variablen an beliebiger Stelle im Programm definieren werden, was den Vorteil bietet, dass eine Variable genau an der Stelle definiert werden kann, an der sie auch benötigen wird.

Ganzzahl-Datentypen

C++ kennt folgende Ganzzahl-Datentypen, die auch als Integer-Datentypen bezeichnet werden:

signed char short int int long int long long int
unsigned char unsigned short int unsigned int unsigned long int unsigned long long int

Variablen der Datentypen signed char, short int, int, long int und long long int können sowohl positive als auch negative Werte annehmen, während Variablen der Datentypen unsigned xxx nur positive Werte (einschließlich der 0) annehmen können.

Bei den Datentypen short, long und long long kann die Angabe von int auch entfallen, d.h. anstelle short int kann  auch lediglich short stehen. Und bei den Datentypen short int, int, long int  und long long int kann noch explizit das Schlüsselwort signed vorangestellt werden, also zum Beispiel signed short int. Aber das ist nun wirklich die Langfassung. In der Praxis haben sich die Schreibweisen short, long, long long, unsigned short, unsigned long und unsigned long long eingebürgert. Software-Entwickler schreiben nun einmal nicht gerne mehr als unbedingt notwendig ist.

AchtungBei den char Ganzzahl-Datentypen ist signed bzw. unsigned zwingend vorgeschrieben, da es auch noch den Datentyp char gibt, der später behandelt wird. Dieser char Datentyp ist nicht identisch mit dem signed char bzw. unsigned char Datentyp.

Der Unterschied zwischen den einzelnen Datentypen liegt im Wertebereich, den eine entsprechende Variable aufnehmen kann und damit indirekt auch in deren Speicherbedarf. Die Sprache C++ schreibt die Wertebereiche der Datentypen nicht explizit vor, jedoch muss immer folgender Zusammenhang gelten:

signed char   = 1 Byte
signed char  <=  short
short  <= int
int  <= long
long long  >= 64 Bit Länge

1 Byte ist definiert als die Speichergröße, die benötigt wird um ein Zeichen abzulegen. Dies sind in der Regel 8 Bit. Es mag aber durchaus Implementierungen geben, bei denen 1 Byte aus 4 Bit oder gar 16 Bit besteht. Der C++ Standard sagt darüber nichts aus. Jedoch ist dem Autor zurzeit keine gängige Plattform bekannt, bei der 1 Byte nicht 8 Bit groß ist.

Somit können Integer-Variablen nun wie folgt definiert werden.

PgmHeader
// short Variable definieren
short myValue;
// Variable mit großem Wertebereich definieren
signed long long counter;
// Variable mit kleinem Wertebereich mit nur positiven Zahlen
unsigned char index;

Welchen Wertebereich die einzelnen Datentypen aufnehmen können, hängt letztendlich vom verwendeten Compiler und der verwendeten Rechner-Plattform ab. In der nachfolgenden Tabelle sind einmal die jeweiligen Wertebereiche des MinGW Compilers aufgeführt.

Datentyp    Wertebereich
signed char    -128...127
short    -32768... 32767
long    -2147483648...2147483647
long long    -9223372036854775808...9223372036854775807
unsigned char    0...255
unsigned short    0...65535
unsigned long    0...4294967295
unsigned long long    0..18446744073709551615

Mehr zu den auf Ihrem System gültigen Wertebereichen und sonstigen Eigenschaften der verschiedenen Datentypen können Sie sich gleich noch ansehen.

DetailWenn Sie wollen, können Sie sich durch Anklicken des Symbols links auch einmal ansehen, wie negative Zahlen im 2er Komplement dargestellt werden.

Gleitkomma-Datentypen

Für die Verarbeitung von Gleitkommazahlen stehen 3 verschiedene Datentypen zur Verfügung: float, double und long double. Der 'kleinste' Gleitkomma-Datentyp ist der Datentyp float, gefolgt vom Datentyp double und long double.

Diese Datentypen unterscheiden sich zum einen im darstellbaren Wertebereich und zum anderen, was fast noch wichtiger für Berechnungen ist, durch die Anzahl der Stellen, mit denen der Datentyp arbeitet.

Berechnung : 1./3.
float : 0.3333333432674407959
double : 0.3333333333333333148
long double : 0.3333333333333333333

Zeichen-Datentypen

Zum Abspeichern von Zeichen werden die Datentypen char und wchar_t verwendet. Der Datentyp char dient zur Aufnahme eines 1 Byte großen Zeichens, während der Datentyp wchar_t ein Zeichen eines erweiterten Zeichensatzes aufnehmen kann. In der Regel werden in wchar_t Zeichen abgelegt, welche nicht in einem Byte Platz finden. Denken Sie hierbei z.B. an den chinesischen Zeichensatz.

PgmHeader
// char Variable definieren
char anyCharacter;
// Variable für den erweiterten Zeichensatz definieren
wchar_t unicodeChar;
AchtungDer Datentyp char ist ein eigenständiger Datentyp und entspricht nicht dem Datentyp signed char! Dieser Sachverhalt spielt später beim Überladen von Funktionen/Memberfunktionen noch wichtige Rolle.

Weitere explizite Datentypen

Und gleich haben wir die grundlegenden Datentypen (wenigstens vorläufig) geschafft. Was jetzt noch fehlt, sind die beiden Datentypen bool und void.

Der Datentyp bool kann nur die beiden Zustände (Werte) true und false annehmen. Er wird immer dann eingesetzt, wenn im Programm Bedingungen auf wahr (erfüllt) oder nicht wahr (nicht erfüllt) verglichen werden soll.

PgmHeader
// Eine bool Variable definieren
bool allDone;

Der Datentyp void ist ein unvollständiger Datentyp, der niemals alleine auftritt und auch nicht bei der Definition einer Variable verwendet werden kann. Er wird hauptsächlich als Returntyp bei Funktionen/Memberfunktionen oder im Zusammenspiel mit Zeigern verwendet. Auf ihn werden wir später noch zurückkommen.

Es sei an dieser Stelle noch darauf hingewiesen, dass es noch weitere Datentypen gibt, die erst später behandelt werden. Dies sind im Einzelnen:

Sonstiges zur Variablendefinition

Variablen lassen sich bei ihrer Definition gleich mit einem bestimmten Wert initialisieren (vorbelegen). Dies erfolgt in der Art, dass nach der Definition der Variable der Zuweisungsoperator = folgt und danach der Initialwert.

PgmHeader
// int-Variable initialisieren
int startValue = 10;
// double-Variable initialisieren
double oneThird = 1.0/3.0;
// bool-Variable initialisieren
bool isDone = false;

Einer bei ihrer Definition initialisierten Variable können im Verlaufe des Programms jederzeit andere Werte zugewiesen werden. Und selbstverständlich muss der Datentyp des Initialwerts auch mit dem Datentyp der Variable übereinstimmen. Mehr zum Datentyp des Initialwerts gleich bei der Behandlung von Konstanten.

Wenn eine Variable bei ihrer Definition initialisiert wird, so kann der Compiler aufgrund des Datentyps des Initialisierungsausdrucks automatisch den Datentyp der Variable bestimmen. Damit der Compiler diese tut, wird bei der Variablendefinition der bisherige explizite Datentyp durch das Schlüsselwort auto ersetzt. Damit lassen sich die vorherigen Variablendefinitionen dann wie folgt umschreiben:

PgmHeader
// int-Variable initialisieren
auto startValue = 10;
// double-Variable initialisieren
auto oneThird = 1.0/3.0;
// bool-Variable initialisieren
auto isDone = false;
AchtungDie Definition einer Variable mittels auto entfernt die Qualifizierer const und volatile und sowie die Referenz. Den Qualifizierern ist noch ein eigenes Kapitel gewidmet und was es mit der Referenz auf sich hat, das erfahren Sie gleich im nächsten Abschnitt.

Außerdem können mehrere Variable des gleichen Datentyps innerhalb einer Anweisung definiert werden. Die Namen der zu definierenden Variablen werden durch Komma voneinander getrennt. Zusätzlich können die Variablen bei Bedarf auch gleich noch initialisiert werden.

PgmHeader
// zwei int Variablen definieren
int startValue, endValue;
// zwei double-Variable definieren und initialisieren
double beginOfRange = -2.5, endOfRange = 2.5;

Referenz-Variablen

Referenz-Variablen enthalten Verweise auf bereits definierte Variablen, d.h. es kann ein und dieselbe Variable unter zwei Namen angesprochen werden.

Um eine Referenz-Variable zu definieren, folgt nach dem Datentyp das Symbol &, dann der Name der Referenz-Variable RNAME und anschließen der Namen der Variable VNAME, auf die die Referenz verweisen soll.

DTYP& RNAME = VNAME;

Referenz-Variablen unterliegen folgenden Einschränkungen:

Das nachfolgende Beispiel zeigt, wenn auch nicht ganz praxisgemäß, die Verwendung von Referenz-Variablen.

PgmHeader
// int-Variablen definieren
int val10 = 10;
int val20 = 20;
// Referenz-Variable definieren
// refVar verweist danach auf val10

int& refVar = val10;
// Ausgabe von val10 über Referenz
cout << refVar << endl;   // gibt 10 aus
// Zuweisung an eine Referenzvariable
// refVar verweist danach immer noch auf val10
// jedoch enthaelt val10 danach den Wert von val20

refVar = val20;
// Ausgabe
cout << refVar << endl;  // gibt jetzt 20 aus

Vielleicht mag im Augenblick die Verwendung von Referenz-Variablen noch etwas merkwürdig erscheinen, aber später werden Sie den Einsatz von Referenz-Variablen noch zu schätzen wissen.

AchtungDie Definition einer Variable mittels auto entfernt u.a. die Referenz.
PgmHeader
// Variable definieren
long var1;
// Referenz auf Variable
long& refVar = var1;
// Datentyp von var2 ist long und nicht long&
auto var2 = refVar;

Typdefinition mittels decltype

Mit Hilfe des Operators decltype kann der Datentyp eines Ausdrucks bestimmt und damit unter anderem den Datentyp einer Variablen indirekt festlegt werden. Die allgemeine Syntax lautet:

decltype (AUSDRUCK) VARIABLE;

Je nach Typ von AUSDRUCK liefert decltype folgenden Datentyp zurück:

  1. Ist AUSDRUCK eine Variable, liefert decltype den Datentyp der Variable zurück.
  2. Ist AUSDRUCK eine Funktion, so liefert decltype den Returntyp der Funktion zurück (Funktionen werden später noch ausführlich behandelt). Der Returntyp der Funktion darf nicht vom Typ void sein und die Funktion selbst wird nicht ausgeführt.
  3. Ist AUSDRUCK ein Ausdruck, welcher rechts vom Zuweisungsoperator = steht (auch als rvalue bezeichnet), liefert decltype den Datentyp des Ergebnisses des Ausdrucks zurück.
  4. Ist AUSDRUCK ein Ausdruck, welcher links vom Zuweisungsoperator = steht (auch als lvalue bezeichnet), liefert decltype einen Referenzdatentyp zurück.
  5. Wird für AUSDRUCK das Schlüsselwort auto angegeben, so liefert decltype den exakten Datentypen des rechts vom Zuweisungsoperator stehenden Ausdrucks.

Im folgenden (nicht wirklich praxisbezogenen) Beispiel besitzt die Variable dvar stets denselben Datentyp wie die Variable var. D.h. wenn der Datentyp von var geändert wird, ändert sich der Datentyp von dvar automatisch mit. In der dritten Anweisung erhält svar den Datentyp des Returnwertes der Funktion MyFunc(). Liefert MyFunc() z.B. einen int-Wert zurück, so ist der Datentyp von svar ein int.

PgmHeader
// Variable definieren
int var;
// dvar erhaelt den gleichen Datentyp var
decltype(var) dvar;
// Datentyp einer Variablen aus Returntyp einer Funktion bestimmen
decltype(MyFunc()) svar;

Wie im letzten Punkt erwähnt, kann innerhalb der Klammer auch das Schlüsselwort auto stehen. In diesem Fall muss die Variable selbstverständlich initialisiert werden und hat den exakten Datentyp des Initialisierungsausdrucks. Und damit ist es möglich, einer auto-Variablen auch eine Referenz zuzuweisen. Das weiter oben aufgeführte Beispiel zur Referenzvariablen lässt sich damit wie folgt umschreiben:

PgmHeader
// Variable definieren
long var1;
// Referenz auf Variable
long& refVar = var1;
// Datentyp von var2 ist nun long& und nicht mehr long
decltype(auto) var2 = refVar;

Primär ist diese Art der Variablendefinition dazu gedacht, Rückgaben von Funktionen per Referenz aufzunehmen, da eine einfache auto-Definition bekanntermaßen die Referenz entfernt.

Nach dem wir uns nun die Definitionen von Variablen angesehen haben, ein Ratschlag:

HinweisDefinieren Sie Variablen genau an der Stelle im Programm, an der sie das erste Mal verwendet werden und lassen Sie die Datentypen der Variablen mittels auto durch den Compiler bestimmen. Sie vermeiden dadurch zum einen, dass es uninitialisierte Variablen gibt und zum anderen können Sie sicher sein, dass die Variablen auch genau das enthalten, was rechts vom Zuweisungsoperator steht. Der strengen Typsicherheit von C++ tut dies keinen Abbruch!

 

Eigenschaften von Datentypen

DetailWenn Sie wieder das Symbol links anklicken, können Sie sich ein kleines Programm ansehen, das einige Eigenschaften für die vorgestellten Datentypen ermittelt. Für die Ermittlung der Eigenschaften brauchen Sie das Programm (noch) nicht zu vollständig verstehen, da es hier schon ziemlich 'ins Eingemachte' geht (Stichwort: Templates). Geben Sie beim Aufruf der Funktion PrintNumLimits<DTYP>() innerhalb den spitzen Klammern lediglich den Datentyp an, dessen Eigenschaften Sie ermitteln wollen. Übersetzen das Programm dann erneut und starten es dann.
Aufruf Datentyp Eigenschaften
PrintNumLimits<unsigned long>(); Minimum: 0
Maximum: 4294967295
Anzahl Bits (ohne Vorzeichen): 32
Vorzeichenbehaftet: false
Ganzzahl (Integer): true
keine Rundungsfehler möglich: true
kleinster von 1 abweichender Wert: 0
PrintNumLimits<double>(); Minimum: 2.22507e-308
Maximum: 1.79769e+308
Anzahl Bits (ohne Vorzeichen): 53
Vorzeichenbehaftet: true
Ganzzahl (Integer): false
keine Rundungsfehler möglich: false
kleinster von 1 abweichender Wert: 2.22045e-016

Wie der Aufstellung u.a. entnommen werden kann, gibt es beim Rechnen mit dem unsigned long Datentyp keine Rundungsfehler (wie übrigens bei allen Integer-Datentypen), während es beim double-Datentyp (wie bei allen Gleitkomma-Datentypen) Rundungsfehler geben kann. Dies liegt darin begründet, dass wegen der internen Darstellung von Gleitkommazahlen nicht jede beliebige Zahl exakt darstellbar ist.

sizeof() Operator

Um den von einem Datentyp oder einer Variable belegten Speicherplatz in Bytes zu ermitteln, steht der Operator sizeof(ARG) zur Verfügung. ARG gibt den Datentyp oder die Variable an, deren Speicherbedarf ermittelt werden soll.

PgmHeader
auto anyValue = 10;  // int-Variable definieren
...
cout << sizeof(char) << endl;        // sizeof(datentyp)
cout << sizeof(anyValue) << endl;    // sizeof(variable)
cout << sizeof(long long) << endl;   // sizeof(datentyp)
Programmausgabe 1
 4
 8

sizeof() ist ein Compilezeit-Ausdruck, d.h. der Compiler wird schon beim Übersetzen des Programms die Größe berechnen und entsprechende in den Code einsetzen. Daraus folgt, dass sizeof() selbst keine zusätzliche Programmlaufzeit benötigt!

Alignment

Unter Alignment versteht man die Ausrichtung von Daten im Speicher. Da immer mehr sogenannte Embedded Systeme (hardware-nahe Systeme zur Steuerung, Überwachung und Regelung) auch in C++ programmiert werden, bei denen die Ausrichtung von Daten eine wichtige Rolle spielen kann, bietet C++ die Möglichkeit, Daten explizit an Speichergrenzen auszurichten.

HinweisErstellen Sie ausschließlich Applikationen für den PC, so können Sie diesen Abschnitt überspringen und gleich zum nächsten Kapitel übergehen. Auf dem PC spielt die Ausrichtung von Daten in der Regel keine Rolle, außer Sie wollen direkt auf die Hardware zugreifen.

Um Daten auf eine bestimmte Speichergrenze auszurichten, ist vor dem Datentyp der Spezifizierer alignas(ARG) anzugeben. Der Parameter ARG bestimmt die Speichergrenze und kann eine Konstante oder einer der Grunddatentypen sein. Die mit alignas() vorgenommene Ausrichtung gilt aber nur für dieses eine Datum.

Ferner steht der Operator alignof(ARG) zur Verfügung, um das Alignment des im ARG angegeben Datentyps oder einer der Variablen zu ermitteln.

PgmHeader
// main() Funktion
int main()
{
    auto val1 = 123;               // int-Variable anlegen
    alignas(1024) auto val2 = 987; // int-Variable auf 1kB Grenze ausrichten
    cout << &val1 << endl;         // Adressen der Variablen ausgeben
    cout << &val2 << endl;         // Der Adress-Operator & wird spaeter noch behandelt
    cout << alignof(val1) << endl; // Alignment ausgeben
    cout << alignof(val2) << endl;
}
Programmausgabe 0x28f7fc
 0x28f400
 4
 1024

Wie der Ausgabe entnommen werden kann, wird die Variable auf val2 auf einer 1kB Grenze ausrichtet (1kB = 1024 = 0x400).

Ein Beispiel und eine Übung entfallen an dieser Stelle, da eine alleinige Definition von Variablen keinen Sinn macht und wir den Zuweisungsoperator noch nicht behandelt haben.