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:
- Er muss mit einem Buchstaben oder Unterstrich beginnen,
- darf nicht mit einem reservierten Schlüsselwort übereinstimmen und
- darf keine Umlaute oder ß enthalten.
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:
- Eine Referenz-Variable muss bei ihrer Definition initialisiert werden.
- Nach der Initialisierung kann der Verweis nicht mehr geändert werden. Wird einer Referenz-Variable ein Datum zugewiesen, wird der Inhalt des Datums geändert, auf das die Referenz-Variable verweist.
- Der Datentyp der Referenz-Variable muss mit dem Datentyp des Datums übereinstimmen, auf das verwiesen wird. Am sichersten geht dies durch eine auto& Definition.
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:
- Ist AUSDRUCK eine Variable, liefert decltype() den Datentyp der Variable zurück.
- Ist AUSDRUCK ein Funktionsaufruf, liefert decltype() den Returntyp der Funktion zurück (Funktionen werden später ausführlich behandelt). Der Returntyp der Funktion darf nicht vom Typ void sein.
- Ist AUSDRUCK ein Funktionsname, liefert decltype() einen Funktionszeiger vom Typ der Funktion zurück.
- Ist AUSDRUCK ein rvalue Ausdruck, liefert decltype() den Datentyp des Ergebnisses des Ausdrucks zurück.
- Ist AUSDRUCK ein lvalue Ausdruck, liefert decltype() einen Referenzdatentyp zurück.
- Wird für AUSDRUCK das Schlüsselwort auto angegeben, liefert decltype() den exakten Datentypen eines rechts vom Zuweisungsoperator stehenden Initialisierungsausdrucks.
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