Datentypen und Variablen

Unser Hauptanliegen in dieser Vorlesungsreihe besteht darin, die Eigenschaften von physikalischen Systemen und die Lösung von mathematischen Problemstellungen mittels des Computers zu simulieren. Hat man ein physikalisches Problem in einer oder mehreren mathematischen Gleichungen formuliert und quantifiziert, muss man diese Ausdrücke in eine dem Computer verständlichen Form bringen, z.B. die Gleichungen in einem C++ Programm implementieren. Die in den Gleichungen auftretenden Variablen beschreiben Eigenschaften des physikalischen Systems, die das Programm dann berechnen soll.
Nehmen wir zum Beispiel das mathematische Problem der Berechnung der Kreiszahl $\pi = 3.141592653589793...$ und betrachten die im Jahre 1682 von Gottfried Wilhelm Leibniz vorgestellte iterative Annäherung an diese irrationale Zahl in Form der Gleichung der Leibniz-Reihe: \[ \begin{equation} \sum_{k=0}^N \frac{(-1)^k}{2 k +1} = 1 - \frac{1}{3} + \frac{1}{5} \, - ... + \frac{(-1)^N}{2 N +1} ,\quad \hbox{mit:} \quad \sum_{k=0}^\infty \frac{(-1)^k}{2 k +1} = \frac{\pi}{4} \end{equation} \] Möchte man diese Gleichung in einem C++ Programm implementieren, muss man das Ergebnis und die Summenformel, inklusive ihrer Variablen ($k$ und $N$), im C++ Programm deklarieren. Ähnlich, wie bei einer präzisen mathematischen Definition, deklariert man die Variablen in einem C++ Programm auch anhand ihrer Zahlenmengen-Zugehörigkeit. Die Variablen $k$ und $N$ sind Element der natürlichen Zahlen ($k \in ℕ $ und $N \in ℕ $) und eine entsprechende Deklaration der Variable $k$ in einem C++ Programm lautet z.B.:
Deklaration der Variable $k$: "int k ;"
Der Zusatz int am Anfang bedeutet, dass die Variable k aus der Zahlenmenge der ganzen Zahlen ℤ stammt und das Semikolon am Ende markiert einfach das Ende eines jeden C++ Ausdruckes. Genau genommen hätte man die Variable k wohl besser als unsigned int deklarieren sollen (siehe untere Tabelle der C++ Datentypen), da die Indexvariable $k$ in der obigen Leibniz-Reihe nur positive ganze Zahlenwerte annehmen kann. Durch die Deklaration "int k ;" wird der Typ der Variable als ganzzahliger "Integer Wert" festgelegt und im Hauptspeicher (RAM) des Computers ein gewisser Speicherplatz für den Wert der Variable reserviert.

C++ Datentypen, Variablen und der Wert einer Variable

Die Programmiersprache C++ stellt eine Vielzahl von Datentypen bereit und die folgende Tabelle listet einiger dieser Typbezeichner auf und gibt ihre Bedeutung und den im Hauptspeicher reservierten Speicherbedarf in Byte (zusammengehörige Folge von acht Bits) an.

Datentyp Bedeutung Speicherbedarf in Byte
bool boolescher Wert (true oder false) 1
char Zeichen, z.B. "a", "z" oder "9" 1
int Ganzzahliger Wert $\in$ ℤ 4
short Kurzer ganzzahliger Wert $\in$ ℤ 2
long Langer ganzzahliger Wert $\in$ ℤ 8
unsigned int Positiver ganzzahliger Wert $\in$ ℕ 4
unsigned short Positiver kurzer ganzzahliger Wert $\in$ ℕ 2
unsigned long Positiver langer ganzzahliger Wert $\in$ ℕ 8
float Reellwertiger Wert einfacher Genauigkeit $\in$ ℝ (6 Stellen) 4
double Reellwertiger Wert doppelter Genauigkeit $\in$ ℝ (15 Stellen) 8
long double Reellwertiger Wert noch höherer Genauigkeit $\in$ ℝ (19 Stellen) 16

