Liczby zmiennoprzecinkowe: Zakres i błędy zaokrągleń

Kurs: Wstęp do programowania
Lekcja 2: Podstawy składni języka programowania
Temat 13: Liczby zmiennoprzecinkowe: Zakres i błędy zaokrągleń

⇓ spis treści ⇓


Liczby zmiennoprzecinkowe są kluczowym elementem w obliczeniach numerycznych i komputerowych, ale ich reprezentacja w systemie komputerowym wiąże się z pewnymi ograniczeniami i trudnościami, w tym z błędami zaokrągleń. W tej lekcji szczegółowo omówimy, jak liczby zmiennoprzecinkowe są przechowywane w pamięci komputera, jaki jest ich zakres, jak działają operacje na tych liczbach oraz jak radzić sobie z problemami związanymi z precyzją i zaokrągleniami.

Reprezentacja liczb zmiennoprzecinkowych

W komputerach liczby zmiennoprzecinkowe są przechowywane zgodnie ze standardem IEEE 754, który definiuje sposób reprezentowania i operowania na tych liczbach. Standard ten wprowadza dwa główne formaty:

  • Float (pojedyncza precyzja): Używa 32 bitów do przechowywania liczby.
  • Double (podwójna precyzja): Używa 64 bitów do przechowywania liczby.

Struktura liczby zmiennoprzecinkowej

Liczba zmiennoprzecinkowa w standardzie IEEE 754 jest podzielona na trzy części:

  1. Bit znaku: Określa, czy liczba jest dodatnia (0) czy ujemna (1).
  2. Wykładnik: Koduje przesunięcie wagi liczby, co pozwala reprezentować zarówno bardzo duże, jak i bardzo małe wartości.
  3. Mantysa (lub część ułamkowa): Przechowuje znaczące cyfry liczby.
Przykład: Reprezentacja liczby zmiennoprzecinkowej w C++
float liczba = 123.456f;
std::cout << "Liczba zmiennoprzecinkowa: " << liczba << std::endl;

W tym przykładzie liczba 123.456f jest przechowywana jako liczba zmiennoprzecinkowa w formacie pojedynczej precyzji. W pamięci komputera zostaje rozłożona na bit znaku, wykładnik i mantysę.

Zakres liczb zmiennoprzecinkowych

Zakres wartości, które można reprezentować za pomocą liczb zmiennoprzecinkowych, zależy od formatu:

  • Float (pojedyncza precyzja): Zakres wynosi od około 1.2 × 10^-38 do 3.4 × 10^38, z precyzją około 7 cyfr znaczących.
  • Double (podwójna precyzja): Zakres wynosi od około 2.2 × 10^-308 do 1.8 × 10^308, z precyzją około 15-16 cyfr znaczących.
Przykład: Zakres liczb zmiennoprzecinkowych w C++
std::cout << "Float - najmniejsza wartość: " << std::numeric_limits<float>::min() << std::endl;
std::cout << "Float - największa wartość: " << std::numeric_limits<float>::max() << std::endl;
std::cout << "Double - najmniejsza wartość: " << std::numeric_limits<double>::min() << std::endl;
std::cout << "Double - największa wartość: " << std::numeric_limits<double>::max() << std::endl;

W tym przykładzie używamy std::numeric_limits, aby uzyskać zakres wartości dla liczb float i double.

Błędy zaokrągleń i ich przyczyny

Jednym z głównych problemów związanych z liczbami zmiennoprzecinkowymi są błędy zaokrągleń, które wynikają z ograniczonej precyzji. Komputery nie mogą dokładnie przechowywać niektórych liczb ułamkowych, co prowadzi do drobnych różnic w wynikach obliczeń.

Przykład błędu zaokrąglenia w Pythonie
a = 0.1 + 0.2
print("Wynik 0.1 + 0.2:", a)  # Wynik: 0.30000000000000004

W powyższym przykładzie suma 0.1 + 0.2 nie daje dokładnie 0.3 z powodu ograniczeń w precyzji reprezentacji zmiennoprzecinkowej.

