Elementare Syntax von C++: Fundamentale Datentypen

FĂŒr das Erlernen der Sprache C++ ist dieser Abschnitt zentral: es werden die wichtigsten fundamentalen Datentypen ausfĂŒhrlich vorgestellt, also Zeichen, Boolesche Variable, ganze Zahlen und Gleitkommazahlen. Die Datentypen sind immer verknĂŒpft mit den Operationen, die mit ihnen ausgefĂŒhrt werden können. Weiter werden die Typumwandlungen (und die damit verbundenen Fehlerquellen) zwischen den fundamentalen Datentypen besprochen.
Noch keine Stimmen abgegeben
Noch keine Kommentare

Einordnung des Artikels

In diesem Abschnitt wird die in

von einer speziellen Programmiersprache unabhÀngige Diskussion auf C++ angewendet. Um all die Spitzfindigkeiten zu verstehen, die einem C++-Programmierer hier Kummer bereiten können, ist es unerlÀsslich die Unterscheidung der fundamentalen Datentypen und die Speicherorganisation zu kennen.

Plattform-AbhÀngigkeit der Datentypen

Anders als in Java, sind fĂŒr C++ in der Sprach-Spezifikation keine exakten Festlegungen vorgenommen, wieviel Speicherplatz von den elementaren Datentypen belegt wird. Die meisten Festlegungen erfolgen in der Form von Ungleichungen, etwa

  • Datentyp 1 belegt mindestens n Byte oder
  • Datentyp 1 belegt mindestens so viel Speicherplatz wie Datentyp 2.

Wenn in den folgenden Abschnitten entsprechende Angaben gemacht werden, beziehen sie sich auf MinGW-Compiler unter Windows 10 (64 Bit). Auf anderen Plattformen können die Angaben abweichen.

Der Datentyp void

Der Datentyp void (wörtlich: void = leer, nichtig) ist ein Grenzfall:

  • man kann kein Objekt von diesem Datentyp bilden.
  • Aber eine Funktion kann als RĂŒckgabewert den Datentyp void besitzen: Sie fĂŒhrt dann lediglich Anweisungen aus, erzeugt aber keinen RĂŒckgabewert.

EingefĂŒhrt wurde der Datentyp void im Abschnitt Prozeduren und Funktionen im Kapitel Übersicht ĂŒber Programmiersprachen.

Zeichen

Eigenschaften von Zeichen

Die Datentypen, die in C++ fĂŒr Zeichen zur VerfĂŒgung stehen, werden in zwei Gruppen eingeteilt:

  1. narrow character types: char , signed char , unsigned char
  2. wide character types: char16_t , char32_t , wchar_t

Weiter unten folgt ein Programm (FundTyp.cpp), das zeigt, wie man diese Datentypen nĂ€her untersuchen kann — sie können auf unterschiedlichen Rechnern andere Eigenschaften haben.

Gesetzt werden Zeichen mit der Anweisung:

char c = 'x';

Hier wurde also der — gebrĂ€uchlichste — Datentyp char verwendet; der Name der Variable ist natĂŒrlich frei wĂ€hlbar. Wichtig ist, dass das Zeichen — hier x — in die sogenannten Hochkommas gesetzt wurde; falsch wĂ€re die Verwendung von AnfĂŒhrungsstrichen (diese werden fĂŒr string, also Zeichenketten, eingesetzt):

char c = "x";       // FALSCH! Compiler meldet Fehler

Das folgende Programm gibt aus, wieviele Zeichen ihr System unterstĂŒtzt, und gibt sie danach aus:

// FundTyp.cpp
// Informationen ĂŒber die fundamentalen Datentypen

#include <limits>
#include <iostream>
using namespace std;

int main(){

    int min = static_cast<int>(numeric_limits<char>::min());
    int max = static_cast<int>(numeric_limits<char>::max());
    
    cout << "Minimum: " << min << endl;      // Minimum = - 128
    cout << "Maximum: " << max << endl;      // Maximum = 127

    for (int i = min; i < max + 1; i++) {
        char c = i;
        cout << i << "\t\t" << c << endl;               // Ausgabe der Zeichen 
        cout << "sizeof c = " + sizeof c << endl;       // sizeof c = 1 (1 Byte)
    }
    
    cout << "sizeof (char) = " << sizeof (char) << endl;      // sizeof (char) = 1 (1 Byte)
}

dazu wird im Header (Zeile 4)

#include <limits>

geladen, um auf die entsprechenden Konstanten zugreifen zu können (Zeile 10 und 11):

numeric_limits<char>::min()
numeric_limits<char>::max()

Diese werden in den Zahlen min und max abgespeichert. (Den dabei eingesetzten static_cast<int> mĂŒssen Sie jetzt noch nicht verstehen.) Anschließend werden in einer for-Schleife alle Zeichen ausgegeben (Zeile 16 bis 20).

FĂŒr den ASCII-Code reichen 7 Bit (= 128 Zeichen); in C++ werden fĂŒr char 8 Bit = 1 Byte bereitgestellt. Der ASCII-Code findet sich dann in den Zeichen (als Zahlen interpretiert) 0 - 127, welche Zeichen in den anderen 128 Zeichen (meist entsprechend den Zahlen von -128 bis -1) kodiert werden, hĂ€ngt vom System ab — aber mit obigem Programm können Sie es herausbekommen.

Die Zeile 17 aus obigem Programm

char c = i;

ist nicht leicht zu verstehen: Wie kann man einem Zeichen eine Zahl zuordnen?

Da Zahlen und Zeichen intern identisch behandelt werden, ist diese Umwandlung möglich: auf der rechten Seite steht eine Zahl i, die zwischen min und max liegt, also wie ein Zeichen interpretiert werden kann. Und c wird gerade als dieses Zeichen gesetzt. Man nennt diesen Vorgang eine Typumwandlung (cast); manchmal muss er ausdrĂŒcklich durch den entsprechenden Befehl erzwungen werden, manchmal kann er automatisch durchgefĂŒhrt werden (wie in char c = i). Mehr dazu unter Typumwandlungen.