Eine Variable, die als Datentyp bool deklariert ist, kann lediglich die binären Werte true oder false annehmen, wobei intern true als der Wert "1" und false als der Wert "0" festgelegt wird. Mittels des Datentyps char können einzelne Zeichen, Buchstaben oder Zahlenzeichen deklariert werden. Im Bereich der ganzzahligen Zahlentypen unterscheidet man Typen mit/ohne Vorzeichen und man hat unterschiedliche Größenbereiche zur Auswahl. Die wohl am häufigsten verwendete Typbezeichnung einer ganzen Zahl ist int und sie entspricht der mathematischen Definition $\in$ ℤ. Eine Variable, die als int-Typ deklariert wurde, kann jedoch nur Werte von ganzen Zahlen im Zahlenbereich von -2147483648 bis 2147483647 abbilden. Möchte man noch größere ganze Zahlen verwenden, muss man die entsprechende Variable als long deklarieren (Zahlenbereich -9223372036854775808 .. 9223372036854775807). Eine kurze ganze Zahl (Zahlenbereich -32768 .. 32767) deklariert man hingegen besser mit dem Typ short. Möchte man eine Variable deklarieren, die die Werte im Bereich der positiven ganzen Zahlen (natürliche Zahlenmenge ℕ) annimmt, so fügt man den Zusatz unsigned vor den jeweiligen Typbezeichner (z.B. unsigned int $\rightarrow \,\, \in$ ℕ).
Die Datentypen float, double und long double dienen zur Speicherung von Gleitkommazahlen, und Variablen, die mittels dieser Typen deklariert werden, können Zahlenwerte aus dem Zahlenbereich der reellen Zahlenmenge ($\in$ ℝ) abbilden. Die Speicherung dieser Zahlen erfolgt in der Form "Vorzeichen, Exponent und Mantisse" (näheres siehe ....) und die einzelnen Gleitkommazahlen-Typen stellen Bezeichner für unterschiedliche Genauigkeitsbereiche dar.
Betrachten wir zum Beispiel den folgenden Quelltext des C++ Programms "Datentypen_1.cpp".

Datentypen_1.cpp
#include <iostream>                                    // Ein- und Ausgabebibliothek
using namespace std;                                   // Benutze den Namensraum std

int main(){                                            // Hauptfunktion
    bool b;                                            // Deklaration der boolschen Variable 'b'
    char zeichen;                                      // Deklaration der char Variable 'zeichen'
    int ganze_zahl;                                    // Deklaration der ganzen Zahl Variable 'ganze_zahl'
    unsigned int natueliche_zahl;                      // Deklaration der natürlichen Zahl Variable 'natueliche_zahl'
    short kurze_ganze_zahl;                            // Deklaration der kurzen ganzen Zahl Variable 'kurze_ganze_zahl' 
    long lange_ganze_zahl;                             // Deklaration der langen ganzen Zahl Variable 'lange_ganze_zahl'
    float reelle_zahl;                                 // Deklaration der reellen Zahl Variable 'reelle_zahl'                  
    double reelle_zahl_doppeltgenau;                   // Deklaration der reellen Zahl Variable 'reelle_zahl_doppeltgenau' mit doppelter Genauigkeit
    long double reelle_zahl_long;                      // Deklaration der reellen Zahl Variable 'reelle_zahl_long' mit 19 Stellen 

    b = true;                                        // Initialisierung (Festlegung des Wertes) der boolschen Variable 'b'
    zeichen = 'G';                                     // Initialisierung der char Variable 'zeichen'
    ganze_zahl = -30256;                               // Initialisierung der ganzen Zahl Variable 'ganze_zahl'
    natueliche_zahl = 3278978;                         // Initialisierung der natürlichen Zahl Variable 'natueliche_zahl'
    kurze_ganze_zahl = 245;                            // Initialisierung der kurzen ganzen Zahl Variable 'kurze_ganze_zahl'
    lange_ganze_zahl = 327244582478947248;             // Initialisierung der langen ganzen Zahl Variable 'lange_ganze_zahl'
    reelle_zahl = 2.4573;                              // Initialisierung der reellen Zahl Variable 'reelle_zahl'
    reelle_zahl_doppeltgenau = 2453.4573545742;        // Initialisierung der reellen Zahl Variable 'reelle_zahl_doppeltgenau'
    reelle_zahl_long = 7653.3467587475842L;            // Initialisierung der reellen Zahl Variable 'reelle_zahl_long'
    
    cout << "Die Variable 'b' hat den Wert " << b;                     // Ausgabe eines Textes im Terminal
    cout << " und für sie wurde ein Speicherplatz von ";               // ...
    cout << sizeof(b) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'zeichen' hat den Wert " << zeichen;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(zeichen) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'ganze_zahl' hat den Wert " << ganze_zahl;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(ganze_zahl) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'natueliche_zahl' hat den Wert " << natueliche_zahl;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(natueliche_zahl) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'kurze_ganze_zahl' hat den Wert " << kurze_ganze_zahl;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(kurze_ganze_zahl) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'lange_ganze_zahl' hat den Wert " << lange_ganze_zahl;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(lange_ganze_zahl) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'reelle_zahl' hat den Wert " << reelle_zahl;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(reelle_zahl) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'reelle_zahl_doppeltgenau' hat den Wert " << reelle_zahl_doppeltgenau;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(reelle_zahl_doppeltgenau) << " Byte im Hauptspeicher reserviert." << endl;
    cout << "Die Variable 'reelle_zahl_long' hat den Wert " << reelle_zahl_long;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(reelle_zahl_long) << " Byte im Hauptspeicher reserviert." << endl;
}

