Variablen

Variablendefinition

Zur Programmlaufzeit veränderbare Daten werden in Variablen abgelegt. Eine Variable muss vor ihrer ersten Verwendung definiert werden.

DTYP vName;

DTYP bestimmt den Datentyp der Variable (wird gleich erklärt) und vName ist der Variablenname. Der Name muss eindeutig sein, d.h., es darf keine gleichnamige zweite Variable, Funktion usw. im gleichen Gültigkeitsbereich geben. Was Gültigkeitsbereiche sind, wird später Thema sein. Eine Ausnahme von dieser "One-Definition-Rule (ODR)" bilden inline-Variablen, auf die gleich eingegangen wird.

Für den Namen einer Variable gelten folgende Einschränkungen:

Die maximale Länge eines Namens ist abhängig vom verwendeten Compiler, jedoch unterstützen die meisten C++-Compiler mindestens 64 Zeichen für Namen.

Verwenden Sie eine einheitliche Schreibweise für Variablen. In diesem Tutorial beginnen Variablennamen immer mit einem Kleinbuchstaben und an den Wortgrenzen werden bei Bedarf Großbuchstaben verwendet (z.B. maxTemp oder anzahlMessungen).

Im Augenblick spielt es keine Rolle, ob eine Variablendefinition vor oder innerhalb von main() erfolgt. Worin sich diese beiden Definitionen unterscheiden, folgt später. Variablen können an beliebiger Stelle im Programm definiert werden, was den Vorteil bietet, dass sie an der Stelle definiert werden können, an der sie das erste Mal verwendet werden.

Integer-Datentypen

Um Integer-Daten (Ganzzahlen) in Variablen abzulegen stehen folgende Datentypen zur Verfügung:

Integer mit Vorzeichen Integer ohne Vorzeichen
signed char unsigned char
short unsigned short
int unsigned int
long unsigned long
long long unsigned long long

Die Tabelle enthält die gebräuchlichsten Schreibweisen für die Datentypen. Bei den short, long und long long Datentypen kann optional das Schlüsselwort int mit angegeben werden. Außerdem kann bei den vorzeichenbehafteten Datentypen das Schlüsselwort signed noch stehen. Die Position von int und signed bzw. unsigned ist nicht relevant, d.h. Sie können sowohl signed int short als auch short int signed schreiben.

Der Unterschied zwischen den Datentypen liegt im Wertebereich, den ein Datentyp abdeckt. Welchen Wertebereich die Datentypen besitzen, hängt vom Compiler und der Rechner-Plattform ab. In der nachfolgenden Tabelle sind beispielhaft die jeweiligen Wertebereiche des Microsoft- und MinGW-Compilers unter WINDOWS aufgeführt.

Datentyp Wertebereich
signed char [-127, 128]
short [-32768, 32767]
long (=int) [-2147483648, 2147483647]
long long [-9223372036854775808, 9223372036854775807]
unsigned char [0, 255]
unsigned short [0, 65535]
unsigned long (=unsigned int) [0, 4294967295]
unsigned long long [0, 18446744073709551615]
// short-Variable
short myValue;
// Variable mit großem Wertebereich
long long counter;
// Variable mit nur positivem Wertebereich
unsigned long time;

Wie erwähnt, liegt der Unterschied zwischen den einzelnen Datentypen im Wertebereich und damit auch in dem von einer Variable benötigten Speicherplatz. Ab C++20 sind folgende Größen für die Datentypen (signed und unsigned) garantiert:

(un)signed char ≥ 8 Bit
short ≥ 16 Bit
int ≥ 16 Bit
long ≥ 32 Bit
long long ≥ 64 Bit

Im Anhang F: Das 2er-Komplement ist aufgeführt wie negative Zahlen intern im 2er Komplement dargestellt werden.

Zeichen-Datentypen

Zeichen-Datentypen dienen zum Abspeichern von Zeichen (Buchstaben). C++ kennt folgende Zeichen-Datentypen:

Datentyp Verwendung
char ASCII Zeichen
wchar_t Zeichen eines erweiterten Zeichensatzes
char8_t Zeichen des UTF-8 Zeichensatzes
char16_t Zeichen des UTF-16 Zeichensatzes
char32_t Zeichen des UTF-32 Zeichensatzes

Zum Abspeichern eines Zeichens werden in der Regel 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, wie z.B. chinesische Schriftzeichen.

// char Variable
char anyCharacter;
// Variable für den erweiterten Zeichensatz
wchar_t extChar;