In der for-Schleife werden dann alle erlaubten Zeichen erzeugt und auf der Konsole (zusammen mit ihrer Kodierung) ausgegeben. Die Syntax der for-Schleife wird hier noch nicht erklÀrt; sie sollte aber nach den ErklÀrungen zu den Schleifen im Pseudocode klar sein. Die Anweisung (Zeile 16)

i++     // i wird fĂŒr einen Schleifendurchlauf verwendet; anschließend wird i = i + 1 gebildet

ist nur die AbkĂŒrzung fĂŒr i = i + 1.

Operatoren fĂŒr Zeichen

Der Operator sizeof

Obiges Programm verwendet den Operator sizeof in zwei verschiedenen Versionen:

1. Anwendung auf ein Zeichen:

In Zeile 19 (im Schleifendurchlauf) wird fĂŒr jedes aktuelle Zeichen c

sizeof c

ausgegeben. Hier gibt sizeof c an, wieviel Speicherplatz (in Byte) das Zeichen c belegt.

2. Anwendung auf einen Datentyp:

Nach dem Schleifendurchlauf (Zeile 22) wird dagegen

sizeof (char)

ausgegeben. Man beachte, dass char ein SchlĂŒsselwort ist und den Datentyp char beschreibt. Hier wird also ausgegeben, wieviel Speicherplatz fĂŒr eine Variable des Datentyps char vorgesehen ist.

Der Operator sizeof ist natĂŒrlich nicht nur fĂŒr Zeichen definiert, er kann auf alle elementaren Datentypen angewendet werden. Und es gibt ihn jeweils in den beiden oben besprochenen Varianten.

Aufgaben:

1. FĂŒhren Sie das Programm FundTyp.cpp aus! Dokumentieren Sie

int min static_cast<int>(numeric_limits<char>::min());
int max = static_cast<int>(numeric_limits<char>::max());
cout << "sizeof (char) = " << sizeof (char) << endl;

2. Was passiert, wenn man im Programm FundTyp.cpp die Schleife ĂŒber das Maximum

int max = static_cast<int>(numeric_limits<char>::max());

hinaus weiterlaufen lÀsst?

Tip:

Egal mit welchem fundamentalen Datentyp Sie arbeiten, achten Sie immer darauf, dass Sie den erlaubten Bereich nicht verlassen. Man sollte nicht versuchen, Regeln aufzustellen, wie sich das Programm jenseits des Wertebereiches eines fundamentalen Datentyps verhĂ€lt. (Manchmal kann man tatsĂ€chlich mit Hilfe des modulo-Operators weiterrechnen — dies ist aber sehr fehleranfĂ€llig und nicht fĂŒr jeden Datentyp garantiert.)

Vergleichsoperatoren

Auf der Menge der Zeichen sind alle Vergleichsoperatoren definiert, die es spĂ€ter auch fĂŒr Zahlen geben wird — es ist klar, dass diese Operatoren hier greifen, da die Zeichen intern wie Zahlen behandelt werden. Die möglichen Vergleichsoperatoren sind aus folgendem Programm-Schnipsel zu entnehmen:

char a = 'a';       // Nummer 97 im ASCII-Code
char z = 'z';       // Nummer 122 im ASCII-Code

a == z;      // "a gleich z": false; Vergleichs-Operator: nicht verwechseln mit dem Zuweisungsoperator =
a != z;      // "a ungleich z": true;
a < z;       // " a kleiner als z": true
a > z;       // " a grĂ¶ĂŸer als z": false
a <= z;      // " a kleiner oder gleich z": true
a >= z;      // " a grĂ¶ĂŸer oder gleich z": false

Liest man diese Liste, stellt sich sofort die Frage: was passiert bei der AusfĂŒhrung von Zeile 4:

a == z;

Es wird geprĂŒft, ob die Zeichen identisch sind (hier nicht) und dann wird das Ergebnis als Boolescher Wert (hier false) berechnet. Da der berechnete Wert hier nicht weiterverwendet wird, verschwindet er in irgendeinem Register. (Wie man mit Booleschen Variablen arbeitet, wird im nĂ€chsten Abschnitt erklĂ€rt.) Es wĂ€re also besser gewesen zu schreiben:

cout << (a == z);

Compiliert man obiges Programm-Schnipsel (ohne die Ausgabe mit cout), erhÀlt man eine Warnung vom Compiler:

warning: statement has no effect

Auf zwei hÀufige Fehler im Zusammenhang mit Vergleichsoperatoren muss hingewiesen werden:

Warnung:

1. Anstelle des Vergleichs-Operators == wird oft der Zuweisungsoperator = eingesetzt. Dies fĂŒhrt nicht zu einem Compiler-Fehler, aber zu merkwĂŒrdigem Verhalten des Programmes. Überlegen Sie sich daher bei jedem gleich, das Sie einsetzen wollen: Benötigt man hier den Vergleichs-Operator == oder den Zuweisungsoperator = ?

2. Bei der Verwendung der Operatoren >= , <= und != sind FĂ€lle denkbar, in denen eine Verwechslung der Reihenfolge zu keinem Compiler-Fehler fĂŒhrt, aber zu einer unerwĂŒnschten Berechnung.

In der Mathematik hat es keine Vorteile, ob man eine Aussage mit dem Operator < oder mit <= formuliert — Hauptsache die Aussage ist richtig formuliert. FĂŒr die Programm-AusfĂŒhrung besteht aber der Operator <= aus zwei Vergleichen (< und == ), die nacheinander berechnet werden.

Perfomance-Tip:

Versuchen Sie — vor allem in Schleifen, die oft durchlaufen werden — die Verwendung von <= und >= zu vermeiden. Das Problem lĂ€sst sich meist umformulieren, so dass < beziehungsweise > verwendet werden kann.

Aufgabe:

ÜberprĂŒfen Sie, ob in dem Programm-Schnipsel mit den Vergleichsoperatoren tatsĂ€chlich die richtigen Werte angegeben sind. Beachten Sie dabei, dass anstelle von true immer 1 und anstelle von false immer 0 ausgegeben wird.

Boolesche Variable

Initialisierung

Eine Boolesche Variable ist vom Datentyp bool und kann entweder mit den SchlĂŒsselworten true beziehungsweise false initialisiert werden oder mit den Zahlenwerten 1 beziehungsweise 0. Bei der Ausgabe einer Booleschen Variable erscheinen die Zahlen 1 beziehungsweise 0.

