|
Serwis Edukacyjny w I-LO w Tarnowie
Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy Wałaszek |
©2026 mgr Jerzy Wałaszek
|
| SPIS TREŚCI REMANENT |
|
| Podrozdziały |
Do obliczeń naukowych potrzebne są liczby rzeczywiste, a opisane wcześniej kody pozwalają zapisywać tylko liczby całkowite. Liczby ułamkowe w kodzie dwójkowym można wprowadzać przez rozszerzenie zapisu w prawą stronę. Umówmy się, że pozycje o numerach 0, 1, 2,... oznaczają wagi całkowite o wartości 2numer_pozycji. Tak właśnie są zdefiniowane kody NBC i U2 (w U2 najstarszy bit ma zawsze wagę ujemną). Numerowanie pozycji nie musimy zakończyć na pozycji 0. Możemy pójść dalej w prawo, nadając pozycjom numery ujemne.
Na przykład umówmy się, że wagi pozycji są dalej potęgami liczby 2 o wykładniku równym numerowi pozycji:
| Waga | ... | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | ... |
| Bit | ... | b2 | b1 | b0 | b-1 | b-2 | b-3 | ... |
| Pozycja | ... | 2 | 1 | 0 | -1 | -2 | -3 | ... |
Jeśli wyliczymy wagi pozycji, otrzymamy:
| Waga | ... | 4 | 2 | 1 | 1/2 | 1/4 | 1/8 | ... |
| Bit | ... | b2 | b1 | b0 | b-1 | b-2 | b-3 | ... |
| Pozycja | ... | 2 | 1 | 0 | -1 | -2 | -3 | ... |
Zwróć uwagę, że wagi pozycji ujemnych są ułamkowe. Wartość liczby obliczamy tak samo jak przy poznanych wcześniej kodach NBC i U2: sumujemy wagi pozycji, na których występują bity o stanie 1.
Zobaczmy jak to działa na prostym przykładzie. Załóżmy, że mamy kod 8-bitowy. Ustalamy położenie przecinka w środku słowa kodowego. Jest to położenie umowne, ponieważ w kodzie możemy umieszczać tylko bity 0 lub 1. Mamy słowo kodowe 1110101. Jaką ma ono wartość w tym kodzie? Skoro przecinek jest w środku słowa, to mamy 4 bity całkowite oraz 4 bity ułamkowe:
Bity całkowite mają wartość:
| 11102 = 8 + 4 + 2 = 14 |
Bity ułamkowe mają wartość (sumujemy wagi pozycji z bitem 1):
| ,01012 = 1/4 + 1/16 + 4/16 + 1/16 = 5/16 |
Łączymy wyniki i otrzymujemy wartość liczby przedstawionej tym kodem:
| 1110,01012 = 14 5/16 = 14,0315 |
Jak widzisz, liczba nie jest całkowita.
Jak policzyć zakres wartości liczb w takim kodzie? Bardzo prosto. Część całkowita jest zwykłą liczbą NBC (część ułamkowa też, lecz z uwagi na wagi ułamkowe, potraktujmy ją osobno). Zakres części całkowitej: od 0 (00002) do 15 (11112). Zakres części ułamkowej: od 0 (,00002) do 15/16 (,11112). Sumujemy odpowiednio oba zakresy i otrzymujemy, że kod ten może przedstawiać liczby od 0 do 1515/16 z rozdzielczością 1/16.
Postaraj się udowodnić, że w kodzie n-bitowym z m bitami ułamkowymi (n > m) zakres liczb wynosi od 0 do 2n - m - 1 = (2m - 1) / 2m.
Tego typu kody noszą nazwę kodów stałoprzecinkowych (ang. fixed-point codes). W obliczeniach wykorzystuje się inny rodzaj kodów, zwanych kodami zmiennoprzecinkowymi (floating-point codes). Z zapisem zmiennoprzecinkowym mogłeś się już spotkać na różnych przedmiotach ścisłych, gdzie występowała potrzeba zapisu bardzo dużych lub bardzo małych liczb:
| 3,14 · 1050
=
3140000000000000000000000000000000000000000000000000 6,53 · 10-30 = 0,00000000000000000000000000000653 |
Zaletą takiego zapisu jest tutaj większa czytelność. Jednak przyjrzyjmy się bliżej tak zapisanej liczbie. Składa się ona z 3 osobnych części:
| m · pc |
| m – mantysa p – podstawa c – cecha |
Jeśli umówimy się, że podstawa p jest stała, to nie musimy ją zapisywać. Zostanie nam para liczb (c,m). Cecha c jest liczbą całkowitą. Mantysa m jest liczbą stałoprzecinkową. Możemy teraz skonstruować prosty binarny kod zmiennoprzecinkowy o długości 8 bitów:
| p = 2, podstawa jest zawsze stała i nie
zapamiętujemy jej w kodzie FP. c – 4 bitowa liczba całkowita U2 m – 4 bitowa liczba stałoprzecinkowa U2 |
Podział bitów w naszym słowie kodowym FP będzie następujący:
| cecha | mantysa | ||||||
| -8 | 4 | 2 | 1 | -1 | 1/2 | 1/4 | 1/8 |
| c3 | c2 | c1 | c0 | m0 | m-1 | m-2 | m-3 |
Wartość liczby w tym kodzie jest równa m · 2c. Policzmy wartości kilku kodów.
| 00000010(FP) = ? |
Rozdzielamy bity kodu FP na bity cechy i bity mantysy:
| c = 0000(U2)
= 0 m = 0,010(U2) = 1/4 p = 2 0000010(FP) = m · pc = 1/4 · 20 = 1/4 · 1 = 1/4 = 0,25 |
| 01101110(FP)
= ? c = 0110(U2) = 6 m = 1,110(U2) = -1 + 1/2 + 1/4 = -1/4 p = 2 01101110(FP) = m · pc = -1/4 · 26 = -1/4 · 64 = -16 |
| 10001111(FP)
= ? c = 1000(U2) = -8 m = 1,111(U2) = -1 + 1/2 + 1/4 + 1/8 = -1/8 p = 2 10001111(FP) = m · pc = -1/8 · 2-8 = -1/8 · 1/256 = -1/2048 = -0,00048828125 |
| 01110111(FP)
= ? c = 0111(U2) = 4 + 2 + 1 = 7 m = 0,111(U2) = 1/2 + 1/4 + 1/8 = 4/8 + 2/8 + 1/8 = 7/8 p = 2 01110111(FP) = 7/8 · 27 = 7/8 · 128 = 112 |
Jak widzisz, kod FP potrafi zapisywać liczby całkowite i ułamkowe dodatnie i ujemne. Obliczenie wartości kodu nie jest takie proste jak w kodach NBC i U2, które są częścią składową kodu FP.
Przejdźmy do własności liczb zmiennoprzecinkowych. Tutaj bardzo przyda się nasz przykładowy kod FP z uwagi na jego prostotę. Na początek policzmy zakres liczb FP w naszym kodzie.
Liczbę najmniejszą dostaniemy dla najmniejszej (najbardziej ujemnej) mantysy i największej cechy:
| Największa cecha: c = 0111(U2) =
7 Najmniejsza mantysa: m = 1,000(U2) = -1 Najmniejsza wartość: 01111000(FP) = -1 · 27 = -128 |
Największą liczbę otrzymamy dla największej cechy i największej mantysy:
| Największa cecha: c = 0111(U2) =
7 Największa mantysa: m = 0,111(U2) = 7/8 Największa wartość: 01110111(FP) = 7/8 · 27 = 7/8 · 128 = 112 |
Podsumowując, zakres wynosi od -128 do 112.
Zwróć uwagę, że wzór na granice zakresu jest następujący:
| min(m)· 2max(c)... max(m)· 2max(c) |
Ponieważ mantysa jest liczbą mniejszą od 1, to zakres zależy głównie od cechy. Im więcej bitów przeznaczymy na cechę, tym bardziej wzrośnie zakres liczb możliwych do przedstawienia w danym kodzie. Zapamiętaj to. Na co wpływają bity mantysy? Na precyzję liczby FP. Im więcej bitów ma mantysa, tym dokładniej można przedstawić określone wartości. Pokażemy to na przykładach
Załóżmy, że chcemy w naszym kodzie przedstawić liczbę 12. Jak to zrobić? Otóż musimy otrzymać wartość mantysy mniejszą od 1 i wartość cechy. Zapisujemy:
| 12 = 12 · 20
– mantysa za duża, dzielimy przez 2, a cechę
zwiększamy o 1. 12 = 6 · 21 – mantysa wciąż jest za duża, dzielimy ją przez 2 i zwiększamy cechę o 1. 12 = 3 · 22 – kontynuujemy podział. 12 = 3/2 · 23 – kontynuujemy. 12 = 3/4 · 24 – mamy mantysę mniejszą od 1. Zatrzymujemy się. |
Otrzymaliśmy:
| c = 4 = 0100(U2) m = 3/4 = 1/2 + 1/4 = 0,110(U2) |
Otrzymujemy:
| 12 = 01000110(FP) |
Liczbę 12 dało się przedstawić bez problemu. Przyjmijmy roboczo, że przez precyzję liczby binarnej będziemy rozumieli liczbę bitów od pierwszego bitu 1 do ostatniego bitu 1. Liczba 12 w kodzie NBC ma zapis 1100(NBC), posiada zatem precyzję 2 bitów. Mantysa naszego kodu ma precyzję 3 bitów, zatem liczba 12 mieści się w kodzie.
Weźmy teraz liczbę 9, której kod NBC ma postać 1001. Tym razem precyzja wynosi 4 bity. Czy da się tę liczbę zapisać w naszym kodzie? Operację dzielenia przez 2 możemy zastąpić prostą operacją przesuwania bitów w prawo. Zapiszmy liczbę 9 w kodzie stałoprzecinkowym z 3 bitami ułamkowymi i rozpocznijmy przesuwanie bitów w prawo, tak aby najstarszy bit trafił za przecinek, wtedy otrzymamy mantysę mniejszą od 1:
| mantysa | cecha |
| 1001,000 | 0000 |
| 100,100 | 0001 |
| 10,010 | 0010 |
| 1,001 | 0011 |
| 0,1001 | 0100 |
Zwróć uwagę, że w czwartym przesunięciu najmłodszy bit 1 wyszedł poza bity ułamkowe mantysy. Dlatego otrzymujemy:
| c = 0100(U2) = 4 m = 0,100(U2) = 1/2 01000100(FP) = 1/2 · 24 = 1/2 · 16 = 8 |
Z liczby 9 zrobiła nam się liczba 8. Dlaczego? Ponieważ liczba 9 wymaga 4 bitów precyzji, a mantysa udostępnia jedynie 3 bity. Następuje zatem utrata precyzji i wynikowa liczba obarczona jest błędem zaokrąglenia (ang. rounding error). Jest to typowa własność liczb FP. Zwiększając liczbę bitów mantysy, zwiększamy precyzję, a zatem dokładność liczb.
|
Zapamiętaj: Liczby zmiennoprzecinkowe są liczbami przybliżonymi. Dokładnie da się przedstawić tylko takie wartości, których precyzja bitowa mieści się w precyzji bitowej mantysy liczby zmiennoprzecinkowej. |
Istnieją pewne wartości, których nigdy nie da się przedstawić dokładnie w kodzie FP. Taką typową wartością jest np. 1/5. Dlaczego? Liczba FP jest sumą potęg liczby 2. Liczba 1/5 nie daje się przedstawić jako suma potęg liczby 2.