Der Datentyp char ist ein eigenständiger Datentyp und entspricht nicht dem Datentyp (un)signed char! Dieser Sachverhalt spielt später beim Überladen von Funktionen bzw. Methoden eine wichtige Rolle.

Die Datentypen char8_t, char16_t und char32_t dienen zum Verarbeiten von UTF-Zeichensätzen. Da diese nur in speziellen Fällen verwendet werden, wird auf diese Datentypen nicht weiter eingegangen.

C++ kennt standardmäßig nur die Ein- und Ausgabe von char und wchar_t Daten. char Daten werden, wie bekannt, mittels cout ausgegeben und wchar_t Daten mittels wcout.

Gleitkomma-Datentypen

Zum Verarbeiten von Gleitkommadaten stehen 3 Datentypen zur Verfügung: float, double und long double. Diese Datentypen unterscheiden sich zum einen im Wertebereich und zum anderen durch die Anzahl der Stellen, die für Berechnungen verwendet werden.

Datentyp Wertebereich Berechnung 1./3.
float 1.17549e-38, 3.40282e+38 0.3333333432674407959
double 2.22507e-308, 1.79769e+308 0.3333333333333333148
long double 2.22507e-308, 1.79769e+308 0.3333333333333333333

Beim Microsoft-Compiler entspricht der Datentyp long double dem Datentyp double.

Weitere Datentypen

Variablen vom Datentyp bool können nur die Zustände true und false annehmen.

bool allDone;

Dieser Datentyp wird eingesetzt, wenn Bedingungen auf erfüllt (true) oder nicht erfüllt (false) zu vergleichen sind.

Der Datentyp void ist ein unvollständiger Datentyp, der niemals alleine auftritt und nicht für die Definition von Variablen verwendet werden kann. Er wird hauptsächlich als Returntyp von Funktionen/Methoden oder bei Zeigern verwendet und wird in den entsprechenden Kapiteln besprochen.

Die bis hier aufgeführten Datentypen werden als intrinsische Datentypen bezeichnet, da sie durch den Compiler definiert sind.

Es gibt noch weitere Datentypen, die erst später behandelt werden. Dies sind u.a.:

Initialisierungen und auto-Datentyp

Eine Variable kann bei ihrer Definition auf mehrere Arten initialisiert werden.

signed char c1 = 10;   // Zuweisung
signed char c2 = (10); // Zuweisung in runden Klammern
signed char c3 = {10}; // Zuweisung in geschw. Kl.
signed char c4 (10);   // Init-Wert in runden Klammern
signed char c5 {10};   // Init-Wert in geschw. Kl.
signed char c6 = 300;  // Init-Wert zu gross, keine Fehlermeldung
signed char c7 = {300}; // Init-Wert zu gross, Fehlermeldung
signed char c8 {300};  // Init-Wert zu gross, Fehlermeldung

Die Initialisierungen ohne Klammern oder mit runden Klammern sind gleichwertig. Bei der Initialisierung mit geschweiften Klammern überprüft der Compiler beim Übersetzen des Programms zusätzlich, ob der Initialwert ohne Informationsverlust übernommen werden kann und gibt ansonsten eine Fehlermeldung aus. Diese Überprüfung zur Compilezeit wird als preventing narrowing bezeichnet.

Wird eine Variable bei ihrer Definition initialisiert, kann der Compiler aufgrund des Datentyps des Initialisierungsausdrucks automatisch den Datentyp der Variable bestimmen. Dazu wird bei der Variablendefinition der Datentyp durch das Schlüsselwort auto ersetzt.

Die Definition einer Variable mittels auto entfernt die Qualifizierer const und volatile sowie die Referenz vom Initialwert. Den Qualifizierern ist ein eigenes Kapitel gewidmet und was es mit der Referenz auf sich hat folgt gleich.

Die auto-Definition

auto var = {3};

definiert keine int-Variable, sondern eine int-Initialisiererliste. Deshalb mein Rat: Initialisieren Sie eine Variable entweder per Zuweisung ohne Klammern oder ohne Zuweisung mit Klammern

auto var1 = 3;
auto var2{3};

Mehrere Variablen des gleichen Datentyps können in einer Anweisung definiert werden. Die zu definierenden Variablen werden durch Komma voneinander getrennt. Zusätzlich können die Variablen initialisiert werden.