bool b = true;
cout << b << endl;      // b = 1
bool a = 0;
cout << a << endl;      // a = 0

Erstaunlich ist vielleicht, welchen Speicherplatz eine Boolesche Variable einnimmt:

out << "Size: " << sizeof(bool) << endl;       // Size: 1

Obwohl nur ein Bit benötigt wird, wird dennoch ein Byte reserviert. Aber da man einzelne Bits im Hauptspeicher nicht ansprechen kann, sondern immer nur Bytes, ist diese Konvention verstÀndlich.

Damit kann auch das merkwĂŒrdige Verhalten der folgenden Zeilen erklĂ€rt werden:

bool b = 10;
cout << "b = " << b << endl;        // b = 1

FĂŒr eine Boolesche Variable wird 1 Byte Speicherplatz reserviert, aber nur wenn der Inhalt des Bytes gleich null ist (also alle Bits sind null), ist die Boolesche Variable gleich false, andernfalls ist sie true.

Operatoren fĂŒr Boolesche Variablen

FĂŒr Boolesche Variablen sind die grundlegenden logischen Operatoren definiert, sie können entweder mit SchlĂŒsselworten (not, and, or) angesprochen werden oder mit Symbolen (! , && , || ); das Ergebnis ist wieder eine Boolesche Variable:

bool a = false;
bool b = true;
bool c;

// NOT
c = not a;          // c = 1
c = !a;             // c = 0

// AND
c = a and b;        // c = 0
c = a && b;         // c = 0

// OR
c = a or b;         // c = 1
c = a || b;         // c = 1

Verwendung Boolescher Variabler

Im Programm oben wurden die drei Booleschen Variablen a, b, c deklariert und anschließend wurden die logischen Operationen ausgefĂŒhrt. Oftmals ist es gar nicht nötig, die Booleschen Variablen zu deklarieren, da ein Vergleichsoperator (wie grĂ¶ĂŸer als, kleiner gleich und so weiter) als Ergebnis einen Wahrheitswert (true oder false — natĂŒrlich als 1 oder 0) liefert.

Das folgende Programm (genauer Programm-Schnipsel — Sie wissen inzwischen, wie man es zu einem lauffĂ€higen Programm ausbaut) zeigt den Einsatz einer Booleschen Variable, die prĂŒfen soll, ob ein gegebenes Zeichen eine Ziffer ist; man benötigt dazu den Code der Ziffern im ASCII-Code.

bool isNumber;      // Ziffern im ASCII-Code: 48 - 57
char c = 52;
isNumber = (c > 47) && (c < 58);
cout << "Ziffer? " << isNumber << endl;     // Ziffer? 1

Es hat sich bewĂ€hrt, fĂŒr Boolesche Variablen Namen zu vergeben, die fĂŒr sich sprechen und mit is oder has beginnen (wie hier isNumber oder vielleicht hasSubdirectory).

Man beachte die Zeile 3:

isNumber = (c > 47) && (c < 58);

Man kann auf der rechten Seite Vergleichsoperatoren und logische Operatoren beliebig kombinieren. Die Klammersetzung sollte stets dafĂŒr sorgen, dass die Terme ĂŒberschaubar bleiben. Und es ist wohl klar, dass sich hier sehr leicht Denkfehler einschleichen können. Durch den zusĂ€tzlichen Einsatz der BedingungsprĂŒfung (mit if) und der Alternative (mit if else) kann man den Ablauf von Programmen steuern. Dies wird spĂ€ter diskutiert im Kapitel Elementare Syntax von C++: Bedingung, Alternative und Mehrfachalternative.

Logisches Und, logisches Oder: short-circuit evaluation

Der Operator && besitzt folgende Eigenschaft: ist in

c = a && b;

a = 0, so ist auch c = 0, egal welchen Wert b besitzt. In C++ wird deswegen im Fall von a = 0 der Ausdruck

a && b

nicht vollstÀndig ausgewertet, sondern es wird sofort c = 0 gesetzt.

Man nennt dieses Verfahren short-circuit evaluation.

Es wird auch beim logischen Oder || angewendet:

c = a || b;

FĂŒr a = 1 muss c = 1 sein (egal welchen Wert b hat) — auch hier wird sofort c = 1 gesetzt.

Ganzzahlige Datentypen (echte Zahlen)

Eigenschaften

Es wurde bereits gezeigt, welche Vielzahl von Datentypen in C++ fĂŒr ganze Zahlen existieren, am hĂ€ufigsten verwendet werden short int, int und long int.

Aufgabe:

Suchen Sie in der C++-Dokumentation nach

std::numeric_limits

und versuchen Sie Àhnlich wie oben im Programm

FundTyp.cpp
#include <limits>
. . .

möglichst viele Eigenschaften der drei ganzzahligen Datentypen short int, int und long int herauszubekommen.

Dokumentieren Sie die Ergebnisse — Sie werden noch öfter darauf zugreifen mĂŒssen.

Deklaration und Initialisierung, Initialisierungsliste

Der Unterschied zwischen Deklaration und Initialisierung einer Variablen wurde bereits bei den Konsolen-Eingaben am Programm CinCout.cpp erklĂ€rt (siehe Elementare Syntax: Ein- und Ausgaben ĂŒber die Konsole). Die Deklaration ist nur die AnkĂŒndigung der Variablen, damit fĂŒr sie — entsprechend ihrem Datentyp — der geeignete Speicherplatz reserviert werden kann. Erst mit der Initialisierung wird der Variable ein Wert zugewiesen (im Speicherplatz abgelegt); ĂŒber den Namen der Variable kann der Wert jederzeit aus dem Speicher geladen werden.

short a;                    // Deklaration der short-Variable a
cout << "a = " << a << endl;        // a unbestimmt
a = 17;                     // Initialisierung der Variable a mit dem Wert 17
short b = 18;               // Deklaration und Initialisierung

Wenn möglich, sollen Variablen sofort initialisiert werden, da nicht initialisierte Variable zu sehr merkwĂŒrdigem Verhalten fĂŒhren können. Meist ist nach der Deklaration der Wert der Variable unbestimmt (indeterminate). Das heißt: Wird der Wert der Variable abgefragt, wird einfach derjenige Wert genommen, der im Augenblick der Deklaration (zufĂ€llig) im Speicher gestanden ist.

