Zum Inhalt springen
Topos Uranos

Zusammenfassung

Eine Ganzzahl im Programm ist keine ganze Zahl der Mathematik: Sie ist eine endliche Folge von Bits, und mit nn Bits lassen sich genau 2n2^n verschiedene Werte darstellen. Diese Lektion untersucht diese Darstellung und ihre Folgen. Sie lehrt, eine ganze Zahl zur Basis 2 und zur Basis 16 zu schreiben und das Ergebnis mit den Formatangaben {:b} und {:x} zu prüfen; sie stellt die ganzzahligen Typen von C++ vor, ihre Größe, die man mit sizeof misst, und ihren Wertebereich, den man mit std::numeric_limits abfragt; sie erklärt, wie negative Zahlen mithilfe des Zweierkomplements dargestellt werden und warum der Wertebereich nicht symmetrisch ist; sie unterscheidet vorzeichenbehaftete von vorzeichenlosen Ganzzahlen und untersucht, was geschieht, wenn eine Operation den Wertebereich verlässt: Bei vorzeichenlosen legt der Standard das Ergebnis fest, nämlich Arithmetik modulo 2n2^n; bei vorzeichenbehafteten legt er es nicht fest, und das Programm verliert jede Garantie. Am Ende steht ein Kriterium, um den Typ jedes Datums zu wählen.

Lernziele

  1. Ganze Zahlen zwischen den Basen 10, 2 und 16 umrechnen und die Umrechnung mit den Formatangaben {:b} und {:x} prüfen.
  2. Den Wertebereich einer vorzeichenbehafteten und einer vorzeichenlosen Ganzzahl mit nn Bits berechnen und ihn mit std::numeric_limits und sizeof prüfen.
  3. Das Zweierkomplement erklären und die Darstellung einer kleinen negativen Zahl mit acht Bits aufschreiben.
  4. Den vorzeichenbehafteten Überlauf, der undefiniertes Verhalten ist, vom vorzeichenlosen Überlauf unterscheiden, der Arithmetik modulo 2n2^n ist, und das Ergebnis des zweiten vorhersagen.
  5. Den passenden ganzzahligen Typ für ein Datum nach seinem voraussichtlichen Wertebereich wählen und dabei die Typen fester Breite aus <cstdint> verwenden, wenn die Breite wichtig ist.

Stellenwertsysteme

Die Lektion Variablen und Speicher hat die Variable als Speicherzelle mit Namen, Typ und Wert vorgestellt. Nun speichert der Speicher keine Dezimalziffern: Er speichert Bits, von denen jedes 0 oder 1 ist, zusammengefasst zu Bytes aus acht Bits (der Standard verlangt mindestens acht; alle heutigen Systeme verwenden genau acht). Um zu wissen, welche Werte eine ganzzahlige Variable annehmen kann, muss man also wissen, wie eine Zahl mit nur zwei Ziffern geschrieben wird, und genau das leistet ein Stellenwertsystem zur Basis 2.

In einem Stellenwertsystem zur Basis bb zählt jede Ziffer nach der Stelle, die sie einnimmt: Die letzte wird mit b0=1b^0 = 1 multipliziert, die vorletzte mit b1b^1, die davor mit b2b^2, und so fort. Zur Basis 10 bedeutet 20262026 so viel wie 2⋅103+0⋅102+2⋅101+6⋅1002 \cdot 10^3 + 0 \cdot 10^2 + 2 \cdot 10^1 + 6 \cdot 10^0. Zur Basis 2 sind die Ziffern 0 und 1, und die Stellenwerte (Gewichte) sind die Zweierpotenzen: 1,2,4,8,16,32,64,128,…1, 2, 4, 8, 16, 32, 64, 128, \ldots Daraus folgt die Regel für den Weg vom Binär- zum Dezimalsystem: Man addiert die Stellenwerte der Stellen, an denen eine 1 steht.

BeispielVom Binär- zum Dezimalsystem

Die Abbildung liest das Bitmuster 00101101 aus acht Bits. Über jeder Zelle steht ihr Stellenwert; die Einsen leuchten nacheinander auf, von links nach rechts, und der Wert sammelt ihre Stellenwerte.