// zwei int-Variablen definieren
int startValue, endValue;
// zwei double-Variablen definieren und initialisieren
double beginOfRange = -2.5, endOfRange = 2.5;
// oder einfacher
auto first{-2.5}, second{2.5};

Werden mehrere Variablen in einer Zeile mittels auto definiert und initialisiert (siehe letzte Zeile), müssen alle Initialwerte denselben Datentyp besitzen.

inline-Variable

Durch Voranstellen des Spezifizierers inline vor dem Datentyp wird eine inline-Variable definiert. Sie kann innerhalb eines Programms mehrfach definiert werden, innerhalb einer Übersetzungseinheit (Quellcode-Datei) jedoch nur einmal. Sehen Sie sich hierzu das folgende Beispiel an.

// Datei common.h

// globalVar definieren/initialisieren
inline int globalVar = 10;
// Datei main.cpp

#include "common.h" // globalVar einbinden
#include <print>

extern void AnyFunc();

int main()
{
    // globalVar ausgeben
    std::println("main: globalVar = {}",globalVar);
    // AnyFunc ausfuehren
    // Funktionen werden spaeter noch erklaert
    AnyFunc();
}
// Datei another.cpp

#include "common.h" // ebenfalls globalVar einbinden
#include <print>

void AnyFunc()
{
    // globalVar ausgeben
    std::println("AnyFunc: globalVar = {}",globalVar);
}

main: globalVar = 10
AnyFunc: globalVar = 10

In der Datei common.h wird die Variable globalVar definiert und initialisiert. Diese Datei wird sowohl von main.cpp wie auch von another.cpp eingebunden. Ohne die Definition von globalVar als inline-Variable wäre die Variable damit doppelt definiert, was beim Linken des Programms zu einem Fehler führt. Dadurch, dass die Variable als inline definiert ist, kann der Linker diese Mehrfach-Definition auflösen und legt die Variable nur einmal an.

Später werden Sie ein weiteres Verfahren kennenlernen, um die Mehrfach-Definition von Daten aufzulösen. Stichwort: C++ Module

Referenz-Variable

Eine Referenz-Variable enthält einen Verweis auf ein bestehendes Datum und wird dadurch gekennzeichnet, dass nach dem Datentyp das Symbol & steht.

Eine Referenz-Variable unterliegt folgenden Einschränkungen:

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

#include <print>

int main()
{
    // int-Variablen definieren
    int val10 = 10;
    int val20 = 20;
    // Referenz-Variable definieren
    // refVar verweist danach auf val10
    auto& refVar = val10;     // oder: int& refVar{val10};
    // Ausgabe von val10 über Referenz
    std::println("{}",refVar);   // 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
    std::println("{}\n",refVar); // gibt 20 aus
}

10
20

Das Symbol & kann auch unmittelbar vor dem Namen der Referenzvariable oder zwischen dem Datentyp und dem Namen stehen.

DTYP &refName = datum; // oder auch
DTYP & refName = datum;

Die Definition einer Variable mittels auto entfernt u.a. die Referenz. Soll die Referenz erhalten bleiben, ist die Variable mittels auto& zu definieren (siehe Zeile 10).

Vielleicht mag im Augenblick die Verwendung von Referenzen etwas seltsam erscheinen. Später aber wird deren Einsatz sich als vorteilhaft erweisen.

Typdefinition mittels decltype

Der Spezifizierer decltype() liefert den Datentyp eines Ausdrucks und kann eingesetzt werden, um unter anderem den Datentyp einer Variable indirekt festzulegen. Die Syntax lautet:

decltype (AUSDRUCK)

Abhängig von AUSDRUCK liefert decltype() folgende Datentypen zurück:

Ein rvalue Ausdruck ist ein Ausdruck, der ein temporäres Ergebnis liefert oder von dem keine Adresse gebildet werden kann. Dementsprechend ist ein lvalue Ausdruck, vereinfacht ausgedrückt, alles was kein rvalue Ausdruck ist. So ist in

erg = 1 + var;

erg ein lvalue Ausdruck, da erg eine Variable ist und somit eine Adresse hat. 1 + var ist dagegen ein rvalue Ausdruck, da das Ergebnis des Ausdrucks nach der Zuweisung 'gelöscht' wird.

Im folgenden Beispiel besitzt die Variable dvar stets denselben Datentyp wie die Variable var. D.h., wird der Datentyp von var geändert, ändert sich automatisch der Datentyp von dvar. In Zeile 6 hat svar den Datentyp des Returnwertes der Funktion MyFunc(). Liefert MyFunc() z.B. einen int-Wert zurück, ist der Datentyp von svar ebenfalls ein int. Beachten Sie den Unterschied zwischen den Zeilen 6 und 8!

// Variable definieren
int var;
// dvar erhaelt den gleichen Datentyp wie var
decltype(var) dvar;
// Datentyp aus Returntyp einer Funktion bestimmen
decltype(MyFunc()) svar;
// Dies liefert aber einen Zeiger auf die Funktionen
decltype(MyFunc) zvar;

Wird decltype() zusammen mit auto verwendet, ist die Variable zu initialisieren. Die Variable hat dann den Datentyp des Initialisierungsausdrucks und damit ist es möglich, einer auto-Variable eine Referenz zuzuweisen.

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

Noch ein Tipp: Definieren Sie eine Variable an der Stelle im Programm, an der sie das erste Mal benötigt wird, und lassen Sie den Datentyp der Variable 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 das enthalten, was rechts vom Zuweisungsoperator steht.

Eigenschaften von Datentypen

Mithilfe der C++-Standardbibliothek können die Eigenschaften von Datentypen ermittelt werden um darauf bei Bedarf reagieren zu können. Im Anhang G: Eigenschaften von Datentypen ist ein kleines Programm angegeben, das einige Eigenschaften der vorgestellten Datentypen ausgibt. Beim Aufruf der Funktion PrintNumLimits(<DTYP>)() wird innerhalb der spitzen Klammer der Datentyp angegeben, dessen Eigenschaften ermittelt werden sollen.

Aufruf Datentyp Eigenschaft
PrintNumLimits<unsigned long>(); Min: 0
Max: 4294967295
Anzahl Bits (ohne Vorzeichen):32
Vorzeichenbehaftet: false
Integer: true
keine Rundungsfehler moeglich: true
kleinster von 1 abweichender Wert: 0
PrintNumLimits<double>(); Min: 2.22507e-308
Max: 1.79769e+308
Anzahl Bits (ohne Vorzeichen):53
Vorzeichenbehaftet: true
Integer: false
keine Rundungsfehler moeglich: false
kleinster von 1 abweichender Wert: 2.22045e-16

Wie der Tabelle u.a. entnommen werden kann, gibt es beim Rechnen mit unsigned long Daten keine Rundungsfehler (wie bei allen Integer-Daten), während es bei double Daten (wie bei allen Gleitkommadaten) Rundungsfehler geben kann. Dies liegt darin begründet, dass wegen der internen Darstellung von Gleitkommadaten nicht jedes beliebige Datum exakt darstellbar ist.

sizeof-Operator

Der Operator sizeof(ARG) liefert die von einem Datentyp oder einem Datum benötigte Anzahl von Bytes als size_t-Datum zurück. Ist ARG ein Datum, kann die Klammerung entfallen.

#include <iostream>

int main()
{
    auto anyValue = 10;  // int-Variable definieren
    std::cout << sizeof(char) << '\n';     // sizeof(datentyp)
    std::cout << sizeof anyValue << '\n';  // sizeof datum
    std::cout << sizeof(long long);        // sizeof(datentyp)
}

1
4
8

sizeof ist ein Compilezeit-Ausdruck, d.h., der Compiler berechnet beim Übersetzen des Programms das Ergebnis des sizeof-Operators und setzt das Ergebnis in den Code ein.

Der Datentyp size_t ist in der Header-Datei cstdlib definiert, welche in dem meisten Fällen aber nicht explizit inkludiert werden muss, da sie durch viele Standard-Header-Dateien inkludiert wird.

Übung

var_01

Definieren Sie 3 Variablen zum Abspeichern eines Geburtsdatums, bestehend aus Tag, Monat, Jahr. Die Variablen sind bei ihrer Definition zu initialisieren und es ist der kleinstmögliche Datentyp zu verwenden.

Geben Sie das Geburtsdatum wie unten angegeben aus. Die Ausgabe des Tags und Monats soll 2-stellige zu erfolgen.

Definieren Sie eine Referenzvariable, die auf das Geburtsjahr verweist. Geben Sie das Geburtsjahr über diese Referenzvariablen wie angegeben aus.

Geben Sie die Anzahl der für das Geburtsjahr benötigten Bytes aus.

Geburtsdatum ist der 01.05.1980
Geburtsjahr ist 1980
Geburtsjahr belegt 2 Bytes