Eine generelle Regel, wie sich eine nicht-initialisierte Variable verhÀlt, kann man nicht aufstellen: Das Verhalten ist vom Datentyp abhÀngig und davon, ob es eine lokale oder eine globale Variable ist (dieser Unterschied wird spÀter noch erklÀrt). Daher ist eine verstreute Deklaration und Initialisierung (wie in Zeile 1 und 3 oben) nicht zu empfehlen. Besser ist die gleichzeitige Deklaration und Initialisierung (Zeile 4 oben).

Tip:

Es ist ratsam, Variable sofort zu initialisieren. Sollte es einmal nicht geschehen und der Grund dafĂŒr ist nicht offensichtlich, sollte der Grund in einem Kommentar vermerkt werden.

Ein weiterer Fehler, der bei der Initialisierung auftreten kann, ist dass der Variable ein Wert zugewiesen wird, der nicht im erlaubten Bereich des Datentyps liegt.

short a = 5;
short b = 100000;           // kein Compiler-Fehler, obwohl b außerhalb des erlaubten Bereichs fĂŒr short
cout << "a + b = " << a + b << endl;        // ein "Ergebnis" wird berechnet, es ist aber nicht 100005

Sind fĂŒr den Datentyp short 2 Byte vorgesehen, liegt der Wert von b außerhalb des erlaubten Bereichs (Zeile 2). Der Compiler liefert in diesem Fall nur eine Warnung (die man leicht ĂŒbersehen kann), aber das Programm kann dennoch ausgefĂŒhrt werden. Und es liefert ein nicht zu erwartendes Ergebnis.

Um diesen Fehler zu vermeiden, wurde eine weitere Möglichkeit der Initialisierung eingefĂŒhrt (mit Hilfe von geschweiften Klammern):

short a {5};
short b {100000};       // Compiler-Fehler: 
                        // schon beim Compilieren wird ĂŒberprĂŒft, ob der Wert von b einem erlaubten short-Wert entspricht
cout << "a + b = " << a + b << endl;

Jetzt fĂŒhrt die Initialisierung in Zeile 2 zu einem Compiler-Fehler; das Programm kann nicht ausgefĂŒhrt werden.

Diese neue Form der Initialisierung wurde mit dem Standard C++11 eingefĂŒhrt und wird als Initialisierungsliste (List Initialization) bezeichnet. Der Name rĂŒhrt daher, dass zuvor schon Listen initialisiert wurden, indem die Werte in geschweiften Klammern und durch Kommas getrennt angegeben wurden; eine Variable kann eben auch als eine Liste mit nur einem Eintrag aufgefasst werden.

Möglich ist auch die Schreibweise mit Gleichheitszeichen und geschweiften Klammern:

short a = {5};
short b = {100000};
cout << "a + b = " << a + b << endl;

Die Initialisierungsliste hat einen weiteren Vorteil: im folgenden Programm-Schnipsel wird eine short-Variable mit einer Gleitkommazahl initialisiert.

short pi = 3.14;                    // keine Compiler-Warnung und kein Compiler-Fehler
cout << "pi = " << pi << endl;      // pi = 3

Da pi als short definiert ist, wird sie sofort in die Zahl 3 konvertiert. Der Compiler zeigt dabei weder einen Fehler noch eine Warnung.

Dagegen fĂŒhrt die Verwendung der Initialisierungsliste hier zu einem Compiler-Fehler:

short pi {3.14};            // Compiler-Fehler
cout << "pi = " << pi << endl;

Tip:

Auch wenn die Symbolik nicht so suggestiv ist wie bei der Verwendung des Zuweisungsoperators, verwenden Sie fĂŒr die Initialisierung stets die Initialisierungsliste.

Arithmetische Operationen

Das folgende Programm-Schnipsel zeigt, welche arithmetischen Operationen fĂŒr ganze Zahlen definiert sind:

int a = 20;
int b = 3;
int c;

c = a + b;      // Addition
c = a - b;      // Subtraktion
c = a * b;      // Multiplikation
c = a / b;      // Division (mit Rest); 20:3 = 6 Rest 2; c = ganzzahliger Anteil; c = 6
c = a % b;      // modulo (Rest); c = 2

Besonders fehleranfÀllig ist der Einsatz der Division, da die beiden Operatoren leicht verwechselt werden können. Und wenn Gleitkommazahlen eingesetzt werden, ist die Division nicht mehr als die Division mit Rest definiert.

Aufgaben:

1. Untersuchen Sie das Verhalten bei einer Division durch 0:

  • Gibt es einen Compiler-Fehler?
  • Oder gibt es einen Laufzeit-Fehler? (Das heißt das Programm compiliert, aber erst bei der AusfĂŒhrung tritt ein Fehler auf. Wenn ja: Welcher?)

2. Untersuchen Sie die Eigenschaften der Division mit Rest:

  • Wie sind ganzzahliger Anteil und Rest definiert, wenn a > 0 und b < 0 (beziehungsweise umgekehrt)?
  • Wie sind ganzzahliger Anteil und Rest definiert, wenn a < 0 und b < 0?

Zu den arithmetischen Operationen zÀhlen noch die sogenannten Bit-Operationen, die hier noch nicht besprochen werden. Darunter fallen:

  • bitweise Negation
  • bitweises Rechtssschieben (right-shift)
  • bitweises Linksschieben (left-shift)
  • bitweises UND
  • bitweises ODER
  • bitweises XOR.

Vergleichsoperatoren

Auf ganzzahligen Datentypen sind die Vergleichsoperatoren definiert, die schon bei den Zeichen eingefĂŒhrt wurden. Sie werden hier entsprechend eingesetzt und liefern wieder einen Wahrheitswert (true oder false, also 1 oder 0). Hier folgt ein Beispiel mit dem Datentyp short:

short a {5};       
short b {17};  

a == b;      // "a gleich b": false; Vergleichs-Operator: nicht verwechseln mit dem Zuweisungsoperator =
a != b;      // "a ungleich b": true;
a < b;       // " a kleiner als b": true
a > b;       // " a grĂ¶ĂŸer als b": false
a <= b;      // " a kleiner oder gleich b": true
a >= b;      // " a grĂ¶ĂŸer oder gleich b": false

