Im letzten, eher technischen Artikel ging es um den Stack und wie er genutzt wird, um Funktionen, Funktionsaufrufe und Variablendeklarationen abzubilden; bisher können wir primär allerdings nur ganze Zahlen darstellen. Heute wollen wir darauf aufbauend uns wieder der konkreten Programmierung mit C++ zuwenden und besprechen, wie sich auch andere und komplexere Daten darstellen lassen.
Wie wir in einem früheren Artikel besprochen haben, muss bei der Deklaration von Variablen in C++ immer auch ihr Wertebereich – der Datentyp – mit angegeben werden. Das hat 2 wichtige Gründe: erstens kann so der Compiler an Hand des Datentyps abschätzen, wie viel Speicherplatz eine Variable auf dem Stack benötigt und entsprechenden Code generieren (nicht jeder Typ braucht 4 Byte) und zweitens kann der Compiler überprüfen, ob nur sinnvolle Operationen im Programm ausgeführt werden – einer ganzen Zahl eine Zeichenkette zuweisen ist nicht sinnvoll und kann durch den Compiler so ausgeschlossen werden.
Bisher kennen wir lediglich den Datentyp der ganzen Zahlen, bei der Variablendeklaration mit int
markiert. Üblicherweise werden für ganze Zahlen 4 Byte, also 32 Bit benutzt; damit lassen sich insgesamt 232, also 4.294.967.296 verschiedene Zahlen darstellen (und bevor entsprechende Kommentare kommen: ja, int
ist nicht auf 32 Bit im C++-Standard festgelegt und kann auch andere Größen annehmen; oft sind es jedoch 32 Bit), je zur Hälfte positive und negative, mit −2.147.483.648 als der kleinsten und 2.147.483.647 als der größten Zahl.
Daneben existieren in C++ noch weitere, fest eingebaute Datentypen zur Speicherung von Informationen; die folgende Tabelle gibt einen Überblick über die entsprechenden Schlüsselwörter, die zur Deklaration genutzt werden können, zusammen mit den typischerweise auf aktuellen Systemen für den jeweiligen Datentyp gültigen Wertebereichen (die Tabelle ist nicht erschöpfend, umfasst aber die wichtigsten Typen) und der Anzahl der Bytes, die zur Darstellung der Werte verwendet wird (der Bitbreite, falls der Wert in Bit angegeben wird). Zu beachten ist: die Wertebereiche sind (abgesehen von char
, welches immer 1 Byte groß ist) lediglich gängige Werte in der C++-Welt; sie sind nicht durch den C++-Standard festgelegt und können zwischen Betriebssystemen und Compilern variieren – lediglich ihre Reihenfolge ist standardisiert (bei den Zahlen gehe ich davon aus, dass 1 Byte 8 Bit entspricht – auf den meisten Systemen ist das auch so).
Typ | Bytes | Minimum | Maximum |
---|---|---|---|
bool | 1 | 0 (falsch) | 1 (wahr) |
char | 1 | -128 | 127 |
short | 2 | -32.768 | 32.767 |
int | 4 | -2.147.483.648 | 2.147.483.647 |
long | 4 | -2.147.483.648 | 2.147.483.647 |
long long | 8 | -9.223.372.036.854.775.808 | 9.223.372.036.854.775.807 |
float | 4 | -3,40282 * 1038 | 3,40282 * 1038 |
double | 8 | -1,79769 * 10308 | 1,79769 * 10308 |
Der Typ bool
dient zur Darstellung von Wahrheitswerten; historisch bedingt unterscheidet C++ allerdings nicht zwischen Zahlen und Wahrheitswerten, weswegen an eine Variable vom Typ bool
neben den speziellen (Wahrheits-)Werten true
und false
auch Zahlen zugewiesen werden können
Neben den hier genannten Typen gibt es von den meisten Typen auch noch eine Variante mit gleicher Bitbreite, allerdings ohne Vorzeichen, sprich, mit einem Wertebereich ausschließlich im Bereich der positiven Zahlen. Diese Datentypen werden einfach mit dem vorangestellten Schlüsselwort unsigned
gekennzeichnet und haben üblicherweise die folgenden Wertebereiche (der Minimalwert ist natürlich immer 0):
Typ | Bytes | Maximum |
---|---|---|
unsigned char | 1 | 255 |
unsigned short | 2 | 65.535 |
unsigned int | 4 | 4.294.967.295 |
unsigned long | 4 | 4.294.967.295 |
unsigned long long | 8 | 18.446.744.073.709.551.615 |
Wollen wir also eine Variable deklarieren, die eine Zahl im Bereich von 0 bis 255 aufnehmen kann, so schreiben wir (der Variablenname kann natürlich frei gewählt werden):
unsigned char c;
Eine natürliche Zahl wird so deklariert:
unsigned int i;
Für eine reelle Zahl einfacher Genauigkeit schreiben wir
float f;
Und für eine reelle Zahl doppelter Genauigkeit entsprechend:
double d;
Der für die einzelnen Variablen auf dem Stack reservierte Speicher hängt dabei von verschiedenen Faktoren ab, unter anderem natürlich von der Bitbreite, aber auch von diversen Einstellungen des Compilers, die hier aber erst einmal nicht näher diskutiert werden sollen.
Nun kennen wir zwar weitere Möglichkeiten zur Darstellung von Werten, aber ganz befriedigend ist das auch noch nicht. Stellen wir uns einmal vor, wir wollen 10 ganze Zahlen auf einmal darstellen. Natürlich könnte man dafür 10 Variablen anlegen, aber das ist mühselig und wird umso umständlicher, je mehr Zahlen man darstellen möchte. Weiterhin ist es bei einzelnen Variablen kaum möglich, Algorithmen umzusetzen, welche diese 10 Werte in erst zur Laufzeit bekannter Reihenfolge auswerten sollen. Wir benötigen also eine weitere Struktur, die uns 1. die Deklaration einer Sammlung von Werten gleichen Typs erlaubt und 2. auch einen Zugriff auf beliebige dieser Werte zur Laufzeit gestattet.
Das Mittel der Wahl zu diesem Zweck sind in C++ die sogenannten Arrays (oder deutsch Felder). Ein Array ist, wie angekündigt, eine Sammlung gleichartiger Werte mit wahlfreiem Zugriff auf diese Werte zur Laufzeit. Bei der Deklaration eines Arrays wird üblicherweise dessen Größe mit angegeben, so dass der Compiler bereits bei der Kompilierung berechnen kann, wie viel Speicherplatz auf dem Stack für dieses Array benötigt wird. Die Deklaration eines Arrays erfolgt über die Angabe des zugrunde liegenden Datentyps und der Größe, also etwa folgendermaßen:
int as[10];
Hier haben wir ein Integer-Array der Größe 10 (in den eckigen Klammern) deklariert, also ein Array, welches 10 ganze Zahlen enthalten kann (dass die Größe hinter dem Variablennamen und nicht hinter dem Datentyp steht, ist eine Eigenheit von C++ und etwas bedauerlich, leider aber nicht zu ändern). Möchten wir auf ein bestimmtes Element innerhalb des Arrays zugreifen, erfolgt ebenfalls durch die Nutzung der eckigen Klammern und der Angabe eines Indexes; um etwa dem 5. Element des Arrays einen Wert zuzuweisen, schreiben wir:
as[4] = 10;
Wer jetzt denkt, die 4
wäre ein Tippfehler, der irrt; Arrays sind in C++ 0-indiziert, das heißt, dass das erste Element im Array mit dem Index 0, das zweite Element mit dem Index 1 und so weiter angesprochen wird. Auf den ersten Blick ist das nicht sonderlich intuitiv, aber man gewöhnt sich dran. Möchte man alle Werte eines Arrays gleich bei der Deklaration des Arrays setzen, bietet sich die folgende Schreibweise an:
int as[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Der folgende Code ist dagegen leider nicht möglich (die Gründe hierfür kenne ich allerdings nicht – wem sie bekannt sind, der möge es in den Kommentaren schreiben):
int as[10]; as = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Als Index für ein Array lassen sich natürlich nicht nur konstante Zahlen benutzen; auch Variablen, welche eine natürliche (oder ganze) Zahl repräsentieren, können als Index verwendet werden. Das könnte dann etwa so aussehen (warum die Änderung am Array auch außerhalb des Funktionsaufrufes Auswirkungen hat, werden wir in einem zukünftigen Artikel sehen):
void set( int as[10], int i ) { as[i] = 5; }
Hierbei ist übrigens darauf zu achten, dass man sich innerhalb der Grenzen des Arrays aufhält, also keinen Index verwendet, der außerhalb des gültigen Bereiches liegt (für ein Array der Größe 10 gilt also: 0 ≤ i ≤ 9
). Leider kann der Compiler in der Regel nicht sicherstellen, dass diese Grenzen im Code eingehalten werden, so dass diese Aufgabe dem Programmierer obliegt. Noch schlimmer ist, dass auch zur Laufzeit des Programms Verletzungen der Array-Grenzen oft nicht bemerkt werden und darin münden, dass mit unsinnigen Werten gerechnet wird oder noch schlimmer, Werte in Speicherbereiche geschrieben werden, wo sie gar nicht hin sollten – es ist also besonders wichtig, sicherzustellen, dass die Grenzen eingehalten werden.
Gerechnet werden kann mit Array-Elementen ebenfalls; in folgendem Code etwa werden das i
-te und j
-te Element eines Arrays miteinander addiert und als Ergebnis zurückgegeben:
int add( int as[10], int i, int j ) { return as[i] + as[j]; }
Was ist nun aber, wenn wir beim Schreiben des Programmcodes noch nicht genau wissen, wie groß ein Arrays sein muss, um alle benötigten Daten aufzunehmen? Intuitiv könnte man annehmen, dass der folgende Code das Problem löst und ein Array der Größe n
deklariert:
int n; ... n = ... ... int as[n];
Das funktioniert allerdings nicht, und zwar aus einem ganz einfachen Grund: bei einer Variablendeklaration in C++ muss der Compiler zur Kompilierungszeit bereits wissen, wie viel Speicher die Variable benötigt, um entsprechenden Code generieren zu können. In der obigen Konstellation wäre die Größe des Arrays zur Zeit der Kompilierung aber nicht bekannt und dem Compiler wäre nicht bekannt, wie viel Platz auf dem Stack reserviert werden müsste. Wir benötigen also einen anderen Mechanismus, um Speicher erst zur Laufzeit reservieren zu können. Das Schlüsselwort hierfür ist der Heap – was das aber genau ist, werden wir im nächsten Artikel sehen.
Kommentare (22)