Einführung in die Programmierung: Fundamentale Datentypen
In der Erstellung von Algorithmen konzentriert man sich zunächst auf die Abfolge der Befehle, die in ihrer Gesamtheit das gegebene Problem (meist die Berechnung von Ausgabewerten aus Eingabewerten) lösen. Dabei gerät man schnell zur Frage, wie dabei auftretende veränderliche Größen modelliert werden. Jede Programmiersprache bietet dazu fundamentale Datentypen an, auf denen gewisse Operationen ausgeführt werden können (wie etwa die ganzen Zahlen zusammen mit den Grundrechenarten). Vorgestellt werden hier fundamentale Datentypen wie ganze Zahlen, Gleitkommazahlen, logische Werte und Zeichen. Zu ihrem besseren Verständnis ist es nötig, in Grundzügen die Speicherorganisation kennenzulernen.
Einordnung des Artikels
- Einführung in die Informatik
- Einführung in die Programmierung
- Fundamentale Datentypen
- Einführung in die Programmierung
Wie? und Was?
Algorithmen und Daten
Der aktuelle Teil des Kurses, also Einführung in die Programmierung, hat bisher vielleicht den Eindruck erweckt, dass es beim Programmieren immer nur darum geht Algorithmen zu schreiben, also Anweisungen (oder Operationen) in geeigneter Reihenfolge anzugeben. Unter einem Programm stellt man sich also einen Vorgang vor und Algorithmierung konzentriert sich auf das Wie?
Der Blick auf die Aufgaben aus dem letzten Abschnitt verrät aber einen weiteren Aspekt: Das Wie? ist untrennbar mit dem Was? verknüpft. Damit ist gemeint, dass die Objekte, mit denen in den Anweisungen jongliert wird, ganz bestimmte Eigenschaften haben, die irgendwo niedergelegt werden müssen.
Dazu betrachte man etwa die Aufgabe:
Ausgabe der Hexadezimalzahlen von (0000)16 bis (FFFF)16, wobei jede Zahl vierstellig angegeben werden soll (also eventuell mit führenden Nullen).
Wie soll man die vierstelligen Hexadezimalzahlen modellieren?
- Etwa als vierstellige Zahlen — aber es kommen doch die Ziffern A, B, ..., F vor!
- Also als Zeichen — mit dem erlaubten Zeichensatz 0, 1, ..., 8, 9, A, ..., F.
- Andererseits soll man mit den Ziffern rechnen können — und wie soll man mit Zeichen rechnen?
- Und dass es sich um vierstellige Zahlen handelt, spricht dafür, mit einem zusammengesetzten Datentyp zu arbeiten: also mit einem Feld, einer Liste oder einem Vektor der Länge 4.
Es soll hier nicht versucht werden, den geeigneten Datentyp für obige Aufgabe zu finden; die Diskussion soll lediglich zeigen, wie selbstverständlich Algorithmen und Datentypen miteinander verschränkt sind; kurz: das Wie? ist untrennbar mit dem Was? verknüpft.
Übersicht über Datentypen
In der Mathematik unterscheidet man grundlegende Mengen wie
- natürliche Zahlen N
- ganze Zahlen Z
- rationale Zahlen Q
- reelle Zahlen R.
Datentypen sind zwar an die Mengen der Mathematik angelehnt, sie müssen aber immer zusammen mit den Operationen gesehen werden, die auf der Menge definiert sind (das Was? ist also mit dem Wie? verknüpft). Kurz:
Ein Datentyp ist eine Wertemenge zusammen mit den darauf definierten Operationen.
Mit den elementaren Datentypen sind immer diejenigen Datentypen einer Programmiersprache gemeint, für die gilt:
- Sie sind schon durch die Programmiersprache vorgegeben (müssen also nicht erst vom Programmierer definiert werden).
- Sie sind nicht zerlegbar, wie Zahlen oder Zeichen (bei einer Zeichenkette als Datentyp würde man von einem zusammengesetzten Datentyp sprechen).
- Gewisse Operationen sind bereits für den Datentyp definiert.
Die zusammengesetzten Datentypen werden oft als Datenstruktur bezeichnet; Beispiele dafür sind komplexe Zahlen, Listen, Vektoren.
In den Bibliotheken von Programmen, die in ein eigenes Programm eingebunden werden können, sind bei allen heutigen Programmiersprachen zahlreiche zusammengesetzte Datentypen enthalten. Und da dort auch eine Vielzahl von Operationen bereits definiert ist, ist es ratsam, zuerst in den Bibliotheken nachzuschlagen, wenn man einen zusammengesetzten Datentyp benötigt.
Erst wenn man eine zu spezielle Datenstruktur benötigt, wird man sie selber definieren (also zusammen mit den entsprechenden Operationen) oder eine vorgegebene Datenstruktur erweitern; man spricht dann von einem selbstdefinierten Datentyp. Die selbstdefinierten Datentypen ermöglichen erst objektorientierte Programmierung (OOP) und werden mit deren Grundlagen ausführlich besprochen.
Aufgabe:
Warum ist es in der Maschinensprache nicht möglich selbstdefinierte Datentypen anzulegen?
Vorerst werden die elementaren Datentypen besprochen, für die es zahlreiche Namen gibt, anstelle von Datentypen sagt man meist nur Typen:
- einfache Datentypen
- primitive Datentypen
- eingebaute Datentypen
- fundamentale Datentypen.
Im Englischen werden sie meist als fundamental types bezeichnet.
Die Liste der elementaren Datentypen von C++ ist sehr lang (und es ist für den Anfänger nicht nötig alle zu kennen) — in Java ist sie deutlich kürzer. Aber da man die Möglichkeit bieten möchte, Programme zu schreiben, die sehr wenig Speicherplatz benötigen und sehr schnell sind, muss der Programmierer für seine Anwendung die optimale Kodierung der Daten auswählen können. Umgekehrt entstehen dadurch Spitzfindigkeiten und Fehlerquellen, die man mit weniger elementaren Datentypen vermieden hätte.
Die elementaren Datentypen können zunächst in zwei Gruppen eingeteilt werden:
- Ganzzahlige Typen (integral types)
- Gleitkommazahlen (Zahlen mit Nachkommastellen, floating-point types)
Bei den ganzzahligen Typen können wiederum drei Untergruppen unterschieden werden:
- Boolesche Variable
- C++: bool
- Java: boolean
- Zeichen
- C++: char, signed char, unsigned char
- Java: char
- echte Zahlen
- C++: short int, int, long int, unsigned short int, unsigned int, unsigned long int und noch weitere
- Java: byte, short, int, long
Die ersten beiden Untergruppen mögen verwundern: warum sind sie in ganzzahligen Typen enthalten? Rechner-intern werden sie wie ganze Zahlen behandelt und bilden daher keine eigene Gruppe.
Um die Unterscheidung zwischen den Datentypen und die mit ihnen erlaubten Operationen zu verstehen, ist es unerlässlich zu kennen, wie der Speicher organisiert ist, in dem eine Variable abgelegt wird (Größe des Speichers, Interpretation des Inhaltes).
Speicherorganisation und elementare Datentypen
Mögliche Darstellungen für ganze Zahlen
Zunächst mag es merkwürdig erscheinen, dass die meisten elementaren Datentypen mit oder ohne Vorzeichen angeboten werden (etwa int und unsigned int). Dies wird aber verständlicher, wenn man sich überlegt, wie man ganze Zahlen abspeichern kann.
Für positive Zahlen sollte klar sein: sie werden im Speicher als Binärzahlen dargestellt — man spricht von der Binärdarstellung.
Aber wie werden negative Zahlen dargestellt? Naheliegend ist es ein Bit für das Vorzeichen zu reservieren und den Betrag der Zahl in den verbleibenden Bits darzustellen. Man nennt dies die Vorzeichendarstellung. Daneben gibt es auch die Zweierkomplementdarstellung, die komplizierter ist, aber leichter zu verarbeiten ist als die Vorzeichendarstellung.
Im Folgenden werden alle Darstellungen mit 4 Bit Speicherplatz diskutiert; wie man diese Überlegungen auf größere Speicherzellen überträgt, sollte dann nicht schwer sein.
Binärdarstellung
Hat man 4 Bit zur Verfügung kann man insgesamt 24 = 16 verschiedenen Zahlen bilden; beginnt man bei 0, ist die größte darzustellende Zahl 15 (für die Zahl 16 benötigt man schon ein Bit mehr):
0000 0 0001 1 0010 2 0011 3 0100 4 0101 5 0110 6 0111 7 1000 8 1001 9 1010 10 1011 11 1100 12 1101 13 1110 14 1111 15
Entsprechend erhält man bei 8 Bits = 1 Byte:
00000000 0 01000000 64 10000000 128 11000000 192 00000001 1 01000001 65 10000001 129 11000001 193 00000010 2 01000010 66 10000010 130 11000010 194 00000011 3 01000011 67 10000011 131 11000011 195 00000100 4 01000100 68 10000100 132 11000100 196 00000101 5 01000101 69 10000101 133 11000101 197 00000110 6 01000110 70 10000110 134 11000110 198 00000111 7 01000111 71 10000111 135 11000111 199 00001000 8 01001000 72 10001000 136 11001000 200 00001001 9 01001001 73 10001001 137 11001001 201 00001010 10 01001010 74 10001010 138 11001010 202 00001011 11 01001011 75 10001011 139 11001011 203 00001100 12 01001100 76 10001100 140 11001100 204 00001101 13 01001101 77 10001101 141 11001101 205 00001110 14 01001110 78 10001110 142 11001110 206 00001111 15 01001111 79 10001111 143 11001111 207 00010000 16 01010000 80 10010000 144 11010000 208 00010001 17 01010001 81 10010001 145 11010001 209 00010010 18 01010010 82 10010010 146 11010010 210 00010011 19 01010011 83 10010011 147 11010011 211 00010100 20 01010100 84 10010100 148 11010100 212 00010101 21 01010101 85 10010101 149 11010101 213 00010110 22 01010110 86 10010110 150 11010110 214 00010111 23 01010111 87 10010111 151 11010111 215 00011000 24 01011000 88 10011000 152 11011000 216 00011001 25 01011001 89 10011001 153 11011001 217 00011010 26 01011010 90 10011010 154 11011010 218 00011011 27 01011011 91 10011011 155 11011011 219 00011100 28 01011100 92 10011100 156 11011100 220 00011101 29 01011101 93 10011101 157 11011101 221 00011110 30 01011110 94 10011110 158 11011110 222 00011111 31 01011111 95 10011111 159 11011111 223 00100000 32 01100000 96 10100000 160 11100000 224 00100001 33 01100001 97 10100001 161 11100001 225 00100010 34 01100010 98 10100010 162 11100010 226 00100011 35 01100011 99 10100011 163 11100011 227 00100100 36 01100100 100 10100100 164 11100100 228 00100101 37 01100101 101 10100101 165 11100101 229 00100110 38 01100110 102 10100110 166 11100110 230 00100111 39 01100111 103 10100111 167 11100111 231 00101000 40 01101000 104 10101000 168 11101000 232 00101001 41 01101001 105 10101001 169 11101001 233 00101010 42 01101010 106 10101010 170 11101010 234 00101011 43 01101011 107 10101011 171 11101011 235 00101100 44 01101100 108 10101100 172 11101100 236 00101101 45 01101101 109 10101101 173 11101101 237 00101110 46 01101110 110 10101110 174 11101110 238 00101111 47 01101111 111 10101111 175 11101111 239 00110000 48 01110000 112 10110000 176 11110000 240 00110001 49 01110001 113 10110001 177 11110001 241 00110010 50 01110010 114 10110010 178 11110010 242 00110011 51 01110011 115 10110011 179 11110011 243 00110100 52 01110100 116 10110100 180 11110100 244 00110101 53 01110101 117 10110101 181 11110101 245 00110110 54 01110110 118 10110110 182 11110110 246 00110111 55 01110111 119 10110111 183 11110111 247 00111000 56 01111000 120 10111000 184 11111000 248 00111001 57 01111001 121 10111001 185 11111001 249 00111010 58 01111010 122 10111010 186 11111010 250 00111011 59 01111011 123 10111011 187 11111011 251 00111100 60 01111100 124 10111100 188 11111100 252 00111101 61 01111101 125 10111101 189 11111101 253 00111110 62 01111110 126 10111110 190 11111110 254 00111111 63 01111111 127 10111111 191 11111111 255
Vorzeichendarstellung
Die Vorzeichendarstellung erhält man sofort aus der Binärdarstellung, indem man das erste Bit als Vorzeichen liest, die restlichen drei Bits stehen für den Betrag der Zahl. Zum Beispiel verwendet man 0 für + und 1 für -.
0000 0 0001 1 0010 2 0011 3 0100 4 0101 5 0110 6 0111 7 1000 0 1001 -1 1010 -2 1011 -3 1100 -4 1101 -5 1110 -6 1111 -7
Die Vorzeichendarstellung hat zwei Nachteile:
- Die Zahl 0 ist nicht mehr eindeutig dargestellt, sie wird durch 0000 und 1000 kodiert (beide Zahlen haben den Betrag 0).
- Möchte man zwei Zahlen addieren, kann man nicht sofort loslegen, sondern muss eine Fallunterscheidung machen und zunächst das Vorzeichen abfragen.
Aufgabe:
Es sollen zwei Zahlen addiert werden, so dass das Ergebnis immer noch im gegebenen Zahlenbereich (bei 4 Bit) liegt. Zum Beispiel 7 + 5 in der Binärdarstellung oder 2 + 3 in der Vorzeichendarstellung.
Formulieren Sie: Für welche Zahlen erhält man das richtige Ergebnis, wenn die Addition als Addition der kodierten Zahlen (mit Basis 2) durchgeführt wird.
Lösung:
In der Binärdarstellung kommt immer das richtige Ergebnis heraus, denn die Zahlen sind richtig angeordnet und es darf keinen Unterschied machen, ob man zuerst ins Dualsystem umrechnet und dann addiert oder ob man die Dezimalzahlen addiert und dann ins Dualsystem umrechnet.
Bei der Vorzeichendarstellung ist die Addition komplizierter:
1. Addiert man zwei positive Zahlen, ist das Ergebnis richtig (Begründung wie bei der Binärdarstellung).
2. Addiert man eine positive und eine negative Zahl, erhält man zwei unterschiedliche Ergebnisse. Zum Beispiel 2 + (-3) = -1. Übersetzt man beide Zahlen in die Binärdarstellung, erhält man:
0010 + 1011
Addiert man diese Zahlen, so wie man Dualzahlen addieren muss, erhält man:
0010 + 1011 = 1101 = -5
In der Binärdarstellung ist 1101 eine negative Zahl vom Betrag 5, also als -5 zu interpretieren.
3. Addiert man zwei negative Zahlen, zum Beispiel (-2) + (-3), so ergibt sich:
1010 + 1011 = 1101 = -5
Hier stimmt das Ergebnis; man kann allgemein zeigen, dass in der Binärdarstellung die Addition immer dann mit den Dualzahlen ausgeführt werden darf, wenn beide Summanden positiv oder negativ sind; ist einer positiv und einer negativ, führt die Addition der Dualzahlen zum falschen Ergebnis.
Eine Addition, die den gegebenen Zahlenbereich verlässt, muss in beiden Fällen gesondert behandelt werden.
Die oben genannten Nachteile der Vorzeichendarstellung behebt die sogenannte Zweierkomplementdarstellung.
Zweierkomplementdarstellung
Die letzte Aufgabe liefert den Hinweis, wie man negative ganze Zahlen kodiert: Man sucht eine Anordnung mit folgenden Eigenschaften:
- das Vorzeichen muss leicht erkennbar sein (am Besten wie in der Binärdarstellung am ersten Bit),
- die Addition soll immer das richtige Ergebnis liefern, wenn die zugehörigen Dualzahlen addiert werden.
Die folgende Anordnung der Zahlen im Kreis erfüllt beide Bedingungen.
Beispiel: Addition von Zahlen in Zweierkomplementdarstellung
Man kann an den Beispielen der Aufgabe oben testen, ob die Addition stets mit Dualzahlen ausgeführt werden darf:
1. Bei primitiven Zahlen ist es klar (solange der Zahlenbereich nicht verlassen wird).
2. Addiert man zum Beispiel 2 + (-3) = -1, so erhält man jetzt mit Dualzahlen:
0010 + 1101 = 1111 = -1
3. Addiert man zwei negative Zahlen, zum Beispiel (-2) + (-3), so ergibt sich:
1110 + 1101 = 1011 = -5
Dabei hätte sich allerdings ein Übertrag ergeben, er wurde weggelassen.
Die Zweierkomplementdarstellung hat also die Vorteile, dass man am ersten Bit das Vorzeichen einer Zahl erkennt und dass man immer ihre Dualzahlen addieren darf. Man könnte einwenden, dass ein Vorzeichenwechsel nicht so einfach ist wie in der Vorzeichendarstellung — dort musste nur das erste Bit invertiert werden. Aber auch hier ist der Vorzeichenwechsel einfach:
Zum Vorzeichenwechsel bildet man das Komplement (alle Bits invertieren) und addiert anschließend 1.
Aufgabe:
Überprüfen Sie an Abbildung 4, dass diese Operation tatsächlich den Vorzeichenwechsel liefert.
Die Subtraktion zweier Zahlen in Zweierkomplementdarstellung wird dann aus dem Vorzeichenwechsel und der anschließenden Addition zusammengesetzt.
Nach diesen ausführlichen Überlegungen zur Zweierkomplementdarstellung sollte klar geworden sein, warum man verschiedene elementare Datentypen anbietet und warum man diese penibel unterscheiden muss. In der Mathematik konnte man die verschiedenen Zahlenbereiche problemlos ineinander einbetten (natürliche Zahlen, ganze Zahlen, rationale Zahlen, reelle Zahlen). Für den Computer, der den Inhalt einer 4 Bit-Speicherzelle interpretieren muss, ist das nicht so einfach: der Eintrag
1111
steht in der
- Binärdarstellung für die Dezimalzahl 15,
- Vorzeichendarstellung für -7 und
- in Zweierkomplementdarstellung für -1.
Nach diesen Ausführungen, sollte es nicht mehr schwer sein, die zahlreichen elementaren Datentypen von C++ oder Java zu verstehen.
Boolesche Variable
Boolesche Variable oder Wahrheitswerte können nur zwei Werte annehmen: true und false, die rechnerintern durch 1 und 0 dargestellt werden; eine Boolesche Variable kommt daher mit einem Bit aus.
Verwendet werden Boolesche Variable bei Bedingungsprüfungen, Alternativen und zum Steuern von Schleifen (soll eine Schleife abbrechen oder nochmals durchlaufen werden).
Zeichen
Mit einem Zeichen ist immer ein einziges Zeichen gemeint, also eine Ziffer, ein Buchstabe, ein Sonderzeichen oder auch nicht-sichtbare Zeichen wie Steuerzeichen (Zeilenvorschub, Tabulator).
Unterscheiden muss man Zeichen von Zeichenketten (strings), die keine elementaren Datentypen mehr sind und intern aus Zeichen aufgebaut werden.
Ausgangspunkt in jeder Programmiersprache für den Datentyp Zeichen ist der ASCII-Code (ASCII = American Standard Code for Information Interchange). Er umfasst 27 = 128 Zeichen, die von 0 bis 127 durchnumeriert werden.
Der ASCII-Code stammt aus der Anfangszeit des Computers als jedes Bit Speicherplatz wertvoll war; für heutige Ansprüche reicht er nicht mehr, da man auch Sonderzeichen — und diese für zahlreiche Sprachen — darstellen möchte. Heute werden stattdessen zur Kodierung von Texten Erweiterungen des ASCII-Codes benutzt (wie Unicode, UTF-8, UTF-16 und so weiter).
Die Zeichen mit der Kodierung von 0 bis 32 sind nicht-sichtbare Steuerzeichen, ebenso 127; die sichtbaren Zeichen des ASCII-Codes zeigt folgende Tabelle:
33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 56 8 57 9 58 : 59 ; 60 < 61 = 62 > 63 ? 64 @ 65 A 66 B 67 C 68 D 69 E 70 F 71 G 72 H 73 I 74 J 75 K 76 L 77 M 78 N 79 O 80 P 81 Q 82 R 83 S 84 T 85 U 86 V 87 W 88 X 89 Y 90 Z 91 [ 92 \ 93 ] 94 ^ 95 _ 96 ` 97 a 98 b 99 c 100 d 101 e 102 f 103 g 104 h 105 i 106 j 107 k 108 l 109 m 110 n 111 o 112 p 113 q 114 r 115 s 116 t 117 u 118 v 119 w 120 x 121 y 122 z 123 { 124 | 125 } 126 ~
Gleitkommazahlen
Für Zahlen mit Nachkommastellen — hier werden sie als Gleitkommazahlen bezeichnet — verwendet man in der Mathematik oder in den Naturwissenschaften keine strengen Regeln, wie sie dargestellt werden müssen. Es gibt nur einige lockere Konventionen, die sich an die Regeln anlehnen, die Taschenrechner üblicherweise anwenden:
- solange die Zahlen überschaubar sind, vermeidet man Zehnerpotenzen, etwa: der Nullpunkt der Celsius-Skala liegt bei 273,15 K
- erst bei schwer lesbaren Zahlen verwendet man Zehnerpotenzen, etwa: die Lichtgeschwindigkeit beträgt 3,0 * 108 m/s
- es werden nur soviele Nachkommastellen angegeben wie messtechnisch vertretbar ist (diese Regel kennt ein Taschenrechner natürlich nicht), etwa die Raumtemperatur beträgt 21,5 °C (welches Thermometer kann schon genauer messen?).
Im Computer werden Gleitkommazahlen normiert dargestellt — intern wieder mit Dualzahlen; damit ist gemeint, dass jede Zahl aus vier Bestandteilen besteht:
- Vorzeichen
- Mantisse (eine Zahl zwischen 0 und 1)
- Vorzeichen des Exponenten
- Betrag des Exponenten
Bildet man aus obigen Dezimalzahlen die normierte Darstellung, so lauten etwa:
- der Nullpunkt der Celsius-Skala liegt bei +0,27315 * 103 K
- der absolute Nullpunkt liegt bei -0,27315 * 103 °C
- die Lichtgeschwindigkeit beträgt +0,3 * 109 m/s
- die Raumtemperatur beträgt +0,215 * 102 °C.
Wie oben gezeigt wurde, gibt es für Gleitkommazahlen in jeder Programmiersprache unterschiedliche Datentypen (etwa float und double in C++). Diese unterscheiden sich darin, wieviele Bits für Mantisse und Exponent vorgesehen sind; das Vorzeichen erhält immer ein Bit.
Damit ist aber auch klar, dass die Datentypen Zahlen
- nur in gewissen Intervallen darstellen können (abhängig von der Anzahl an Bits, die für den Exponenten vorgesehen sind) und
- die Genauigkeit der Darstellung variiert (abhängig von der Anzahl der Bits für die Mantisse).
Aufgabe:
Die folgende Abbildung zeigt eine Möglichkeit, wie sich Vorzeichen, Mantisse und Exponent den Speicherplatz eines Bytes teilen können.
Welche Zahlen können dargestellt werden, wenn obige normierte Darstellung vorausgesetzt wird?
Die Darstellung lässt sich sogar noch verbessern:
- In der Mantisse ist ein Bit redundant; es ist also möglich, ein Bit mehr für die Genauigkeit zu nutzen. Welche redundante Information ist hier angesprochen?
- Das Vorzeichen-Bit des Exponenten lässt sich einsparen. Wie kann das geschehen?