Warnung:

Der Vergleichs-Operator == vergleicht bei fundamentalen Datentypen tatsÀchlich, ob die Speicherzellen gleichen Inhalt haben. Bei selbstdefinierten Datentypen wird dies nicht mehr richtig sein, was eine hÀufige Fehlerquelle ist.

Das Verhalten des Vergleichs-Operators == kann man an folgenden Beispielen untersuchen:

short a {5};
short b = a;
cout << "b == a: " << (a == b) << endl;     // true: klar, da b = a gesetzt wurde

b = 5;
cout << "b == a: " << (a == b) << endl;     // true: auch klar, da a = 5

int c = 5;
cout << "b == c: " << (c == b) << endl;     // true: obwohl b und c von unterschiedlichem Datentyp

b = 100000;         // Compiler-Warnung, da b vom Datentyp short
c = 100000;         // bei int ok!
cout << "b == c: " << (c == b) << endl;     // false: b besitzt einen unbestimmten Wert, der nicht mit 100000 ĂŒbereinstimmen kann 
                                                    //denn: 100000 ist außerhalb des erlaubten Wertebereiches
                                                                    
char x = 5;                 // Vorsicht: es heißt NICHT: char x = '5';
cout << "x == a: " << (a == x) << endl;     //true: obwohl unterschiedlicher Datentyp

Typumwandlungen

In den letzten Abschnitten wurden zahlreiche Datentypen besprochen; da jeder einen bestimmten Speicherplatz beansprucht, kann es zu Problemen fĂŒhren, wenn man einen Datentyp in einen anderen verwandeln möchte.

Man muss zwei Arten von Typumwandlungen (cast) unterscheiden:

  1. Explizite Typumwandlung
  2. Implizite Typumwandlung.

Bei der expliziten Typumwandlung wird ein Operator angegeben, der die Typumwandlung ausdrĂŒcklich anzeigt. Dagegen wird bei der impliziten Typumwandlung einfach ein eigentlich unpassender Datentyp eingesetzt ohne ausdrĂŒckliche Angabe eines Operators (eigentlich unpassend soll hier heißen, dass laut Sprachregeln ein anderer Datentyp erwartet wurde) — die Umwandlung wird vom Compiler vorgenommen.

Geschehen die impliziten Typumwandlungen nur zwischen elementaren Datentypen, werden sie als Standard-Konversionen bezeichnet; spÀter wird es auch Typumwandlungen zwischen selbstdefinierten Datentypen geben.

Fehlerquelle:

Manche Typumwandlungen erfolgen fehlerfrei, bei manchen Typumwandlungen wird der Wert der GrĂ¶ĂŸe, die umgewandelt werden soll, verĂ€ndert. Man muss mit den Eigenschaften der fundamentalen Datentypen und der Typumwandlungen gut vertraut sein, um stets zu wissen, welcher Fall vorliegt und ob man den möglichen Fehler in Kauf nehmen möchte (oder einen Sicherungsmechanismus in das Programm einbaut).

Die explizite Typumwandlung sollte mit dem Operator

t1 = static_cast<Typ1> (Typ2 t2)

erfolgen. Die Schreibweise soll andeuten, dass eine Variable t2 (oder ein Wert) vom Datentyp Typ2 gegeben ist und dass der Wert von t2 der Variable t1 (vom Datentyp Typ1) zugewiesen werden soll; wĂŒnschenswert wĂ€re dabei, dass die Werte von t1 und t2 identisch sind und dass sich nur der Datentyp verĂ€ndert.

Als Beispiel diene nachfolgendes Schnipsel, das zugleich den hÀufigsten Fehler bei Typumwandlungen zeigt:

int n {100000};                         // 100000 ist zu groß fĂŒr short
short s = static_cast<short>(n);        // weder Compiler-Fehler noch Compiler-Warnung 
cout << "s = " << s << endl;            // s unbestimmt

Der Speicherplatz einer short-Variable reicht nicht aus, um die Zahl 100000 aufzunehmen, daher ist der Wert von s unbestimmt.

Man kann obige Typumwandlung auch folgendermaßen durchfĂŒhren — diese Schreibweise sollte aber nicht mehr verwendet werden, da sie bei verschachtelten AusdrĂŒcken leicht zu ĂŒbersehen ist:

short s = (int) n;                  // weder Compiler-Fehler noch Compiler-Warnung 
cout << "s = " << s << endl;        // s unbestimmt

Es ist auch möglich, die Typumwandlung ohne static_cast -Operator auszufĂŒhren, wie folgendes Beispiel zeigt:

int n {100000};         // 100000 ist zu groß fĂŒr short
short s {n};            // Compiler-Warnung: warning narrowing conversion of 'n' from 'int' to 'short int'
cout << "s = " << s << endl;        // s unbestimmt

Die Verwendung der Initialisierungsliste zur Initialisierung von s sorgt fĂŒr eine Compiler-Warnung; gleichwertig — bis auf die Compiler-Warnung — ist auch:

int n {100000};          // 100000 ist zu groß fĂŒr short
short s = n;             // keine Compiler-Warnung
cout << "s = " << s << endl;         // s unbestimmt

Die Umwandlung in den short-Wert erfolgt in beiden FĂ€llen automatisch, also ohne den Aufruf des static_cast -Operators. Man nennt dies eine implizite Typumwandlung.

Die implizite Typumwandlung birgt daher dieselbe Fehlerquelle wie die explizite Typumwandlung: man kann unbestimmte Werte erzeugen. Der Vorteil des static_cast -Operators liegt lediglich darin, dass die Typumwandlung deutlich im Quelltext erkennbar ist.

Es stellt sich natĂŒrlich die Frage: wann liefert eine implizite Typumwandlung keinen Fehler?

Es gibt zwei FĂ€lle:

1. Bei einer sogenannten Aufweitung wird der gegebene Wert in einem Datentyp mit grĂ¶ĂŸerem Speicherplatz abgelegt — hierbei kann kein Fehler auftreten; das folgende Beispiel zeigt dies:

short n {5};
int m = n;      // m == 5, ist jetzt aber vom Datentyp int

