Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

obrazek

Autor artykułu: mgr Jerzy Wałaszek

©2026 mgr Jerzy Wałaszek

obrazek

Kody binarne

Liczby zmiennoprzecinkowe

SPIS TREŚCI REMANENT
Podrozdziały
 

Konwersje

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:

1110,0101

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).


do podrozdziału  do strony 

Operacje arytmetyczne

Dodawanie/odejmowanie liczb zmiennoprzecinkowych

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:

LFP = Ld + 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):

  1. Wyznacz przybliżoną wartość błędu ε.
  2. Sprawdź, czy |x(FP) - y(FP)| ≤ ε.
  3. Jeśli tak, to przyjmij, że x(FP) ≈ y(FP).
  4. Jeśli nie, to przyjmij, że x(FP) ≠ 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.

Mnożenie/dzielenie liczb zmiennoprzecinkowych

Zasada mnożenia liczb zmiennoprzecinkowych jest następująca:

Mamy 2 liczby zmiennoprzecinkowe:

obrazek

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 zmiennoprzecinkowe są liczbami przybliżonymi.
  • Precyzja określa liczbę cyfr dokładnych w zapisie dziesiętnym.
  • Pewnych wartości nie można przedstawić dokładnie w systemie zmiennoprzecinkowym, np. 1/5, 2/5,1/3...
  • Działania na liczbach zmiennoprzecinkowych obarczone są zwykle błędami zaokrągleń.
  • Liczby zmiennoprzecinkowej x uzyskanej z obliczeń nie należy przyrównywać do wartości dokładnej a, zamiast tego należy sprawdzać, czy:

    gdzie ε jest wyznaczoną dokładnością obliczeń. Jeśli powyższa nierówność będzie spełniona, to zakładamy, że x jest równe a z dokładnością ε.


do podrozdziału  do strony 

Liczby pseudolosowe

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)
Python: 1.0 - random.random()
(0,1)– otwarty obustronnie:
C++   : (rand() + 1) / (double)(RAND_MAX + 2)
Python: (random.randint(0,10**15) + 1) / (10**15 + 2)
<0,1)– domknięty lewostronnie, otwarty prawostronnie:
C++   : rand() / (double)(RAND_MAX + 1)
Python: random.random()

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)
Python: random.uniform(a,b)

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++:

obrazek

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).


do podrozdziału  do strony 

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: i-lo@eduinf.waw.pl
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.