Es werden zunächst diverse Variablen unterschiedlicher Datentypen deklariert (bool b; , char zeichen; , ...) und danach wird ihnen ein ihnen spezifischer Wert zugewiesen - eine sogenannte Initialisierung der Variablen (b = true; , zeichen = 'G'; , ..., reelle_zahl_long = 7653.3467587475842L;), wobei das L am Ende des Initialisierungswertes der long double Variable "reelle_zahl_long" bedeutet, dass es sich um einen long double Initialisierungswert handelt. In den folgenden Zeilen des Quelltextes werden dann die Werte der einzelnen, nun definierten Variablen und ihr im Hauptspeicher reservierter Speicherplatz in Byte ausgegeben. Der Speicherplatz einer Variable wird hierbei mittels des Befehls sizeof( Variablenname ) ermittelt.
Wir kompilieren das Programm und führen es im Terminal aus (näheres siehe Das erste C++ Programm (Hello World)). Das untere Bild zeigt dies auf meinem Computer.

Die Ausgabe nach dem Ausführen des Programms ist meist korrekt, jedoch erkennt man bei den letzten beiden Ausgabezeilen, dass die ausgegebenen Werte der Variablen "reelle_zahl_doppeltgenau" und "reelle_zahl_long" nicht den initialisierten Werten entsprechen. Dies hat jedoch lediglich mit den Eigenschaften des Ausgabebefehls "cout" zu tun, da standardmäßig nur eine begrenzte Anzahl von Stellen bei Werten von Variablen ausgegeben wird. Die Genauigkeit der cout-Ausgabe kann man mit dem Befehl "std::cout.precision(...);" verändern und im Programm Datentypen_1a.cpp wurde eine Zeile mit dem Befehl "std::cout.precision(40);" vor den cout-Befehlen dem Quelltext hinzugefügt und somit die Genauigkeit der cout-Ausgabe auf 40 festgelegt. Kompiliert man dieses Programm (beim Komilierungsprozess wird das alte ausführbare Programm "a.out" überschrieben, siehe unteres Bild)

und führt es im Terminal aus so erhält man die folgende Ausgabe:

Die ausgegebenen Werte der Variablen "reelle_zahl_doppeltgenau" und "reelle_zahl_long" stimmen nun gut mit den initialisierten Werte überein. Jedoch fällt auf, dass die ausgegebenen Werte nicht exakt mit den initialisierten übereinstimmen. Dies liegt daran, dass im Computer deklarierte Gleitkommazahlen einer Zahlenmenge entstammen (sagen wir ℝ$_C$), die nur eine echte Teilmenge der reellen Zahlen ℝ ist ℝ$_C \subsetneq$ ℝ. Die möglichen Gleitkommazahlen, die in einem C++ Programm definiert werden können, sind diskrete Werte mit einer begrenzten Anzahl von Nachkommastellen und die reellwertigen Zahlen, die sich zwischen zwei Maschinenzahlnachbarn befinden, können nicht exakt abgebildet werden. Der reellwertige Raum ℝ der Mathematik kann somit mittels Computersimulationen nicht abgebildet werden und mathematische Begriffe wie z.B. $\infty$ und transzendente Zahlen wie z.B. die Kreiszahl $\pi$ können mit dem Computer nicht exakt realisiert werden. Es können sogar nicht alle Brüche (Zahlen aus der Menge der rationalen Zahlen ℚ $\subsetneq$ ℝ) genau dargestellt werden und diese werden in einem C++ Programm nur mit einer gewissen Genauigkeit approximiert (z.B. $1/3=0.333...$). Näheres finden Sie am Ende des noch folgenden Unterkapitels Die Ein- und Ausgabe und im Unterkapitel Die Computerarithmetik und der Fehler in numerischen Berechnungen.

Die Initialisierung einer Variable

Im Programm Datentypen_1.cpp hatten wir bereits die Initialisierung von zuvor deklarierten Variablen kennengelernt. Diese Initialisierung (Festlegung des nummerischen Wertes) erfolgte mittels des Gleichheitszeichens, z.B. "reelle_zahl = 2.4573;". Man hätte diese Initialisierung auch direkt bei der Deklaration der Variable durchführen können und man spricht dann von einer Variablen-Definition. Man kann die gleichzeitige Deklaration und Initialisierung einer Variable auf unterschiedliche Arten schreiben und die folgenden drei Varianten Definieren alle die ganze Zahl 4 als Integer Typ

int ganze_zahl = 4; 	 oder 	 int ganze_zahl (4); 	 oder 	 int ganze_zahl {4}; 

,wobei die letzte Form eine universelle Art der Initialisierung darstellt, die auch bei Initialisierungslisten verwendet wird.
Initialisiert man gleich bei der Deklaration, ist es nicht mehr nötig den Typ explizit anzugeben und man benutzt dafür dann den Bezeichner auto welcher beim Initialisierungsprozess den Typ automatisch festlegt. Im folgenden Programm (Datentyp_auto.cpp) werden die unterschiedlichen Initialisierungsschreibweisen und der Bezeichner auto benutzt:

Datentyp_auto.cpp
#include <iostream>                                // Ein- und Ausgabebibliothek

using namespace std;                               // Benutze den Namensraum std

int main(){                                        // Hauptfunktion
    int ganze_zahl_1 = 4;                          //Deklaration(Definition) einer ganzen Zahl und gleichzeitige Initialisierung mit dem Wert 4
    int ganze_zahl_2 (3);                          //Deklaration(Definition) einer zweiten ganzen Zahl und gleichzeitige Initialisierung mit dem Wert 3 
    int ganze_zahl_3 {5};                          //Deklaration(Definition) einer dritten ganzen Zahl und gleichzeitige Initialisierung mit dem Wert 5 
    
    auto zahl_1 (3);                               // Definition einer Integer Zahl mittels auto
    auto zahl_2 (2.4568858679456457956);           // Definition einer langen Gleitkommazahl mittels auto
    auto zahl_3 (2.4568858679456457956L);          // Definition einer langen Gleitkommazahl mittels auto
    
    std::cout.precision(20);                           // Festlegung der Genauigkeit der cout-Ausgabe
    
    cout << "Die Variable 'ganze_zahl_1' hat den Wert " << ganze_zahl_1;          // Ausgabe eines Textes im Terminal
    cout << " und für sie wurde ein Speicherplatz von ";                          // ...
    cout << sizeof(ganze_zahl_1) << " Byte im Hauptspeicher reserviert." << endl;
    
    cout << "Die Variable 'ganze_zahl_2' hat den Wert " << ganze_zahl_2;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(ganze_zahl_2) << " Byte im Hauptspeicher reserviert." << endl;
    
    cout << "Die Variable 'ganze_zahl_1' hat den Wert " << ganze_zahl_3;
    cout << " und für sie wurde ein Speicherplatz von ";
    cout << sizeof(ganze_zahl_3) << " Byte im Hauptspeicher reserviert." << endl;
    
    cout << "Die Variable 'zahl_1' hat den Wert " << zahl_1;
    cout << " und für sie wurde ein Speicherplatz von "; 
    cout << sizeof(zahl_1) << " Byte im Hauptspeicher reserviert." << endl;
    
    cout << "Die Variable 'zahl_2' hat den Wert " << zahl_2;
    cout << " und für sie wurde ein Speicherplatz von "; 
    cout << sizeof(zahl_2) << " Byte im Hauptspeicher reserviert." << endl;
    
    cout << "Die Variable 'zahl_3' hat den Wert " << zahl_3;
    cout << " und für sie wurde ein Speicherplatz von "; 
    cout << sizeof(zahl_3) << " Byte im Hauptspeicher reserviert." << endl;
}

Wir kompilieren das Programm und führen es im Terminal aus:

Man erkennt in der Terminalausgabe, dass mittels des Bezeichners auto die automatische Deklaration und Typfestlegung durch den Initialisierungsprozess bewerkstelligt wurde.

Es ist manchmal hilfreich, eine Variable im Programm zu definieren, die sich während der gesamten Berechnung nicht verändern soll. Eine solche konstante Variable, wie z.B. die Gravitationskonstante oder der Wert der Lichtgeschwindigkeit, wird im Programm mittels des Typzusatzes const gekennzeichnet. Dieser Zusatz kann vor jeden der Datentypen bei ihrer Definition geschrieben werden (z.B. const int N = 12; ). Ein weiterer Ausdruck, der in ähnlicher Weise konstante Werte für den Compiler kennzeichnet, ist der Bezeichner constexpr und dieser wird ebenfalls als Zusatz vor den jeweiligen Typ bei der Deklaration geschrieben (z.B. constexpr double x = 1.4*N;). Der Wert der double-Variable x wird während des Kompilierungsprozesses zu dem konstanten Wert 1.4*N=16.8 initialisiert. An dieser Stelle wurde die Zahl 1.4 mit N multipliziert, wobei formal der arithmetische Operator der Multiplikation verwendet wurde. Auf die Arithmetik und Operatoren werden wir im nächsten Unterkapitel näher eingehen (siehe Arithmetik und Operatoren).