FĂŒr jeden gĂŒltigen short-Wert wird automatisch ein gĂŒltiger int-Wert erzeugt.

2. Manchmal bei einer Eingrenzung (narrowing conversion), nÀmlich nur in dem Fall, wenn die umzuwandelnde Zahl in den engeren Wertebereich passt:

int i {1000};
short s {i};        // s == 1000

Hier fĂŒhrt die implizite Typumwandlung zu keinem Fehler. Das Problem ist natĂŒrlich, dass mit Variablen gearbeitet wird, deren Werte sich bei der Programm-AusfĂŒhrung verĂ€ndern und die beim Programmieren noch nicht bekannt sind.

Abbildung 1: Typumwandlungen zwischen ganzzahligen Datentypen.Abbildung 1: Typumwandlungen zwischen ganzzahligen Datentypen.

In Abbildung 1 sind die Typumwandlungen zwischen ganzzahligen Datentypen graphisch dargestellt:

  • Bei einer Aufweitung (promotion, grĂŒn) wird der Zahlenwert unverĂ€ndert ĂŒbernommen, aber der Datentyp wird verĂ€ndert; man muss keinen ausdrĂŒcklichen Operator angeben (nur zur besseren Lesbarkeit des Quelltextes) und es kann kein Fehler entstehen.
  • Bei anderen Standard-Konversionen (dunkelrot) kann es dagegen zu einem unbestimmten Zahlenwert kommen; dies kann auch nicht durch den Einsatz eines cast-Operators vermieden werden.

Die unterschiedliche GrĂ¶ĂŸe der dargestellten Datentypen soll die GrĂ¶ĂŸe der entsprechenden Speicherzellen andeuten; der Versatz soll anzeigen, dass bei mit Vorzeichen behafteten Datentypen (signed) der Wertebereich (nahezu) symmetrisch um die Null liegt; bei den anderen Datentypen (unsigned) ist stets null der minimale Wert.

Aufgaben:

1. Untersuchen Sie die Umwandlungen zwischen

  • char und short
  • bool und short.

Bei welchen impliziten Typumwandlungen können Fehler auftreten, bei welchen nicht?

2. Welchen Nutzen hat folgender Code:

int n = 1;
// do something with n
cout << ( n == static_cast<int> ( static_cast<short> (n) ) ) << endl;

Gleitkommazahlen

In C++ gibt es drei Arten von Gleitkommazahlen (floating point numbers): float, double und long double; die Bezeichnung float erklĂ€rt sich von alleine, double soll andeuten, dass die Genauigkeit gegebenĂŒber float meist verdoppelt ist (double precision) und long double ist wie bei den ganzzahligen Datentypen zu verstehen (die Genauigkeit ist nochmals erhöht). Die exakte GrĂ¶ĂŸe der entsprechenden Speicherzellen ist in der Spezifikation wieder nicht vorgegeben und plattformabhĂ€ngig. Allerdings ist der Zahlenbereich von double stets so groß, dass man long double nur in exotischen Anwendungen benötigt.

Initialisierung

Was oben ĂŒber die Initialisierungslisten gesagt wurde, gilt natĂŒrlich auch fĂŒr die Gleitkommazahlen (und alle anderen elementaren Datentypen).

Wie bereits besprochen, werden Gleitkommazahlen intern durch

  • Vorzeichen der Mantisse,
  • Mantisse,
  • Vorzeichen des Exponenten und
  • Exponent

dargestellt. Dies heißt aber nicht, dass sie nur in dieser Form initialisiert werden mĂŒssen, möglich sind auch folgende Schreibweisen:

float a {1};
float b {1.0};
float c {1.444449};
float d {-1.0e12};
float e {1.444449e-12};
float f {1.444449E-12};

cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
cout << e << endl;
cout << e * e << endl;
cout << e * e * e * e << endl;
cout << f << endl;

Die Ausgaben lauten:

1
1
1.44445             // c != 1.444449
-1e+012             // d == - 1 * 10^12
1.44445e-012        // e != 1.444449e-12, wieder der Rundungsfehler
2.08643e-024        // e^2: der Fehler pflanzt sich fort
0                   // e^4: was ist hier passiert?
1.44445e-012

Zeile 1: a wird wie eine ganze Zahl eingegeben, intern aber wie eine Gleitkommazahl (hier float) behandelt.

Zeile 2: b wird ausdrĂŒcklich mit einer Nachkommastelle angegeben, obwohl b == 1 ; diese Angabe ist zwar ĂŒberflĂŒssig, aber empfehlenswert, da der Leser des Programmes sofort sieht, dass es sich um eine Gleitkommazahl handelt. Das Komma muss als Dezimalpunkt geschrieben werden.

Zeile 3: Endlich eine echte Gleitkommazahl.

Wird die Zahl spÀter ausgegeben, wird sie verÀndert: Anstelle von 1.444449 erscheint 1.44445.

Dies liegt daran, dass standardmĂ€ĂŸig die Ausgabe mit 5 Nachkommastellen erfolgt; setzt man diese Genauigkeit hoch, wird die eingegebene Zahl auch richtig ausgegeben.

Aber dennoch hier wird ein Problem sichtbar: Irgendwann ist die Anzahl der Nachkommastellen so groß, dass eine Zahl nicht in der gewĂŒnschten Genauigkeit abgespeichert werden kann. Die Mantisse muss zum Abspeichern verĂ€ndert werden. Da dies in den Dualzahlen passiert, ist es im Allgemeinen schwer vorherzusagen, wie groß der Genauigkeitsverlust ist. (Mehr dazu spĂ€ter unter den Typumwandlungen).

Zeile 4 und 5: Gleitkommazahlen können wie im Taschenrechner in der Zehnerpotenzschreibweise eingegeben werden (dazu wird das kleine e verwendet).

Zeile 6: Anstelle des kleinen e kann auch das große E verwendet werden (bei der Ausgabe erscheint immer e).

Zeile 13: Bei der Berechnung von e2 pflanzt sich der Genauigkeitsverlust weiter fort.

Zeile 14: Das Ergebnis ist eigentlich: e4 ≈ 4.3532 * 10-48. Aber jetzt ist der Exponent so klein, dass er im Datentyp float nicht mehr dargestellt werden kann, daher wird e4 zu 0 abgerundet.

