Lekce 3 - Reprezentace čísel v počítači
V minulé lekci, Přenos bitů aneb od pantáty vedou dráty..., jsme se podívali na to, jak se přenáší bity
a bajty. Nyní jsme v situaci, kdy už jsme nějak dostali bity odněkud někam
ve formě datového proudu. Dokonce jsme si i určili, že data nasekáme na
bloky. Pořád to jsou ale jen nuly a jedničky. A pokud s nimi chceme něco
dělat, musíme se se rozhodnout, jak převedeme tuto sekvenci na čísla. Pokud
se vám až doteď zdálo, že se tu bavíme pouze o teoretických věcech, tak
dnes nabudou konkrétní podobu. Ukážeme si, jaké jsou s čísly a jejich
převody problémy
Reprezentace celých čísel
Začneme celými čísly, která jsou samozřejmě pro ukládání nejjednodušší.
Přirozená reprezentace čísel (unsigned)
Mějme čísla, která pro jednoduchost posíláme po 1 bajtu = 8 bitů, tedy
8 pozic pro číslici 0
nebo 1
. Hodnota
0000000 = 0
, 11111111 = 255
. Tato reprezentace je bez
problémů. Vlastně, malý zádrhel tu je. Jak říci, že chceme číslo
-1
? Nemáme žádný znak pro -
, posílají se pouze
0
a 1
. Proto se tato reprezentace nazývá
unsigned = bezznaménková. Záporná čísla v praxi ale
bohužel často potřebujeme reprezentovat.
Bias (posun)
Tak dobře, chceme i záporná čísla, tak si můžeme rozsah posunout.
Kladnou část zkrátíme na polovinu a tak dokážeme uložit hodnoty od
-128
do 127
namísto od původních 0
až
255
. Určilo se, že nula bude kladná, takže je poměr +/-
zachován. Ke skutečné hodnotě se před uložením vždy přičte
128
a při načtení zas odečte.
Ukažme si několik příkladů, jak různá čísla uložíme:
-128 = 0, -127 = 1, ... , -1 = 127, 0 = 128, 1 = 129, ..., 127 = 255
Při přepsání do binárního zápisu tento posun vypadá takto:
-128 = 00000000, -127 = 00000001, ..., -1 = 01111111, 0 = 10000000, 1 = 10000001, ..., 127 = 11111111
Tato reprezentace je poměrně využívaná, má ale jeden háček. Když se
podíváte, tak 0
není uložena jako 00000000
. A to
je docela divné, úplně proti přirozenému vnímání
Znaménkový bit
Zkusme záporné číslo reprezentovat ještě jinak. Řekněme, že si
vyhradíme nejvyšší bit na tzv. sign bit (znaménkový bit). 1
znamená záporné, 0
znamená kladné. Pro reprezentaci čísla se
tedy dostáváme již jen na 7 číslic a tudíž na maximální hodnotu
127
. Máme opět možnost reprezentovat -128
až
127
. Vlastně ne tak docela. Náš rozsah je nyní od
-127
do 127
, ukážeme si proč:
-127 = 11111111, ..., -1 = 1000001, 0 = 00000000, 0 = 10000000, 1 = 00000001, ..., 127 = 01111111
Všimli jste si? První moucha této reprezentace je, že máme
+0
a -0
. To nám ubralo to jedno záporné číslo,
které můžeme reprezentovat. Navíc je to neintuitivní. Čas od času nuly
porovnáváme. +0 ?= -0
...? Rovnají se, nebo ne? Má to také pár
dalších ošklivých vlastností. V bitovém zápisu je
-127 > -126
. No, ale nemůžeme chtít všechno. Ten nápad je
ale docela dobrý, nešlo by alespoň trochu tuto reprezentaci zlepšit?
Jedničkový doplněk
Jedničkový doplněk se snaží řešit problém, že jsou "zápornější" čísla binárně větší. Chceme-li tedy záporné číslo, použijme doplněk (anglicky ones' complement). Na takové číslo provedeme tzv. negaci a všechny bity obrátíme:
-127 = 10000000, -126 =10000001, ..., -1 = 11111110, 0 = 00000000, 0 = 11111111, 1 = 00000001, ..., 127 = 01111111
Už je to mnohem lepší, teď opravdu platí, že
-127 < -126
. Jen se nám opět zduplikovala 0
.
Navíc docela nehezky. Každopádně už jsme vyřešili jeden problém.
Nemůžeme vyřešit ještě jeden? No, když se tak hloupě ptám...
Dvojkový doplněk
Trochu zmatené jméno, ale jedničkový byl už použit. Představte si, že čísla budeme reprezentovat pomocí jedničkového doplňku a k tomu přičteme jedničku. Kladná čísla zůstanou stejně kladnými a záporná čísla budou mít přičtenou jedničku.
-128 = 10000000, -127 = 10000001, -126 =10000010, ..., -1 = 11111111, 0 = 00000000, 1 = 00000001, ..., 127 = 01111111
Pro ilustraci přikládám obrázek:

Vualá, 2 nuly zmizely a nakonec jsme mohli reprezentovat i hodnotu
-128
. Zde to teoreticky trochu "hapruje", neboť nemůžete
reprezentovat 128
, abyste si sami provedli tuto operaci, ale na
jiných číslech to funguje. Navíc máme pořád sign bit zachovaný.
Využití jednotlivých reprezentací v praxi
Kde se která reprezentace používá? Sem tam se používá unsigned, poměrně často Bias a nejčastější je dvojkový doplněk, zvlášť u desetinných čísel, o tom však až za chvíli.
Přetečení
Jen ještě poznámečka, než se dostaneme k tomu, že "chceme více".
Nabízí se otázka, co se stane, když k 127
přičteme
1
. No, 0111111 + 00000001 = 10000000 = -127
. Takové
pěkné, malé, nenápadné přetečení. Dávejte si na to pozor, je to
nenápadná chybka a nikdo vám nic neřekne. Pro počítač je to naprosto
validní instrukce a je možné, že jste to tak chtěli. Kontrolujte si své
typy, v dnešních pamětích nehraje úspora takovou roli, abyste museli
šetřit každý kus. Pište kód srozumitelně, přehledně a bezpečně. (Na
druhou stranu přeci jen není úplně rozumné ukládat věk člověka v longu
)

Reprezentace větších čísel
Co když chceme větší čísla než 127
? Mimochodem, tento
číselný typ existuje jako byte
. Můžeme přeci mít třeba typ
int
od -2147483648
do 2147483649
. Další
typy se výrazně liší u každé implementace jazyka. Java
např. unsigned nemá, ale v C# je možné mít i
uint
(unsigned int) v intervalu <0, 4294967295>
.
Existují i short
, long
, char
a další
typy. Ty ale nechme stranou.
Převádění mezi typy
Používejme dvojkový doplněk, který je nejběžnější a možná i díky těmto převodům se nejlépe osvědčil. Pro další převody budu předpokládat, že zvětšujeme či zmenšujeme rozsah dvakrát. Čistě z praktických důvodů, stránka má omezenou šířku. V závorkách jsou uvedeny příklady takovýchto konverzí.
Převod z menšího rozsahu čísla na větší (int => long)
Je-li číslo kladné nebo záporné, rozkopírujeme signed bit:
1B = 8b | 2B = 16b | desítkový zápis |
00000001 | 0000000000000001 | 1 => 1 |
10000001 | 1111111110000001 | -127 => -127 |
Převod z většího rozsahu čísla na menší (long => int)
Je-li číslo v rozsahu toho menšího, ať už kladné, nebo záporné, není problém. Je-li však číslo ve větším rozsahu než menší z nich, máme problém...
2B = 16b | 1B = 8b | desítkový zápis |
1111111110011001 | 10011001 | -147 => -147 |
0000000000000110 | 00000110 | 6 => 6 |
1110111110011001 | 10011001 | -4199 => -147 |
0001100000000000 | 00000000 | 6144 => 0 |
Pokud si nejste opravdu jistí, co máte v longu dosazené, pozor na převody!
Reprezentace reálných čísel
Když jsme se rozehřáli, přejděme rovnou na reprezentaci reálných
čísel. Musíme si totiž uvědomit, co vlastně chceme. Dělili jste někdy na
kalkulačce číslo 10 / 3 * 2
? Vyšlo vám většinou něco jako
6,66666666667
. Matematicky je to nesmysl, jenže počítač v sobě
nemá nic nekonečné. Cokoliv, co dělá, dělá s konečnou pamětí a
konečným časem. Prostě nemáme několik gigabajtů volného prostoru jen na
uložení (a to ještě nepřesného) výsledku. Přesto ale můžeme
reprezentovat desetinnáa čísla poměrně slušně. Možná vás napadlo
1100,01
= 12,25
. Čárku ale samozřejmě také
nemáme. Co s tím?
Pevná desetinná čárka
Můžeme si určit, že desetinná čárka bude tam a tam. Např., že 3
cifry budou vždy za desetinnou čárkou a tedy 5 cifer zbude na číslo. Pro
jednoduchost uvažujme unsigned typ (tedy nezáporné číslo). Takto bychom
zapsali např. čísla 00101,001 = 5,25
a 01101,001
=
13,5. (Desetinná čárka je v binárním zápisu samozřejmě jen pro nás pro
přehlednost).
Pro lepší zápis můžeme čísla ukládat jako mocniny dvojky (v
exponenciálním tvaru, tzv. mantisa), tedy jako 1,101001 * 2^3
.
Pořád se ale jedná o velmi podobný zápis jako jsou integery, jen mám v
nějakém protokolu zaznamenáno, jak desetinná čísla vypadají.
Sčítání funguje dobře, máme u sebe blízko podobně velká čísla a vše je v pořádku. Co když ale chceme počítat s čísly, aniž bychom předem znali jejich rozsah? Pokud počítáme pokojovou teplotu, můžeme si pevnou desetinnou čárku dovolit. Co ale když budeme chtít počítat finanční transakce a zrovna na potvoru se na bankovní aplikaci přihlásí Bill Gates a Jenda Podšívka z Horních Koblížků u Dubenic...
Plovoucí desetinná čárka
U plovoucí desetinné čárky si přímo řekneme, kolik bajtů se používá pro mantisu (číslo bez desetinné čárky) a exponent (čím násobíme mantisu). Pro 8 bitů se používá právě 1 sign bit, 2 bity exponent a 5 bitů mantisa. Zbytek zahodíme. Z toho plyne, že desetinná čísla nejsou moc přesná. Proč se tomu říká plovoucí? Aneb protože nemáme pevně zadrátováno, kde v tom čísle desetinná čárka bude.

Zde si můžeme všimnout ještě jedné zvláštní hodnoty,
NaN
. V desetinných číslech totiž jde dělit nulou... Ano, celý
vesmír se hroutí, ale ne tak docela. Prostě dostaneme výsledek "Not a
number", což je takové příjemné číslo. Nemůžete s ním dělat nic a
jakákoliv operace s ním vám vyhodí znovu NaN
. Pro ukázku máte
ještě jeden obrázek ukazující typy desetinných čísel. (float a
double):

Dokonce si můžeme dovolit první jedničku vůbec nepsat, čímž
zpřesníme zápis čísla. Každé číslo se skládá ze sign bitu, mantisy a
exponentu. Mantisa může být ve dvojkovém doplňku a v exponentu se
používá Bias (viz výše). Jelikož máme Bias, exponent může být i
záporný, což znamená reálná čísla od 0
do 1
či od -1
do 0
. Rozdíl jasněji popíše tabulka
níže:

Pozn.: Shared exponent tu má smysl, neboť všechny fixed point čísla mají "stejně hodnotný" první bit.
Pozor!
Když se podíváte na tabulku výše, tak plovoucí desetinná čárka je
očividně velmi nepřesná notace. Spoustu bitů zahazuje. Nikdy, opravdu nikdy
proto nepoužívejte na porovnávání float
či
double
čísel ==
. Pokud máte číslo a
a
číslo b
, tak dělejte něco jako
a – b < 0.0001
či něco podobného. Může se stát, že se
čísla jednoduše o kousek minou a podmínka neprojde, i když byste
očekávali, že ano. Pokud můžete, používejte celá čísla. A jestli z
toho máte v hlavě guláš, vězte, že nebudete první, ani poslední. Tak
hlavu vzhůru
V příští lekci, Babylonské zmatení kódování, se podíváme pro změnu na reprezentaci textu.