Beweis

  1. Acht Zellen, alle aus: Der Wert ist 0. Die Stellenwerte sind, von rechts nach links, 1,2,4,…,1281, 2, 4, \ldots, 128.

  2. Die erste Eins steht an der Stelle mit Stellenwert 32: Der angesammelte Wert ist 32.

  3. Die nächste an der mit Stellenwert 8: 32+8=4032 + 8 = 40.

  4. Die nächste an der mit Stellenwert 4: 40+4=4440 + 4 = 44.

  5. Die letzte an der mit Stellenwert 1: 44+1=4544 + 1 = 45. Das Muster 00101101 stellt die Zahl 45 dar.

  6. In Vierergruppen zusammengefasst, ergeben die Bits die Hexadezimalziffern: 0010 ist 2 und 1101 ist 13, was zur Basis 16 als d geschrieben wird. Folglich wird 45 zur Basis 16 als 2d geschrieben.

Der umgekehrte Weg, vom Dezimal- zum Binärsystem, führt über fortgesetzte Division: Man teilt die Zahl durch 2, notiert den Rest (0 oder 1) und wiederholt das mit dem Quotienten, bis man bei 0 ankommt; die Reste, vom letzten bis zum ersten gelesen, sind die Binärziffern. Für 45: 45=2⋅22+145 = 2 \cdot 22 + 1, 22=2⋅11+022 = 2 \cdot 11 + 0, 11=2⋅5+111 = 2 \cdot 5 + 1, 5=2⋅2+15 = 2 \cdot 2 + 1, 2=2⋅1+02 = 2 \cdot 1 + 0, 1=2⋅0+11 = 2 \cdot 0 + 1; rückwärts gelesen ergeben die Reste 101101, und das ist mit zwei Nullen davor das Muster der Abbildung. Der Grund ist, dass jeder Rest die Einerziffer der gerade geteilten Zahl ist und jeder Quotient diese Zahl, um eine Stelle nach rechts verschoben. Dasselbe Verfahren gilt für jede Basis: Für die Basis 16 teilt man durch 16, und die Reste von 10 bis 15 werden mit den Buchstaben a bis f geschrieben.

Die Basis 16, das Hexadezimalsystem, gibt es aus Bequemlichkeit: Da 16=2416 = 2^4 gilt, entspricht jede Hexadezimalziffer genau vier Bits, und ein Byte wird mit zwei Ziffern geschrieben. Eine Zahl mit 32 Bits braucht binär zweiunddreißig Ziffern, die sich schlecht lesen, und hexadezimal nur acht, die sich ohne Rechnung, Ziffer für Ziffer, ins Binärsystem übertragen lassen. Deshalb lesen Programmierende den Speicher hexadezimal. Die mathematische Grundlage der Stellenwertsysteme wird im Kurs Algebra und Geometrie I behandelt, in der Lektion „Stellenwertsysteme“.

C++ erlaubt ganze Zahlen, die direkt in diesen Basen geschrieben sind: Ein Literal, das mit 0b beginnt, ist binär, und eines, das mit 0x beginnt, hexadezimal. In jedes Literal kann man einen Apostroph als Zifferntrennzeichen setzen, den der Compiler ignoriert und für den die Lesenden dankbar sind: 0b0010'1101, 1'000'000. In der Ausgabe schreibt die Formatangabe {:b} eine ganze Zahl binär, {:x} hexadezimal mit Kleinbuchstaben und {:X} mit Großbuchstaben; das Zeichen # stellt das Präfix voran, und eine Breite mit vorangestellter Null füllt links mit Nullen auf.

BeispielEine Zahl, drei Schreibweisen
C++-Programm
#include <print>​int main(){    const int decimal{45};    const int binary{0b0010'1101};    const int hexadecimal{0x2d};    std::println("{} {} {}", decimal, binary, hexadecimal);    std::println("binär: {:b}", decimal);    std::println("acht Bits: {:08b}", decimal);    std::println("hexadezimal: {:x} {:X} {:#x}", decimal, decimal, decimal);}
Ausgabe
45 45 45binär: 101101acht Bits: 00101101hexadezimal: 2d 2D 0x2d

Die drei Variablen speichern denselben Wert, in drei Basen geschrieben, und die erste Zeile der Ausgabe zeigt es: Die Basis ist eine Eigenschaft der Schreibweise, nicht der Zahl. Der Speicher enthält in allen drei Fällen dasselbe Bitmuster. Die Angabe {:b} lässt die führenden Nullen weg; {:08b} ergänzt auf acht Ziffern.