Aufgabe:

Suchen Sie in der C++-Dokumentation nach

std::numeric_limits

und versuchen Sie Àhnlich wie oben im Programm

FundTyp.cpp
#include <limits>
. . .

möglichst viele Eigenschaften der Gleitkommazahl-Datentypen float und double herauszubekommen.

Dokumentieren Sie die Ergebnisse — Sie werden noch öfter darauf zugreifen mĂŒssen.

Typumwandlungen

Aufweitung und Eingrenzung

Was oben bei den Typumwandlungen zwischen ganzzahligen Datentypen gesagt wurde, bleibt fĂŒr Gleitkommazahlen gĂŒltig:

  • Eine Aufweitung (promotion) erfolgt fehlerfrei, nur der Datentyp wird geĂ€ndert; die Typumwandlung kann als implizite Konversion vorgenommen werden (es ist kein static_cast -Operator nötig).
  • Eine Eingrenzung (narrowing conversion) dagegen kann zu einem unbestimmten Zahlenwert fĂŒhren; dies kann auch der static_cast -Operator nicht vermeiden. Man muss hier die Grenzen der Zahlenbereiche kennen, um keinen Fehler zu machen.

Beispiele:

// Aufweitung:
float f {1.5e20};

double d = static_cast<double> (f);     // mit static_cast
cout << "d = " << d <<  endl;           // 1.5e+020

d = f;                                  // ohne static_cast
cout << "d = " << d <<  endl;           // 1.5e+020

// Eingrenzung:
d = 1.5e200;

f = static_cast<float> (d);         // mit static_cast
cout << "f = " << f <<  endl;       // 1.#INF

f = d;                              // ohne static_cast
cout << "f = " << f <<  endl;       // 1.#INF

Der Überlauf, nĂ€mlich dass eine zu große Zahl im Datentyp float abgespeichert wird (Zeile 13 und 16), wird angezeigt durch:

1.#INF

Genauigkeitsverlust

Eine weitere Fehlerquelle ist der sogenannte Genauigkeitsverlust. Im folgendem Beispiel wird ein Bruch 2/3 berechnet, aber das Ergebnis einmal als float und einmal als double abgespeichert (Zeile 4 und 5). Da beide Datentypen eine unterschiedliche LĂ€nge der Mantisse besitzen, wird die Zahl 2/3 mit anderer Genauigkeit gespeichert. Die Folge ist, dass die Differenz 2/3 - 2/3 ≠ 0 (Zeile 7):

float z {2};         // ZĂ€hler
float n {3};         // Nenner

float b1 = z / n;
double b2 = z / n;

double diff = b2 - b1;
cout << "diff = " << diff << endl;          // diff = -1.98682e-008

Dieser Genauigkeitsverlust scheint harmlos zu sein: am obigen Beispiel sieht man, dass die Differenz sehr klein ist im Vergleich zu 2/3. Bedenkt man aber, dass in der Mathematik zahlreiche Kriterien formuliert werden mit Aussagen wie:

Falls eine GrĂ¶ĂŸe D = 0 ist, gilt ... , falls die GrĂ¶ĂŸe D ≠ 0, gilt ...

Wie kann man ein derartiges Kriterium in ein Programm umsetzen?

Aufgaben:

1. Wo in der Mathematik werden oben genannte Kriterien verwendet?

Nennen Sie einige Beispiele!

Welche praktische Bedeutung haben sie?

2. Warum tritt der Effekt des Genauigkeitsverlust im Beispiel oben nicht auf, wenn man 1/2 - 1/2 berechnet?

Der oben beschriebene Genauigkeitsverlust tritt natĂŒrlich bei jeder Eingrenzung auf, wenn im engeren Datentyp nicht genĂŒgend Stellen fĂŒr die Nachkommastellen zur VerfĂŒgung stehen, siehe folgendes Beispiel:

// Genauigkeitsverlust bei Eingrenzung
double x {1.444444449};
float y {x};

// Ausgabe ohne setprecision, default-Wert ist 5:
cout << "double: x = " << x <<  endl;
cout << "float: y = " << y <<   endl;
cout << "double mit setprecision(10): x = " << setprecision(10) << x << endl;
cout << "float mit setprecision(10): y = " << y <<  endl;
double: x = 1.44444
float: y = 1.44444
double mit setprecision(10): x = 1.444444449
float mit setprecision(10): y = 1.444444418

Umgekehrt kann daher auch die Anzahl der Nachkommastellen ein Grund fĂŒr die Verwendung eines bestimmten Datentyps sein (und nicht nur der grĂ¶ĂŸere Wertebereich).

Aufgabe:

Um die am hĂ€ufigsten benötigten Gleitkomma-Zahlenbereiche float und double richtig einzusetzen, benötigt man die Wertebereiche (wurden in einer Aufgabe oben bereits bestimmt) und die Anzahl der Nachkommastellen, die richtig angegeben werden. Dokumentieren Sie beide GrĂ¶ĂŸen.

Informationsverlust

Ein Àhnlicher Effekt wie der Genauigkeitsverlust ist der Informationsverlust: Er betrifft die Umwandlung einer Gleitkommazahl in einen ganzzahligen Datentyp, wie im folgenden Beispiel:

double x = 1.5;
short n = static_cast<short> (x);       // n == 1

Da man in short keine Nachkommastellen abspeichern kann, werden sie einfach abgeschnitten (und damit 1.5 in 1 verwandelt). Der static_cast -Operator kann den Informationsverlust nicht vermeiden (bei einer impliziten Konversion wÀre der Fehler ebenso aufgetreten).

static_cast

Ganzzahlige Division und

Bei den bisherigen Typumwandlungen war der static_cast -Operator nicht nötig (er sorgt lediglich fĂŒr mehr Klarheit im Quelltext), man kann alle Typumwandlungen auch mit impliziten Konversionen durchfĂŒhren. Das folgende Beispiel zeigt, dass es tatsĂ€chlich FĂ€lle gibt, bei denen man auf den static_cast -Operator nicht verzichten darf:

short z {3};            // ZĂ€hler
short n {4};            // Nenner
float b1 =  static_cast<float>(z) / n;          // b1 == 0.75
float b2 = z / n;                               // b2 == 0 (ganzzahlige Division 3:4 = 0 Rest 3)
float b3 = static_cast<float>( z / n);          // b3 == 0; Der cast kommt zu spÀt!

ZĂ€hler und Nenner eines Bruches sind als short gegeben. Werden sie dividiert wie in Zeile 4, wird die Division als ganzzahlige Division durchgefĂŒhrt und liefert das Ergebnis 0. Dass das Ergebnis als float deklariert wurde, verhindert den Fehler nicht.

Es nĂŒtzt auch nichts, das Ergebnis der Division mit dem static_cast -Operator in float zu verwandeln (Zeile 5): der cast kommt zu spĂ€t!

Richtig ist, den ZĂ€hler (oder den Nenner) in float umzuwandeln und dann erst zu dividieren (Zeile 3). Hier kann man auf den static_cast -Operator nicht verzichten.

Mathematische Funktionen

Die Standard-Bibliothek bietet eine Vielzahl von mathematischen Funktionen — die Dokumentation sollten Sie bei Bedarf konsultieren (nur die wenigsten davon werden Sie selber implementieren können).

C++ reference -> Numerics library -> Common mathematical functions

Die wichtigsten Funktionen sind

  • Wurzelfunktion
  • trigonometrische Funktionen
  • Exponential- und Logarithmusfunktion
  • Betragsfunktion
  • mehrere Varianten der Division
  • Funktionen zum Runden von Zahlen nach unterschiedlichen Rundungs-Strategien.

FĂŒr die meisten dieser Funktionen muss cmath inkludiert werden (genaueres entnehmen Sie der Dokumentation):

#include <cmath>

Aufgabe:

Berechnen Sie fĂŒr x = 0, x = ± 0.5 und x = ± 1 folgende Funktionen:

1/x, 1/x2, √x, √(1 - x2), sin x, cos x, tan x, arcsin x, arccos x, exp(x), exp(- x2), ln x, log x.

(die letzten beiden Funktionen sind der natĂŒrliche Logarithmus und der Logarithmus zur Basis 10).

Bei welchen Funktionen liegen die gegebenen x-Werte nicht im Definitionsbereich? Wie reagiert die Programm-AusfĂŒhrung darauf?

Besonderheiten

Konstanten: Das SchlĂŒsselwort const

Es gibt die Möglichkeit, GrĂ¶ĂŸen, deren Wert nicht verĂ€nderlich sein soll, durch das SchlĂŒsselwort const zu kennzeichnen. Der Compiler erkennt, ob eine Anweisung versucht, eine als Konstante deklarierte Variable zu verĂ€ndern und liefert eine Fehlermeldung, siehe folgendes Programm-Schnipsel:

const float PI = 3.14;
// Bessere NĂ€herung:
PI = PI + 0.00159;              // Compiler-Fehler: assignment of read-only variable 'PI'
const float SPEED_OF_LIGHT = 3.0e8;       // Lichtgeschwindigkeit in m/s

Konstanten können zusĂ€tzlich leichter erkennbar gemacht werden, indem sie nur mit Großbuchstaben geschrieben werden. Da dann aber die CamelCase-Schreibweise nicht mehr anwendbar ist, werden Unterstriche eingesetzt (wie in SPEED_OF_LIGHT ).

Hexadezimal-, Oktal- und Dualzahlen und weitere Formatierungshilfen

Zahlen (von beliebigem Datentyp) können auch als Hexadezimal-, Oktal- oder Dualzahlen eingegeben werden; kenntlich gemacht werden sie durch ein fĂŒhrendes 0x oder 0X (fĂŒr Hexadezimalzahlen), durch ein fĂŒhrendes 0 (fĂŒr Oktalzahlen) beziehungsweise ein fĂŒhrendes 0b oder 0B (Dualzahlen):

// Hexadezimalzahlen
int h {0xFFFF};         // 16^4 -1 = 65 535
cout << "h = " << h << endl;

// Oktalzahlen
int o {0777};           // 8^3 - 1 = 511
cout << "o = " << o << endl;

// Dualzahlen
int d {0b1111'1111};            // 2^8 -1 = 255
cout << "d = " << d << endl;

fĂŒhrt zur Ausgabe:

h = 65535
o = 511
d = 255

Damit die Zahlen leichter lesbar sind, kann man (wie oben bei 0b1111'1111 ) Hochkommas als Trennungszeichen einfĂŒgen; allerdings werden diese nicht von allen Compilern als Trennungszeichen erkannt.

Die Möglichkeit Dualzahlen einzugeben, wurde erst mit der Spezifikation C++14 eingefĂŒhrt (ihr Compiler benötigt die zugehörige Option).

FĂŒr die Ausgabe von gibt es vorbereitete Funktionen, wie die folgende Anwendung zeigt, die die drei oben definierten Variablen h, d, o als Hexadezimalzahl ausgibt:

cout << "hex: " << endl;
cout << hex << h << endl;
cout << hex << d << endl;
cout << hex << o << endl;

Die Ausgabe lautet:

hex:
ffff
ff
1ff

FĂŒr Oktalzahlen wird anstelle von hex eingesetzt: oct .

Die Basis kann auch ausdrĂŒcklich gesetzt werden:

#include <iomanip>

// Basis ausdrĂŒcklich setzen (erfordert #include <iomanip>); leider nur Basis 8, 10, 16 möglich
cout << setbase(16) << 10 << endl;       // Ausgabe: a
                                         // andere Basen werden auf 10 abgebildet
                                        
cout << 10 << endl;                      // Ausgabe: a  Denn: Solange die Basis nicht umgestellt wird, bleibt sie 16

Aufgaben:

1. Testen Sie, ob die hier vorgestellten Funktionen mit Gleitkommazahlen arbeiten.

2. Es gibt noch sehr viele weitere Funktionen, die den Input- oder Output-Stream cin oder cout beeinflussen. Diese sind in Input/output manipulators enthalten und werden (wie im Beispiel oben) mit

#include <iomanip>

inkludiert.

Suchen Sie die entsprechenden Seiten in der Dokumentation auf und testen Sie insbesondere die Eigenschaften von

boolalpha
showbase
setbase
setfill
setprecision
fixed
scientific
hexfloat