C++-Programmierung/ Zusatzthemen
Zielgruppe:
Alle die sich für Hintergrundwissen interessieren
Lernziel:
Je nach Kapitel
Interne Zahlendarstellung [Bearbeiten]
Wie bereits in der Einführung der Variablen erwähnt, sind Variablen nichts weiter als eine bequeme Schreibweise für eine bestimmte Speicherzelle bzw. eine Block von Speicherzellen. Die kleinste Speicherzelle, welche direkt vom Prozessor "adressiert" werden kann, ist ein Byte. Ein Byte besteht (heutzutage) aus 8 Bit. So stellen Sie ein Byte grafisch dar:
Das unterste Bit, Bit 0, wird als "niederwertigstes Bit" bezeichnet (englisch: "least significant bit: LSB"), das oberste Bit, Bit 7, als "höchstwertiges Bit" (englisch: "most significant bit: MSB").
Jedes dieser acht Bits kann den Wert 0 oder 1 annehmen. Damit kann ein Byte 28=256 verschiedene Werte annehmen: Wie diese 256 verschiedenen Werte "interpretiert" werden, ist abhängig vom Datentyp. Der C++ Datentyp, der genau ein Byte repräsentiert, ist char, also zu Deutsch: "Zeichen". Jeder Buchstabe, jede Ziffer, jedes der so genannten "Sonderzeichen" kann in einem solchen char gespeichert werden. (Zumindestens sofern Sie in Westeuropa bleiben. Für die vielen verschiedenen Zeichen z.B. der asiatischen Sprachen genügt ein char nicht mehr, doch dafür gibt es den Datentyp wchar_t, der größer als ein Byte ist und somit mehr Zeichen darstellen kann.)
Das Zeichen 'A' wird z.B. durch die Bitkombination 01000001 dargestellt, das Zeichen '&' durch 00100110. Die Zuordnung von Bitwerten zu Zeichen ist im (ASCII-Code) festgeschrieben, den heute eigentlich alle Computer verwenden. Im Anhang D ist der ASCII-Code aufgelistet.
Die Bitkombinationen eines Bytes können Sie natürlich auch als Zahlen auffassen. Die Bitkombinationen 00000000 bis 01111111 entsprechen dann den Zahlen 0 bis 127. Leider ist nun die Frage, welche Zahlen den Bitfolgen 10000000 bis 11111111 entspricht, nicht mehr so eindeutig. Naheliegend wären ja die Zahlen 128 bis 255. Es könnten aber auch die Zahlen -128 bis -1 sein. Um die zugrunde liegenden Zusammenhänge zu erläutern muss etwas weiter ausgeholt werden
Wie auch bei den Ganzzahltypen (und char gehört schon ein wenig mit dazu), wird zwischen "vorzeichenbehafteten" (englisch: "signed") und "vorzeichenlosen" (englisch: "unsigned") Zahlen unterschieden. Diese Unterscheidung existiert jedoch nur im "Kopf des Programmierers". Der Prozessor weiß nicht, ob der Wert einer Speicherzelle vorzeichenbehaftet ist oder nicht. Zum Addieren und Subtrahieren muss er das auch nicht wissen. Die gewählte "Zweierkomplement-Darstellung" gestattet es, mit vorzeichenbehafteten Zahlen genauso zu rechnen wie mit vorzeichenlosen. Nur beim Multiplizieren und Dividieren müssen Sie dem Prozessor mitteilen, ob er eine normale (vorzeichenlose) oder eine vorzeichenbehaftete Operation ausführen soll.
Dezimal | Bit-Stellen | ||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
… | … | ||||||||
252 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | |
253 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | |
254 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | |
255 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | |
256 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
257 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
258 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
259 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
… | … |
Zur besseren Veranschaulichung die beispielhafte Binärdarstellung der Zahlen um 255 herum:
Sie sehen, dass die Zahlen ab 256 nicht mehr in die 8 Bits eines Bytes passen, das Byte "läuft über". Wenn Sie nun z.B. zu 255 eine 1 addieren, und das Ergebnis in einem Byte speichern wollen, wird das oberste Bit abgeschnitten und übrigt bleibt eine 0, die dann gespeichert wird.
Da bei dem Übergang von 255 zu 0 die Zahlen wieder von vorne anfangen, können Sie sich den Zahlenbereich auch als Kreis vorstellen. Entgegen dem Uhrzeigersinn (in Pfeilrichtung) "wachsen" die Zahlen, in die andere Richtung fallen sie. An irgendeiner Stelle erfolgt dann ein "Sprung". Wenn diese Stelle bei einer Berechnung überschritten wird, wird dies einen "Überlauf" genannt. Bei "unsigned" Zahlen unterhalb der 0, bei den "signed" Zahlen genau gegenüber:
Diese Darstellung negativer Zahlen hat einige Vorteile:
- Der Prozessor muss beim Addieren und Subtrahieren nicht darauf achten, ob eine Zahl vom Programmierer als vorzeichenbehaftet oder vorzeichenlos interpretiert wird. Beim Addieren zählt er entsprechend viele Zahlen "links herum", beim Subtrahieren entsprechend andersherum. Falls dabei ein Überaluf auftritt, ist das dem Prozessor "egal".
- Die "Sprungstelle", also der Überlauf, ist möglichst weit weg von der Null und den kleinen Zahlen, die am meisten benutzt werden, so dass Sie beim Rechnen (hoffentlich) nicht in den Bereich dieses Überlaufes kommen.
Diese Darstellung negativer Zahlen wird auch bei den anderen Ganzzahltypen angewendet, nur ist der Kreis dort wesentlich größer, und enthält mehr darstellbare Zahlen.
Während nun bei den Ganzzahltypen int, short und long festgelegt ist, dass sie standardmäßig immer "signed" sind (so können Sie in C++ auch signed int statt int usw. schreiben, es ist der gleiche Datentyp), konnte man sich bei char nicht einigen, ob mit oder ohne Vorzeichen praktischer ist. Folglich kann jeder Compilerhersteller dies nach eigenem Gusto entscheiden. Ob auf einem System der char-Datentyp nun vorzeichenbehaftet ist oder nicht, ist jedoch in der Praxis nicht wichtig, sofern Sie nicht mit ihnen rechnen wollen. (Zum Rechnen werden char-Variablen und -Literale stets erst in ein int umgewandelt. Erst dann wird z.B. aus einem Byte mit den Bits 11111111 der Wert 255 oder -1, je nach dem.)
Sie können jedoch explizit signed char oder unsigned char schreiben, wenn Sie sicher gehen wollen, dass Sie ein vorzeichenbehaftetes bzw. vorzeichenloses char bekommen. (Kurioserweise ist ein char ein eigener Datentyp, auch wenn er stets entweder einem signed char oder einem unsigned char gleicht. Es gibt also in C++ drei verschiedene char-Datentypen!)
Die übrigen Ganzzahltypen belegen mehrere Bytes, wie viele genau, können Sie herausbekommen, in dem Sie ein kleines Programm schreiben, das folgendes ausgibt:
std::cout << "Größe eines short: " << sizeof(short) << std::end;
std::cout << "Größe eines int : " << sizeof(int) << std::end;
std::cout << "Größe eines long : " << sizeof(long) << std::end;
sizeof( Typ_oder_Ausdruck ) gibt die Größe eines Typs oder Ausdrucks in Byte zurück. (Genauer: sizeof() gibt an, wie viele Bytes der Datentyp im Vergleich zu einem 'char' benötigt. sizeof(char) ist definitionsgemäß gleich 1, da keine kleinere Speichergröße direkt angesprochen werden kann.)
Die Größe der Datentypen ist jedoch nicht genau festgelegt und variiert von Compiler zu Compiler und von Prozessor zu Prozessor! Sie dürfen sich also nicht darauf verlassen, dass ein int immer soundsoviel Bytes groß ist! Das einzige, das im C++ Standard festgelegt ist, ist folgende Größenrelation:
sizeof(char) := 1 sizeof(long) ≥ sizeof(int) ≥ sizeof(short) ≥ 1 sizeof( X ) = sizeof( signed X ) = sizeof( unsigned X ) (für X = int, short, long, char)
Wissen Sie jedoch die Größe eines Ganzzahltyps, wüssten Sie auch, wie groß der Wertebereich ist, den er aufnehmen kann:
Größe in Byte | Wertebereich | |
unsigned |
signed
| |
1 (8bit) | 0 … 255 | -128 … +127 |
2 (16bit) | 0 … 65.536 | -32.768 … +32.767 |
4 (32bit) | 0 … 4.294.967.296 | -2.147.483.648 … +2.147.483.647 |
8 (64bit) | 0 … 18.446.744.073.709.551.616 | -9.223.372.036.854.775.808 … +9.223.372.036.854.775.807 |
Warum hat man die Größe der Datentypen nicht festgelegt?
Ganz einfach. Es existieren sehr verschiedene Prozessortypen. Es gibt so genannte 16-Bit-Prozessoren, das heißt, diese können 16-bittige Zahlen "in einem Rutsch" verarbeiten. Die nächste Generation waren die 32-Bit-Prozessoren, sie können 32-Bit-Zahlen besonders schnell verarbeiten. Heute sind 64-Bit-Prozessoren Standard, sie haben die 32-Bit-Prozessoren zumindest im Customer-Bereich weitesgehend verdrängt.
Wäre jetzt festgelegt, dass ein int immer 32-bit groß ist, wären C++ Programme auf 16-Bit-Prozessoren sehr langsam, da sie nur "häppchenweise" mit 32-bit-Werten rechnen können. Darum wurde festgelegt, dass ein int immer so groß sein soll, wie die so genannte "Wortbreite" des Prozessors. Das garantiert, dass Berechnungen mit int's immer am schnellsten ausgeführt werden können.
Mal als Überblick, welche Größen die Ganzzahl-Datentypen auf verschiedenen Plattformen haben:
Plattform | Größe des Datentyps (in Byte) | ||
short |
int |
long
| |
DOS (16 bit "Real Mode") | 2 | 2 | 4 |
Linux (auf 32bit-Plattformen wie x86, PowerPC u.a.) OS/2 (ab 3.0) MS-Windows (ab 9x/NT/2k) |
2 | 4 | 4 |
Linux (auf 64bit-Plattformen wie Alpha, amd64 u.a.) | 2 | 4 | 8 |
Sie sehen, dass unter Linux auch auf einem 64bit-System ein int nur 32 Bit groß ist. Vermutlich wurde sich für 32-bittige int's entschieden, weil es zu viele C- und C++-Programme gibt, die darauf vertrauen, dass ein int immer 32 bit groß ist. :-( Es ist aber nicht sicher, dass das auch in Zukunft so bleiben wird.
Wenn Sie low-level Code schreiben wollen/müssen, und auf die genaue Größe seiner Ganzzahltypen angewiesen sind, gibt es spezielle typedef's in dem Standard-Header <stdint.h>, auf welche später genauer eingegangen wird.
Zusammenfassend lässt sich also aussagen, dass ein int mehrere aufeinander folgende Bytes im Speicher belegt. Wie bereits erläutert, werden die Bits in einem Byte von 0 bis 7 gezählt, das Bit 0 ist dabei die "Einer-Stelle", das Bit 7 hat die Wertigkeit 128 (bzw. -128 bei vorzeichenbehafteten Bytes). Bei einem 2-Byte-short int ergeben sich also 16 Bits, und das Ganze sieht dann wie folgt aus:
Das MSB ist in diesem Falle das Bit 15. Die Bits 0..7 bilden das so genannte "Low Byte", die Bits 8..15 das "High Byte".
So weit so gut. Das Problem ist jetzt die Byte-Reihenfolge, also wie dieses "Doppelbyte" abgespeichert wird. Die einen sagen: Low-Byte zuerst, also das "untere Byte" an die "kleinere Adresse", das High-Byte in die darauf folgende, dies wird "Little Endian" genannt. Andere finden es natürlicher, wenn das High-Byte "zuerst" gespeichert wird, also an der kleinere Adresse im Speicher liegt. Dies nennt sich "Big Endian"-Speicherung. Diese beiden Methoden der Speicherung von Mehrbyte-Werten existieren auch bei 32- und 64-Bit-Werten.
Da sich in dieser Frage die verschiedenen Prozessor-Hersteller nicht einig wurden, existieren bis heute beide Varianten. Der eine Prozessor macht es so herum, ein anderer andersherum:
Prozessor-Plattform | "Endianness" |
---|---|
Intel x86 und kompatible | Little Endian |
Intel Itanium (64-Bit-CPU) | Little Endian |
Motorola 68k | Big Endian |
PowerPC | Vom Betriebssystem einstellbar |
Sun SPARC | Big Endian |
DEC Alpha | Vom Betriebssystem einstellbar |
Wichtig wird diese so genannte "Byte order" immer dann, wenn Daten in eine Datei gespeichert oder über ein Netzwerk auf andere Computer geschickt werden sollen. Denn falls die Datei später von einem Computer mit anderer Byte Order gelesen wird, versteht er die gespeicherten Daten nicht mehr richtig. Sie müssen also bei der Speicherung oder Übermittlung von Daten stets vorher festlegen, welche Byte Order man benutzt. Im Internet wurde dafür der Begriff "network byte order" geprägt. Dieser bedeutet: Big Endian.
Gleitkommazahlen
[Bearbeiten]Gleitkommazahlen werden heutzutage meist nach dem so genannten IEEE 754 Standard abgespeichert. Nach diesem Standard ist ein 32bit- (einfache Genauigkeit) und ein 64bit- (doppelte Genauigkeit) Gleitkommadatentyp definiert. Der 32-bit Gleitkommadatentyp ist dabei wie folgt aufgebaut:
Das Vorzeichen-Bit gibt dabei an, ob die Zahl positiv (Vorzeichen=0) oder negativ (Vorzeichen=1) ist. In der Mantisse werden nur die Binärstellen nach dem Komma gespeichert. Vor dem Komma steht implizit stets eine 1. Der gespeicherte Wert für den Exponenten ist um 127 größer als der echte Exponent. Die gespeicherten Werte von 0..255 für den Exponenten entsprechen also einem realen Wertebereich von -127...+128. Dabei sind die Werte -127 und +128 "reserviert" für spezielle Gleitkommawerte wie "null" oder "unendlich" (und noch einige andere, so genannte NaN's und 'denormalisierte Zahlen', worauf hier aber nicht weiter eingegangen wird). Der Wert einer 32-bit-Gleitkommazahl errechnet sich dabei wie folgt:
Der Wert "null" wird dabei mit Exponent 0 und Mantisse 0 gespeichert. Dabei kann das Vorzeichen 0 oder 1 sein, es wird also zwischen +0.0 und -0.0 unterschieden. Damit kann man z.B. erkennen, ob das Ergebnis einer Berechnung ein "sehr kleiner positiver Wert" war, der auf 0 gerundet wurde, oder ein "sehr kleiner negativer Wert". Ansonsten sind beide Werte identisch. Ein Vergleich (+0.0 == -0.0) ergibt also den Wert "true", also "wahr".
Der Wert "unendlich" wird mit Exponent 255 und Mantisse 0 gespeichert. "Unendlich" bedeutet dabei "größer als die größte darzustellende Zahl". Auch hier wird wieder zwischen "+unendlich" und "-unendlich" unterschieden. "Unendlich" kann dabei durchaus sinnvoll sein. So liefert die mathematische Funktion atan() (arcus tanges) für "unendlich" genau den Wert Pi/2.