Dostajemy nieskończony ułamek binarny. Podobną własność w systemie dziesiętnym mają na przykład liczby:

Wykonując obliczenia, należy zwracać na to uwagę, co pokażemy w następnym podrozdziale.
W języku C++ dostępne są trzy typy dla danych zmiennoprzecinkowych (w Pythonie liczby zmiennoprzecinkowe są standardowo reprezentowane jako typ double - 64 bity):
| Typy zmiennoprzecinkowe | |||
| Typ | Rozmiar | Zakres | Precyzja |
| float | 32 bity – 4 bajty | Zakres ±3,4 · 1038 | 7...8 cyfr |
|---|---|---|---|
| double | 64 bity – 8 bajtów | Zakres ±1,8 · 10308 | 15...16 cyfr |
| long double | 80 bitów – 10 bajtów | Zakres ±1,1 · 104932 | 19...20cyfr |
Współczesne mikroprocesory wyposażone są w tzw. koprocesor arytmetyczny. Jest to wyspecjalizowany moduł, który wykonuje szybkie obliczenia na liczbach zmiennoprzecinkowych. Typ long double jest typem danych wykorzystywanych wewnętrznie przez koprocesor. Zapewnia on wysoką dokładność obliczeń. W mikroprocesorach kompatybilnych z Pentium typ long double ma długość 80 bitów, czyli 10 bajtów. Jednak na innych platformach typ ten może posiadać inny rozmiar.
Precyzja odnosi się tutaj do gwarantowanej liczby początkowych cyfr dziesiętnych liczby zmiennoprzecinkowej, które zostaną zapamiętane dokładnie. Przykładowo w typie float (dostępny, lecz obecnie niezalecany z uwagi na niską precyzję) precyzja obejmuje około 7 pierwszych cyfr liczby. Jeśli liczba posiada więcej cyfr, to dokładnie zostanie zapamiętane tylko 7... 8 pierwszych z nich.
Poniższy program demonstruje tę własność dla trzech typów danych:
C++// Liczby FP - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0025
//---------------------------
#include <iostream>
#include <windows.h>
#include <iomanip>
using namespace std;
int main()
{
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(0)
<< fixed;
cout <<
"Precyzja liczb "
"zmiennoprzecinkowych\n"
"---------------"
"--------------------\n";
long double x;
int i;
x = 1;
cout <<
"Cyfry "
"float "
"double "
"long double\n";
for(i = 1; i < 21; i++)
{
cout
<< setw(5) << i
<< setw(21) << (float)x
<< setw(21) << (double)x
<< setw(21) << x << endl;
x *= 10;
x += (i + 1) % 10;
}
cout << endl;
system("pause");
return 0;
}
|
| Wynik |
Precyzja liczb zmiennoprzecinkowych
-----------------------------------
Cyfry float double long double
1 1 1 1
2 12 12 12
3 123 123 123
4 1234 1234 1234
5 12345 12345 12345
6 123456 123456 123456
7 1234567 1234567 1234567
8 12345678 12345678 12345678
9 123456792 123456789 123456789
10 1234567936 1234567890 1234567890
11 12345678848 12345678901 12345678901
12 123456790528 123456789012 123456789012
13 1234567954432 1234567890123 1234567890123
14 12345679020032 12345678901234 12345678901234
15 123456788103168 123456789012345 123456789012345
16 1234567948140544 1234567890123456 1234567890123456
17 12345678407663616 12345678901234568 12345678901234567
18 123456790519087104 123456789012345680 123456789012345678
19 1234567939550609408 1234567890123456768 1234567890123456789
20 12345679395506094080 12345678901234567168 12345678901234567890
|
Typ long double jest najbardziej "precyzyjny", jednakże nie jest standardowy (na innych platformach może być taki sam jak typ double). Dlatego do zwykłych obliczeń stosujemy typ double, zwany typem o podwójnej precyzji (podwójnej w stosunku do oryginalnego typu float w C/C++).
Typ float był intensywnie używany w przeszłości, gdy komputery nie miały dużej pamięci i były wolne (starsze mikroprocesory nie posiadały koprocesora arytmetycznego, który był osobnym układem scalonym, dosyć drogim i nie zawsze instalowanym na płycie głównej komputera – wszystkie operacje zmiennoprzecinkowe realizował programowo mikroprocesor, co było dosyć czasochłonne). Dzisiaj lepiej go już nie stosować, standardem jest 64-bitowy typ double (w C++) i typ float (w Pythonie). Typ long double wyszedł z użycia i nie ma nawet wsparcia sprzętowego na niektórych platformach (np. architektura ARM).
Nie będziemy się zagłębiać w szczegóły techniczne wykonywania operacji na liczbach zmiennoprzecinkowych. Opiszemy jedynie ich podstawowe własności, które powinieneś znać, aby uniknąć pułapek w trakcie programowania.
Liczbę zmiennoprzecinkową możemy ogólnie zapisać w postaci wyrażenia m · 2c, gdzie m jest mantysą, c jest cechą. Załóżmy, że mamy dwie liczby zmiennoprzecinkowe a i b:

Dodanie tych dwóch liczb do siebie polega na znalezieniu mantysy i cechy wyniku dodawania:

W tym celu wyrównujemy cechy obu liczb:

Otrzymaną w ten sposób mantysę należy znormalizować, tzn. doprowadzić do wartości poprawnej dla danego kodu, np. w naszym przykładowym kodzie 8-bitowym mantysa jest liczbą mniejszą od 1 i większą lub równą -1. Mnożenie przez potęgi liczby 2 jest dokonywane za pomocą przesuwania bitów. Również normalizacja jest wykonywana za pomocą przesunięć. Gdy bity mantysy są przesuwane w lewo, cecha maleje o 1; gdy są przesuwane w prawo, cecha rośnie o 1. Zobaczmy jak wygląda to na przykładzie.
Mamy dodać dwie liczby 8 i 6. Najpierw znajdźmy ich kody FP:
| 8 = 1/2
· 24 c1 = 4 = 0100(U2) m1 = 1/2 = 0,100(U2) 8 = 01000100(FP) 6 = (1/2 + 1/4) · 23 c2 = 3 = 0011(U2) m2 = 1/2 + 1/4 = 0,110(U2) 6 = 00110110(FP) |
Wyrównujemy wykładniki. W tym celu mantysę m2 przesuwamy bitowo w prawo o 1 pozycję, a wykładnik c2 zwiększamy o 1:
| m2' = m2 / 2 =
1/4
+ 1/8 = 0,011(U2) c2' = c2 + 1 = 3 + 1 = 4 = 0100(U2) |
Teraz obie liczby mają ten sam wykładnik, możemy dodać mantysy i otrzymamy mantysę wyniku:
| m1+2 = m1+m2' = 1/2 + 1/4 + 1/8 = 7/8 = 0,111(U2) |
Mantysa mieści się we właściwym przedziale, więc liczby nie musimy normalizować. Otrzymujemy kod wynikowy:
| 01000111(FP) = 7/8
· 24 = 7/8
· 16 = 14 8 + 6 = 14 |
Wynik jest prawidłowy.
Inny przykład. Dodajemy 14 + 14:
| 14 = 01000111(FP)
(wykorzystujemy gotowy kod z poprzedniego przykładu)
c12 = 0100(U2) = 4 m12 = 0,111(U2) = 7/8 |
Ponieważ oba wykładniki są takie same, nie musimy ich wyrównywać. Sumujemy mantysy:
| m = m12 + m12
= 7/8
+ 7/8 = 14/8
= 1 6/8
= 01,110(U2) c = c12 = 4 = 0100(U2) |
Mantysa jest większa od 1, musimy ją znormalizować. Przesuwamy jej bity o 1 pozycję w prawo i zwiększamy wykładnik o 1:
| m' = 0,111(U2) = 7/8 c' = c + 1 = 5 = 0101(U2) |
Teraz mantysa jest mniejsza od 1, mamy gotowy kod:
| 01010111(FP) = 7/8
· 25 = 7/8
· 32 = 28 14 + 14 = 28 |
Wynik sumowania może być niepoprawny, jeśli nastąpi zaokrąglenie mantysy. Zsumujmy liczbę 8 i 3:
| 8 = 1/2
· 24 c1 = 4 = 0100(U2) m1 = 1/2 = 0,100U2 8 = 01000100(FP) 3 = 3/4 · 22 c2 = 2 = 0010(U2) m2 = 3/4 = 0,110(U2) 3 = 00100110(FP) |
Wyrównujemy wykładniki, mantysę m2 przesuwamy o 2 bity w prawo, a wykładnik c2 zwiększamy o 2:
| m2 = 0,110(U2) =
3/4 c2 = 0010(U2) = 2 m2' = 0,0011(U2) = 3/16 c2' = 0100(U2) = 4 |
Zwróć uwagę, iż po przesunięciu, najmłodszy bit mantysy m2' wyszedł poza bity mantysy standardowej, dlatego zaznaczyliśmy go tutaj na czerwono.
Wykładniki są wyrównane, dodajemy mantysy obu liczb:
| m = m1 + m2' = 1/2 + 3/16 = 11/16 = 0,1011(U2) |
Mantysa wynikowa jest mniejsza od 1, nie musimy normalizować wyniku. Jednak najmłodszy bit mantysy wynikowej musi zostać odrzucony, ponieważ nie mieści się w bitach mantysy kodu FP. Dokonujemy tzw. zaokrąglenia
| m = 0,101(U2) = 5/8 |
i otrzymujemy:
| 01000101(FP) = 5/8
· 24 = 5/8
· 16 = 10 8 + 3 = 10 |
Wynik jest niepoprawny, ponieważ liczba 11 wymaga 4 bitów precyzji (11 = 1011(NBC)) , a mantysa udostępnia jedynie 3 bity precyzji.
Przedstawiona tutaj sytuacja występuje również w działaniach na liczbach zmiennoprzecinkowych w języku C++ (jest to cecha wspólna wszystkich kodów zmiennoprzecinkowych).
Poniższy program zwiększa o 1 najpierw małą liczbę typu float, a później robi to samo z dużą liczbą typu float. Wyciągnij z tego wnioski:
C++// Liczby FP - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0026
//---------------------------
#include <iostream>
#include <windows.h>
#include <iomanip>
using namespace std;
int main()
{
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(0)
<< fixed;
cout << "Precyzja liczb FP\n"
"-----------------\n\n";
float x;
x = 999;
cout << "Mała liczba float\n"
"Przed : " << x;
x++;
cout << " Po : " << x
<< endl << endl;
x = 100000000;
cout << "Duża liczba float\n"
"Przed : " << x;
x++;
cout << " Po : " << x
<< endl << endl;
system("pause");
return 0;
}
|
| Wynik |
| Precyzja liczb zmiennoprzecinkowych ----------------------------------- Mała liczba float Przed : 999 Po : 1000 Duża liczba float Przed : 100000000 Po : 100000000 |
Python
(dodatek) # Liczby FP - precyzja
# (C)2026 mgr Jerzy Wałaszek
# Metody numeryczne 0026
print("Precyzja liczb FP\n"
"-----------------\n")
x = 999.0
print("Mała liczba float\n"
f"Przed : {x:.0f}")
x += 1
print(f" Po : {x:.0f}\n")
x = 10000000000000000.0
print("Duża liczba float\n"
f"Przed : {x:.0f}")
x += 1
print(f" Po : {x:.0f}\n")
input("Naciśnij Enter...")
|
Jak napisaliśmy w poprzednim rozdziale, niektóre wartości tworzą nieskończone ułamki binarne i nie dają się dokładnie zapisać w żadnym z typów danych zmiennoprzecinkowych. Taką wartością jest na przykład 0,1 (jedna dziesiąta). Przy sumowaniu tego typu wartości błędy zaokrągleń kumulują się i wynik może odbiegać od wartości dokładnej.
Poniższy program dodaje do zmiennej o początkowej wartości 0 dziesięć razy liczbę 0,1. Następnie odejmuje od tej zmiennej 1. Jeśli wyjdzie wartość 0 (tak wynika z arytmetyki) , to wyświetla napis "DOBRZE". Jeśli wynik odejmowania jest różny od zera, to wyświetla napis "ŹLE" i wypisuje wartość zmiennej z 20 cyframi po przecinku. Wyciągnij wnioski sam:
C++// Liczby FP - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0027
//---------------------------
#include <iostream>
#include <windows.h>
#include <iomanip>
using namespace std;
int main()
{
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(20)
<< fixed;
cout << "Precyzja liczb FP\n"
"-----------------\n\n";
double x = 0;
// Do zmiennej x dodajemy
// dziesięć razy liczbę 0.1.
for(int i = 0; i < 10; i++)
x += 0.1;
// W x powinna być wartość 1.
// Odejmujemy 1 od x.
// Powinniśmy otrzymać 0:
x -= 1;
if(x == 0)
cout << "DOBRZE";
else
cout << "ŹLE: " << x;
cout << endl << endl;
system("pause");
return 0;
}
|
| Wynik |
| Precyzja liczb FP ----------------- ŹLE: -0.00000000000000011102 |
Python
(dodatek) # Liczby FP - precyzja
# (C)2019 mgr Jerzy Wałaszek
# Metody numeryczne 0027
#---------------------------
print("Precyzja liczb FP\n"
"-----------------\n")
x = 0.0
# Do zmiennej x dodajemy
# dziesięć razy liczbę 0.1.
for i in range(10):
x += 0.1
# W x powinna być wartość 1.
# Odejmujemy 1 od x.
# Powinniśmy otrzymać 0:
x -= 1
if x == 0:
print("DOBRZE")
else:
print(f"ŹLE: {x:.20f}\n")
input("Naciśnij Enter...")
|
Błąd jest bardzo mały, lecz komputer porównuje bity i różnica nawet jednego bitu powoduje, iż wyświetli się napis "ŹLE". Wynika z tego, iż liczb zmiennoprzecinkowych nie powinno się porównywać z liczbami dokładnymi, ponieważ z powodu błędów zaokrągleń wynik może być obarczony pewnym błędem. Jak zatem sprawdzić, czy liczba zmiennoprzecinkowa ma pożądaną wartość? Musisz zdecydować, z jaką dokładnością chcesz przeprowadzić to sprawdzenie. Należy zatem oszacować możliwy błąd, Nie jest to zadanie proste i dokładne metody poznasz dopiero na studiach. Tutaj możemy podejść do problemu w sposób prosty, lecz nieprofesjonalny:
Liczbę zmiennoprzecinkową LFP możemy potraktować jako sumę liczby dokładnej Ld oraz błędu zaokrąglenia Le:
Wartość Le zależy od dostępnej precyzji oraz od wartości samej liczby dokładnej Ld. Dlatego zwykle posługujemy się pojęciem błędu względnego:

Ponieważ błąd może być dodatni lub ujemny, to stosujemy wartość bezwzględną tego błędu:

Po podstawieniach mamy:

Jeśli dana reprezentacja FP posiada precyzję n cyfr, to znaczy, że pierwsze n cyfr liczby jest dokładne. Począwszy od cyfry (n + 1) dokładność nie jest gwarantowana i mogą pojawiać się błędy. Jeśli nasza liczba Ld = 0,1, a dokładność wynosi 15 cyfr, to w najgorszym przypadku |Le| = 0,000000000000000999... Dla prostoty przyjmijmy |Le| = 0,000000000000001. Czyli błąd względny ma wartość:

Jeśli zsumujemy 10 razy liczbę FP, to

Po podstawieniach otrzymamy:

Aby sprawdzić, czy suma osiągnęła założoną wartość x(FP) nie porównujemy jej z tą wartością bezpośrednio, lecz sprawdzamy, czy wartość bezwzględna różnicy sumy i wartości oczekiwanej jest równa lub mniejsza od oszacowanego błędu:
Jeśli tak, to przyjmujemy, że suma ma w przybliżeniu wartość oczekiwaną.
Wynika z tego następujący wniosek:
|
Porównanie liczby x(FP) z inną wartością y(FP):
|
Błąd względny zależy od precyzji obliczeń. W przybliżeniu możesz przyjąć (przypadek pesymistyczny) , że:

Gdzie p oznacza liczbę cyfr dokładnych w danej precyzji.
Zmieniamy nasz program następująco:
C++// Liczby FP - precyzja
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0028
//---------------------------
#include <iostream>
#include <windows.h>
#include <cmath>
#include <iomanip>
using namespace std;
// Wartość spodziewanego błędu
const double EPS = 0.00000000001;
int main()
{
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(20)
<< fixed;
cout << "Precyzja liczb FP\n"
"-----------------\n";
double x = 0;
// Do zmiennej x dodajemy
// dziesięć razy liczbę 0,1.
for(int i = 0; i < 10; i++)
x += 0.1;
// W x powinna być wartość 1.
// Odejmujemy 1 od x.
// Powinniśmy otrzymać 0:
x -= 1;
// Sprawdzamy, czy wynik
// wpada w otoczenie zera
// o promieniu EPS
if(fabs(x) <= EPS)
cout << "DOBRZE";
else
cout << "ŹLE: " << x;
cout << endl << endl;
system("pause");
return 0;
}
|
| Wynik |
| Precyzja liczb FP ----------------- DOBRZE |
Python
(dodatek) # Liczby FP - precyzja
# (C)2026 mgr Jerzy Wałaszek
# Metody numeryczne 0028
#---------------------------
import math
# Wartosc spodziewanego bledu
EPS = 0.00000000001
print("Precyzja liczb FP")
print("-----------------")
x = 0.0
# Do zmiennej x dodajemy
# dziesiec razy liczbe 0.1.
for i in range(10):
x += 0.1
# W x powinna byc wartosc 1.
# Odejmujemy 1 od x.
# Powinnismy otrzymac 0:
x -= 1.0
# Sprawdzamy, czy wynik
# wpada w otoczenie zera
# o promieniu EPS
if math.fabs(x) <= EPS:
print("DOBRZE")
else:
print(f"ZLE: {x:.20f}")
print("\n")
input("Nacisnij Enter...")
|
Z tej zasady będziemy korzystali zawsze przy porównywaniu liczb zmiennoprzecinkowych. Zapamiętaj ją.
Odejmowanie jest dodawaniem liczby przeciwnej:
| a(FP) - b(FP) = a(FP) + (-b(FP)) |
Jeśli odejmujesz liczby prawie równe, to wynik może być bardzo niedokładny. Dlaczego? Wyjaśnijmy to na przykładzie. Załóżmy, że mamy precyzję 3 cyfr, tzn. 3 pierwsze cyfry liczby są dokładne, reszta już nie. Weźmy teraz dwie liczby 6-cio cyfrowe:
| L1 = 123???
(znakiem ? oznaczyliśmy cyfry poza naszą precyzją) L2 = 122??? |
Odejmujemy:
| L1 - L2 = 123??? - 122??? = 0,??? |
Precyzja wyniku spadła do 1 cyfry, ponieważ pozostałe są niepewne, a w niekorzystnych okolicznościach nawet ta pierwsza cyfra nie jest pewna.
|
Wniosek: Staraj się unikać odejmowania od siebie liczb prawie równych, ponieważ wynik może być niedokładny. |
Zasada mnożenia liczb zmiennoprzecinkowych jest następująca:
Mamy 2 liczby zmiennoprzecinkowe:

Tworzymy ich iloczyn:

Mantysa wynikowa jest iloczynem mantys obu mnożonych liczb. Cecha wynikowa jest sumą cech mnożonych liczb. Po operacji mnożenia mantysa wynikowa musi zostać znormalizowana, czyli sprowadzona do odpowiedniego zakresu za pomocą przesuwów bitowych, jak przy dodawaniu.
Zobaczmy na przykładzie w naszym prostym systemie FP jak wygląda mnożenie dwóch liczb zmiennoprzecinkowych.
| L1 = 8 = 1/2
· 24 m1 = 1/2 c1 = 4 L1 = 01000100(FP) L2 = 6 = (1/2 + 1/4) · 23 m2 = 1/2 = 1/4 c2 = 3 L2 = 00110110(FP) |
Mnożymy mantysy:
| m = m1 · m2 = 1/2 · (1/2 + 1/4) = 1/2 · 3/4 = 3/8 = 0,011(U2) |
Dodajemy wykładniki
| c = c1 + c2 = 4 + 3 = 7 = 0111(U2) |
Mantysę normalizujemy przesuwając jej bity o jedna pozycję w lewo (odpowiada to pomnożeniu liczby przez 2) i zmniejszając o 1 cechę (odpowiada to podzieleniu liczby przez 2, w sumie dostajemy tę samą liczbę):
| c = 7 = 0111(U2)
m = 3/8
= 0,011(U2) c = 6 = 0110(U2) m = 3/4 = 0,110(U2) |
I otrzymujemy kod wynikowy:
| 01100110(FP) = 3/4 · 26 = 3/4 · 64 = 48 = 8 · 6 |
Dla mnożenia dwóch liczb zmiennoprzecinkowych mamy:

Jeśli precyzja liczby jest duża, to kwadrat błędu jest bardzo mały i można go pominąć:

Przy mnożeniu 3 liczb zmiennoprzecinkowych otrzymujemy:

Możemy to kontynuować dla następnych iloczynów i ostatecznie otrzymamy, że błąd mnożenia n liczb zmiennoprzecinkowych jest w przybliżeniu równy:

Dzielenie jest odwrotnością mnożenia:

Obowiązują tutaj te same zasady, co przy mnożeniu.
|
Zapamiętaj:
|
Liczby pseudolosowe całkowite opisane zostały w poprzednim rozdziale.
Liczbę pseudolosową rzeczywistą możemy otrzymać z wbudowanego generatora pseudolosowego dzieląc odpowiednio jego wynik. Liczby z przedziału obustronnie domkniętego <0,1> daje nam poniższe wyrażenie:
| rand( ) / (double)RAND_MAX |
Rzutowanie na typ double jest konieczne, aby kompilator zastosował dzielenie zmiennoprzecinkowe. W przeciwnym razie dzielenie będzie całkowitoliczbowe z wynikiem 0 (bardzo często, rand( ) < RAND_MAX) lub 1 (bardzo rzadko, rand( ) = RAND_MAX).
W Pythonie korzystamy z biblioteki random. Funkcja random.random( ) zwraca liczbę z przedziału lewostronnie domkniętego <0;1). Aby otrzymać przedział obustronnie domknięty, używamy funkcji random.uniform(0.0,1.0).
Poniższy program generuje 10 rzeczywistych liczb pseudolosowych:
// Liczby FP
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0029
//---------------------------
#include <iostream>
#include <windows.h>
#include <cstdlib>
#include <ctime>
#include <iomanip>
using namespace std;
int main()
{
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(10)
<< fixed;
cout <<
"Pseudolosowe liczby FP "
"w przedziale <0,1>\n"
"-----------------------"
"------------------\n\n";
// Inicjujemy generator
// liczb pseudolosowych
srand(time(nullptr));
double x;
// Generujemy 10 liczb pseudolosowych
for(int i = 0; i < 10; i++)
{
x = rand() / (double)RAND_MAX;
cout << x << endl;
}
cout << endl;
system("pause");
return 0;
}
|
| Wynik |
| Pseudolosowe liczby FP w
przedziale <0,1> ----------------------------------------- 0.1136559856 0.6520596743 0.5023364315 0.1906467162 0.7117840909 0.2913933536 0.1637069805 0.8938939664 0.1087105434 0.6524624379 |
Python
(dodatek) # Liczby FP
# (C)2026 mgr Jerzy Wałaszek
# Metody numeryczne 0029
#---------------------------
import random
print("Pseudolosowe liczby FP "
"w przedziale <0,1>\n"
"-----------------------"
"------------------\n")
# Generujemy 10 liczb FP
# z przedziału <0,1>
for _ in range(10):
print(
f"{random.uniform(0.0, 1.0):.10f}")
print()
input("Nacisnij Enter...")
|
Granice przedziału generowanych liczb możemy modyfikować:
(0,1> – otwarty lewostronnie, domknięty
prawostronnie:C++ : (rand() + 1) / (double)(RAND_MAX
+ 1)(0,1)– otwarty obustronnie:C++ : (rand() + 1) / (double)(RAND_MAX
+ 2)<0,1)– domknięty lewostronnie, otwarty
prawostronnie:C++ : rand() / (double)(RAND_MAX
+ 1) |
Mając przedział 0...1 możemy go dowolnie rozszerzać, np na przedział a...b:
<a;b> – domknięty obustronnie:C++ : a + (b - a) * (rand() / (double)RAND_MAX) |
Poniższy program generuje liczby w zakresie od a do b.
|
C++ // Liczby zmiennoprzecinkowe
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0030
//---------------------------
#include <iostream>
#include <windows.h>
#include <cstdlib>
#include <ctime>
#include <iomanip>
using namespace std;
int main()
{
double a,b,x;
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(5)
<< fixed;
cout <<
"Pseudolosowe liczby FP "
"w przedziale <a,b>\n"
"-----------------------"
"------------------\n\n"
"Podaj granice przedziału:\n";
// Odczytujemy granice
// przedziału liczb
// pseudolosowych
cout << "a = ? "; cin >> a;
cout << "b = ? "; cin >> b;
cout << endl;
// Inicjujemy generator
// liczb pseudolosowych
srand(time(NULL));
// Generujemy 10 liczb
// pseudolosowych
// i zamieniamy je
// na liczby FP
for(int i = 0; i < 10; i++)
{
x = a + (b - a) *
(rand() / (double)RAND_MAX);
cout << setw(12) << x << endl;
}
cout << endl;
system("pause");
return 0;
}
|
| Wynik |
Pseudolosowe liczby FP w przedziale <a,b>
-----------------------------------------
Podaj granice przedziału:
a = ? -200
b = ? 200
164.81292
145.49710
-19.96217
53.89440
-69.71423
177.92743
-145.82139
-166.57449
-81.22680
-104.80364
|
Python
(dodatek) # Liczby zmiennoprzecinkowe
# (C)2026 mgr Jerzy Wałaszek
# Metody numeryczne 0030
#---------------------------
import random
print(
"Pseudolosowe liczby FP "
"w przedziale <a,b>\n"
"-----------------------"
"------------------\n\n"
"Podaj granice przedziału:\n")
# Odczytujemy granice
# przedziału liczb
# pseudolosowych
a = float(input("a = ? "))
b = float(input("b = ? "))
print()
# Generujemy 10 liczb
# pseudolosowych
for _ in range(10):
x = random.uniform(a, b)
print(f"{x:12.5f}")
print()
input("Nacisnij Enter...")
|
Jeśli musisz generować zmiennoprzecinkowe liczby pseudolosowe w swoim programie, to lepszym rozwiązaniem jest skorzystanie z biblioteki szablonowej <random>. Poniższy program jest wersją poprzedniego. Różnica dotyczy zakresu generowanych liczb. Przy obiekcie rozkładu uniform_real_distribution parametry a,b określają przedział <a;b) – domknięty lewostronnie, otwarty prawostronnie.
Pamiętaj o ustawieniu w opcjach kompilatora obsługi standardu 11 języka C++:

Dla przedziału <a;b) w Pythonie wykorzystujemy wzór:
a + (b - a) * random.random( ) |
Poniższy program generuje liczby w przedziale <a;b).
C++// Liczby FP
// (C)2019 mgr Jerzy Wałaszek
// Metody numeryczne 0031
//---------------------------
nclude <iomanip>
#include <iostream>
#include <windows.h>
#include <random>
#include <ctime>
#i
using namespace std;
int main()
{
double a,b,x;
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
cout << setprecision(5)
<< fixed;
cout <<
"Pseudolosowe liczby FP "
"w przedziale <a,b)\n"
"-----------------------"
"------------------\n\n"
"Podaj granice przedziału:\n";
// Odczytujemy granice
// przedziału liczb pseudolosowych
cout << "a = ? "; cin >> a;
cout << "b = ? "; cin >> b;
cout << endl;
// Tworzymy obiekt generatora
// pseudolosowego i inicjujemy go
mt19937 gen(time(NULL));
// Tworzymy klasę rozkładu.
// Rozkład jednorodny liczb
// rzeczywistych w przedziale <a,b)
uniform_real_distribution
<double> dist(a,b);
// Generujemy 10 liczb
// pseudolosowych
for(int i = 0; i < 10; i++)
{
x = dist(gen);
cout << setw(12) << x << endl;
}
cout << endl;
system("pause");
return 0;
}
|
| Wynik |
Pseudolosowe liczby FP w przedziale <a,b)
-----------------------------------------
Podaj granice przedziału:
a = ? -10
b = ? 10
2.13726
-0.05279
-5.74471
-3.62289
-6.07315
6.03373
7.79354
-3.40715
-9.99746
0.74454
|
Python
(dodatek) # Liczby FP
# (C)2026 mgr Jerzy Wałaszek
# Metody numeryczne 0031
#---------------------------
import random
print("Pseudolosowe liczby FP "
"w przedziale <a,b)\n"
"-----------------------"
"------------------\n\n"
"Podaj granice przedziału:\n")
# Odczytujemy granice
# przedziału liczb pseudolosowych
a = float(input("a = ? "))
b = float(input("b = ? "))
print()
# Generujemy 10 liczb
# pseudolosowych
for _ in range(10):
x = a + (b - a) * random.random()
print(f"{x:12.5f}")
input("\nNaciśnij Enter...")
|
Zastanów się, jak generować liczby pseudolosowe w przedziałach <a;b>, (a;b>, (a,b) i <a;b) przy wykorzystaniu jedynie samej klasy generatora pseudolosowego (skorzystaj z funkcji składowych min( ) i max( ) generatora).
![]() |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2026 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.