Dlaczego błędy zaokrągleń występują?

Błędy zaokrągleń są spowodowane przez sposób przechowywania liczb w systemie binarnym. Niektóre liczby, które można dokładnie przedstawić w systemie dziesiętnym (np. 0.1), nie mają dokładnego odpowiednika w systemie binarnym, co powoduje zaokrąglenie.

Reprezentacja liczby 0.1 w systemie binarnym

Liczba 0.1 nie może być dokładnie przedstawiona jako skończony ciąg binarny. W systemie binarnym jej reprezentacja jest nieskończona, co oznacza, że musi zostać zaokrąglona.

Operacje na liczbach zmiennoprzecinkowych

Podczas wykonywania operacji arytmetycznych na liczbach zmiennoprzecinkowych należy pamiętać o możliwych błędach zaokrągleń i utracie precyzji. Oto kilka przykładów operacji, które mogą prowadzić do problemów:

Dodawanie i odejmowanie

Dodawanie i odejmowanie liczb zmiennoprzecinkowych o znacznie różnych wartościach może prowadzić do utraty precyzji.

float a = 1e10;
float b = 1;
std::cout << "Wynik dodawania: " << (a + b) - a << std::endl;  // Wynik: 0, nie 1

W powyższym przykładzie liczba b jest „zgubiona” w wyniku dodawania z powodu ograniczeń precyzji liczby a.

Mnożenie i dzielenie

Mnożenie i dzielenie liczb zmiennoprzecinkowych również może prowadzić do błędów zaokrągleń, zwłaszcza gdy liczby mają bardzo różne skale.

Metody minimalizowania błędów zaokrągleń

Aby minimalizować wpływ błędów zaokrągleń, można stosować różne techniki:

  • Unikanie operacji na liczbach o bardzo różnych skalach: Przeprowadzaj operacje na liczbach o podobnej wielkości, aby zminimalizować utratę precyzji.
  • Używanie podwójnej precyzji (double): W przypadku, gdy wymagana jest większa precyzja, warto używać liczb typu double zamiast float.
  • Zaokrąglanie wyników: Można zaokrąglać wyniki do określonej liczby cyfr znaczących, aby uniknąć niepotrzebnych drobnych różnic.
Przykład: Zaokrąglanie w Pythonie
a = 0.1 + 0.2
print("Zaokrąglony wynik:", round(a, 2))  # Wynik: 0.3

Funkcja round() zaokrągla wynik do 2 miejsc po przecinku, eliminując drobne różnice.

Typy liczb zmiennoprzecinkowych w C++ i Pythonie

W C++ i Pythonie można używać różnych typów danych do reprezentowania liczb zmiennoprzecinkowych:

  • Float: Liczby zmiennoprzecinkowe o pojedynczej precyzji.
  • Double: Liczby zmiennoprzecinkowe o podwójnej precyzji, które oferują większy zakres i dokładność.
  • Long double (C++): Liczby zmiennoprzecinkowe o jeszcze większej precyzji, w zależności od kompilatora i architektury.
Przykład w C++: Różnice między float, double i long double
float a = 123.456f;
double b = 123.456;
long double c = 123.456L;
std::cout << "Float: " << a << std::endl;
std::cout << "Double: " << b << std::endl;
std::cout << "Long double: " << c << std::endl;

Podsumowanie

Liczby zmiennoprzecinkowe są niezbędne w obliczeniach numerycznych, ale ich reprezentacja w systemie komputerowym niesie ze sobą pewne wyzwania, takie jak błędy zaokrągleń i ograniczona precyzja. Zrozumienie, jak liczby te są przechowywane i jakie są ich ograniczenia, jest kluczowe dla pisania poprawnych i wydajnych programów. Używając odpowiednich technik, można minimalizować błędy i zapewnić większą dokładność obliczeń.

Następna lekcja ==> Rozwiązywanie problemów i poprawność programów



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: