4.8 — Liczby zmiennoprzecinkowe

Liczby całkowite doskonale nadają się do liczenia liczb całkowitych, ale czasami musimy przechowywać bardzo duże liczby (dodatnie lub ujemne) lub liczby ze składnikiem ułamkowym. Zmienna zmiennoprzecinkowy to zmienna, która może przechowywać liczbę ze składnikiem ułamkowym, taką jak 4320,0, -3,33 lub 0,01226. zmienna część nazwy zmiennoprzecinkowy odnosi się do faktu, że przecinek dziesiętny może „pływać” – to znaczy może obsługiwać zmienną liczbę cyfr przed i po przecinku. Dane zmiennoprzecinkowe są zawsze podpisane (mogą zawierać wartości dodatnie i ujemne).

Wskazówka

Podczas zapisywania liczb zmiennoprzecinkowych w kodzie separatorem dziesiętnym musi być kropka dziesiętna. Jeśli pochodzisz z kraju, w którym używany jest przecinek dziesiętny, musisz przyzwyczaić się do używania zamiast niego kropki dziesiętnej.

Typy zmiennoprzecinkowe w C++

C++ ma trzy podstawowe typy danych zmiennoprzecinkowych: pojedynczą precyzję float, podwójną precyzję double i rozszerzoną precyzję long double. Podobnie jak w przypadku liczb całkowitych, C++ nie definiuje rzeczywistego rozmiaru tych typów.

KategoriaTyp C++Typowy rozmiar
zmiennoprzecinkowyfloat

4 bajty
double8 bajtów
long double8, 12 lub 16 bajty

W nowoczesnych architekturach typy zmiennoprzecinkowe są konwencjonalnie implementowane przy użyciu jednego z formatów zmiennoprzecinkowych zdefiniowanych w standardzie IEEE 754 (patrz https://en.wikipedia.org/wiki/IEEE_754). W efekcie float ma prawie zawsze 4 bajty, a double prawie zawsze ma 8 bajtów.

Z drugiej strony long double jest dziwnym typem. Na różnych platformach jego rozmiar może wahać się od 8 do 16 bajtów i może, ale nie musi, używać formatu zgodnego z IEEE 754. Zalecamy unikanie long double.

Wskazówka

Ta seria tutoriali zakłada, że Twój kompilator używa formatu zgodnego z IEEE 754 dla float i double.

Możesz sprawdzić, czy typy zmiennoprzecinkowe są zgodne ze standardem IEEE 754 za pomocą następującego kodu:

#include <iostream>
#include <limits>

int main()
{
    std::cout << std::boolalpha; // print bool as true or false rather than 1 or 0
    std::cout << "float: " << std::numeric_limits<float>::is_iec559 << '\n';
    std::cout << "double: " << std::numeric_limits<double>::is_iec559 << '\n';
    std::cout << "long double: " << std::numeric_limits<long double>::is_iec559 << '\n';
}

Dla zaawansowanych czytelników

float jest prawie zawsze implementowany przy użyciu 4-bajtowego formatu pojedynczej precyzji IEEE 754.
double jest prawie zawsze implementowany przy użyciu 8-bajtowy format podwójnej precyzji IEEE 754.

Jednak format używany do implementacji long double różni się w zależności od platformy. Typowe opcje obejmują:

  • 8-bajtowy format IEEE 754 o podwójnej precyzji (taki sam jak double).
  • 80-bitowy (często do 12 lub 16 bajtów) x87 format o rozszerzonej precyzji (kompatybilny z IEEE 754).
  • 16-bajtowy IEEE 754 o poczwórnej precyzji format.
  • 16-bajtowy format double-double (niekompatybilny z IEEE 754).

Zmienne i literały zmiennoprzecinkowe

Oto kilka definicji zmiennych zmiennoprzecinkowych:

float f;
double d;
long double ld;

Jeśli używasz literałów zmiennoprzecinkowych, zawsze uwzględniaj co najmniej jedno miejsce dziesiętne (nawet jeśli liczba dziesiętna wynosi 0). liczba zmiennoprzecinkowa, a nie liczba całkowita.

int a { 5 };      // 5 means integer
double b { 5.0 }; // 5.0 is a floating point literal (no suffix means double type by default)
float c { 5.0f }; // 5.0 is a floating point literal, f suffix means float type

int d { 0 };      // 0 is an integer
double e { 0.0 }; // 0.0 is a double

Zauważ, że domyślnie literały zmiennoprzecinkowe domyślnie mają typ double. Do określenia literału typu float używany jest przyrostek f .

Najlepsza praktyka

Zawsze upewnij się, że typ literałów jest zgodny z typem zmiennych, do których są przypisane lub używane do inicjalizacji. W przeciwnym razie nastąpi niepotrzebna konwersja, która może spowodować utratę precyzja.

Drukowanie liczb zmiennoprzecinkowych

Rozważmy teraz prosty program:

#include <iostream>

int main()
{
	std::cout << 5.0 << '\n';
	std::cout << 6.7f << '\n';
	std::cout << 9876543.21 << '\n';

	return 0;
}

Wyniki tego pozornie prostego programu mogą Cię zaskoczyć:

5
6.7
9.87654e+06

W pierwszym przypadku std::cout wydrukowane 5, mimo że wpisaliśmy 5.0Domyślnie std::cout nie będzie wypisz część ułamkową liczby, jeśli część ułamkowa wynosi 0.

W drugim przypadku liczba zostanie wydrukowana zgodnie z oczekiwaniami.

W trzecim przypadku wydrukowała liczbę w notacji naukowej (jeśli potrzebujesz odświeżenia notacji naukowej, zobacz lekcję 4.7 -- Wprowadzenie do notacji naukowej).

Zakres zmiennoprzecinkowy

FormatZakresPrecyzja
IEEE 754 o pojedynczej precyzji (4 bajty)±1,18 x 10-38 do ±3,4 x 1038 i 0,06-9 cyfr znaczących, typowo 7
IEEE 754 podwójnej precyzji (8 bajtów)±2,23 x 10-308 do ±1,80 x 10308 i 0,015-18 cyfr znaczących, typowo 16
x87 o rozszerzonej precyzji (80 bitów)±3,36 x 10-4932 do ±1,18 x 104932 i 0,018-21 znaczące cyfr
IEEE 754 poczwórna precyzja (16 bajtów)±3,36 x 10-4932 do ±1,18 x 104932 i 0,033-36 cyfr znaczących

Dla zaawansowanych czytelników

80-bitowy x87 Typ zmiennoprzecinkowy o rozszerzonej precyzji jest małą anomalią historyczną. W nowoczesnych procesorach obiekty tego typu są zwykle dopełniane do 12 lub 16 bajtów (co jest bardziej naturalnym rozmiarem do obsługi procesorów). Oznacza to, że te obiekty zawierają 80 bitów danych zmiennoprzecinkowych, a pozostała pamięć jest wypełniona.

Może wydawać się trochę dziwne, że 80-bitowy typ zmiennoprzecinkowy ma ten sam zakres co 16-bajtowy typ zmiennoprzecinkowy. Dzieje się tak, ponieważ mają tę samą liczbę bitów przeznaczonych na wykładnik - jednakże liczba 16-bajtowa może przechowywać więcej cyfr znaczących.

Dokładność zmiennoprzecinkowa

Rozważ ułamek 1/3. Dziesiętna reprezentacja tej liczby to 0,33333333333333… gdzie 3 zmierza do nieskończoności. Jeśli zapiszesz tę liczbę na kartce papieru, w pewnym momencie Twoje ramię zmęczy się i w końcu przestaniesz pisać. A liczba, którą pozostawiłeś, byłaby bliska 0,3333333333…. (gdzie 3 prowadzi do nieskończoności), ale nie dokładnie.

Na komputerze liczba o nieskończonej precyzji wymagałaby nieskończonej pamięci do przechowywania, a zazwyczaj mamy tylko 4 lub 8 bajtów na wartość. Ta ograniczona pamięć oznacza, że ​​liczby zmiennoprzecinkowe mogą przechowywać tylko określoną liczbę cyfr znaczących — wszelkie dodatkowe cyfry znaczące są albo tracone, albo przedstawiane nieprecyzyjnie. Faktycznie zapisana liczba może być zbliżona do żądanej, ale nie dokładna. Pokażemy przykład w następnej sekcji.

Klasa Dokładność typu zmiennoprzecinkowego określa, ile cyfr znaczących może reprezentować bez utraty informacji.

Liczba cyfr precyzji typu zmiennoprzecinkowego zależy zarówno od rozmiaru (liczby zmiennoprzecinkowe mają mniejszą precyzję niż liczby podwójne), jak i konkretnej przechowywanej wartości (niektóre wartości mogą być reprezentowane dokładniej niż inne).

Na przykład zmiennoprzecinkowy ma od 6 do 9 cyfry precyzji. Oznacza to, że liczba zmiennoprzecinkowa może dokładnie reprezentować dowolną liczbę zawierającą maksymalnie 6 cyfr znaczących. Liczba zawierająca od 7 do 9 cyfr znaczących może być przedstawiona dokładnie lub nie, w zależności od konkretnej wartości. Liczba o dokładności większej niż 9 cyfr z pewnością nie zostanie przedstawiona dokładnie.

Wartości podwójne mają precyzję od 15 do 18 cyfr, przy czym większość wartości podwójnych ma co najmniej 16 cyfr znaczących. Long double ma minimalną precyzję 15, 18 lub 33 cyfr znaczących, w zależności od tego, ile bajtów zajmuje.

Kluczowa informacja

Typ zmiennoprzecinkowy może precyzyjnie reprezentować tylko określoną liczbę cyfr znaczących. Użycie wartości zawierającej więcej cyfr znaczących niż minimalna może spowodować niedokładne zapisanie wartości.

Wyprowadzanie wartości zmiennoprzecinkowych

Podczas wyprowadzania liczb zmiennoprzecinkowych std::cout ma domyślną precyzję 6 — to znaczy zakłada, że wszystkie zmienne zmiennoprzecinkowe są istotne tylko do 6 cyfr (minimalna precyzja zmiennoprzecinkowej), dlatego obcina wszystko po to.

Następujący program pokazuje std::cout obcięcie do 6 cyfr:

#include <iostream>

int main()
{
    std::cout << 9.87654321f << '\n';
    std::cout << 987.654321f << '\n';
    std::cout << 987654.321f << '\n';
    std::cout << 9876543.21f << '\n';
    std::cout << 0.0000987654321f << '\n';

    return 0;
}

Ten program wyprowadza:

9.87654
987.654
987654
9.87654e+006
9.87654e-005

Zauważ, że każda z nich ma tylko 6 cyfr znaczących.

Zauważ także, że std::cout w niektórych przypadkach przełączy się na wyświetlanie liczb w notacji naukowej. W zależności od kompilatora wykładnik będzie zazwyczaj dopełniany do minimalnej liczby cyfr. Nie martw się, 9.87654e+006 to to samo co 9.87654e6, tylko z pewnymi dopełnieniami zer. Minimalna liczba wyświetlanych cyfr wykładników zależy od kompilatora (Visual Studio używa 3, inne używają 2 zgodnie ze standardem C99).

Możemy zastąpić domyślną precyzję pokazywaną przez std::cout za pomocą output manipulator funkcji o nazwie std::setprecision(). Manipulatory wyjściowe zmieniają sposób wyprowadzania danych i są zdefiniowane w the iomanip .

#include <iomanip> // for output manipulator std::setprecision()
#include <iostream>

int main()
{
    std::cout << std::setprecision(17); // show 17 digits of precision
    std::cout << 3.33333333333333333333333333333333333333f <<'\n'; // f suffix means float
    std::cout << 3.33333333333333333333333333333333333333 << '\n'; // no suffix means double

    return 0;
}

Wyjścia:

3.3333332538604736
3.3333333333333335

Ponieważ za pomocą std::setprecision() ustawiliśmy precyzję na 17 cyfr, każda z powyższych liczb jest drukowana z 17 cyframi. Ale, jak widać, liczby z pewnością nie są dokładne do 17 cyfr! A ponieważ liczby zmiennoprzecinkowe są mniej precyzyjne niż liczby podwójne, pływak ma więcej błędów.

Wskazówka

Manipulatory wyjściowe (i manipulatory wejściowe) są lepkie - co oznacza, że ​​jeśli je ustawisz, pozostaną ustawione.

Jedynym wyjątkiem jest std::setw. Resetowanie niektórych operacji IO std::setw, więc std::setw należy stosować za każdym razem, gdy jest to potrzebne.

Problemy z precyzją nie wpływają tylko na liczby ułamkowe, mają wpływ na każdą liczbę zawierającą zbyt wiele cyfr znaczących. Rozważmy dużą liczbę:

#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    float f { 123456789.0f }; // f has 10 significant digits
    std::cout << std::setprecision(9); // to show 9 digits in f
    std::cout << f << '\n';

    return 0;
}

Wyjście:

123456792

123456792 jest większa niż 123456789. Wartość 123456789.0 ma 10 cyfr znaczących, ale wartości zmiennoprzecinkowe mają zazwyczaj 7-cyfrową precyzję (a wynik 123456792 jest dokładny tylko do 7 cyfr znaczących). Straciliśmy trochę precyzji! Kiedy traci się precyzję, ponieważ nie można dokładnie zapisać liczby, nazywa się to a Błąd zaokrąglenia.

. W związku z tym należy zachować ostrożność podczas używania liczb zmiennoprzecinkowych, które wymagają większej precyzji, niż mogą pomieścić zmienne.

Najlepsza praktyka

Preferuj opcję double over float, chyba że przestrzeń jest na wagę złota, ponieważ brak precyzji w zmiennoprzecinkowej często prowadzi do niedokładności.

Błędy zaokrąglania powodują porównania zmiennoprzecinkowe trudne

Liczby zmiennoprzecinkowe są trudne w obsłudze ze względu na nieoczywiste różnice między liczbami binarnymi (w jaki sposób przechowywane są dane) i dziesiętnymi (w jaki sposób myślimy). Rozważmy ułamek 1/10. W systemie dziesiętnym można to łatwo przedstawić jako 0,1, a my jesteśmy przyzwyczajeni do myślenia o 0,1 jako o łatwej do przedstawienia liczbie z 1 cyfrą znaczącą. Jednak w systemie binarnym wartość dziesiętna 0,1 jest reprezentowana przez nieskończoną sekwencję: 0,00011001100110011… Z tego powodu, gdy przypiszemy 0,1 do liczby zmiennoprzecinkowej, napotkamy problemy z precyzją.

Efekty tego można zobaczyć w następującym programie:

#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    double d{0.1};
    std::cout << d << '\n'; // use default cout precision of 6
    std::cout << std::setprecision(17);
    std::cout << d << '\n';

    return 0;
}

To daje:

0.1
0.10000000000000001

W górnym wierszu zostanie wydrukowane std::cout zgodnie z oczekiwaniami wynosi 0,1.

W ostatecznym rozrachunku, gdzie std::cout pokazujemy nam 17 cyfr dokładności, widzimy, że d w rzeczywistości niezupełnie 0,1! Dzieje się tak, ponieważ sobowtór musiał obciąć przybliżenie ze względu na ograniczoną pamięć. Wynikiem jest liczba z dokładnością do 16 cyfr znaczących (co gwarantuje podwójną gwarancję), ale liczba ta nie jest dokładnie 0,1. Błędy zaokrągleń mogą spowodować, że liczba będzie nieco mniejsza lub nieco większa, w zależności od miejsca obcięcia.

Błędy zaokrąglenia mogą mieć nieoczekiwane konsekwencje:

#include <iomanip> // for std::setprecision()
#include <iostream>

int main()
{
    std::cout << std::setprecision(17);

    double d1{ 1.0 };
    std::cout << d1 << '\n';
	
    double d2{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // should equal 1.0
    std::cout << d2 << '\n';

    return 0;
}
1
0.99999999999999989

Chociaż moglibyśmy się spodziewać, że d1 i d2 powinno być równe, widzimy, że tak nie jest. Gdybyśmy porównali d1 i d2 w programie, prawdopodobnie program nie działałby zgodnie z oczekiwaniami. Ponieważ liczby zmiennoprzecinkowe są zwykle niedokładne, porównywanie liczb zmiennoprzecinkowych jest generalnie problematyczne — omawiamy ten temat więcej (i rozwiązania) na lekcji 6.7 -- Operatory relacyjne i zmiennoprzecinkowe porównania.

Ostatnia uwaga na temat błędów zaokrągleń: operacje matematyczne (takie jak dodawanie i mnożenie) mają tendencję do zwiększania błędów zaokrągleń. Zatem mimo że 0,1 ma błąd zaokrąglenia w 17. cyfrze znaczącej, gdy dodamy 0,1 dziesięć razy, błąd zaokrąglenia wkradł się do 16. cyfry znaczącej. Kontynuowanie operacji spowodowałoby, że błąd ten stałby się coraz bardziej znaczący.

Kluczowa informacja

Błędy zaokrągleń występują, gdy nie można dokładnie zapisać liczby. Może się to zdarzyć nawet w przypadku prostych liczb, takich jak 0,1. Dlatego błędy zaokrągleń mogą zdarzać się cały czas i zdarzają się. Błędy zaokrągleń nie są wyjątkiem – są normą. Nigdy nie zakładaj, że liczby zmiennoprzecinkowe są dokładne.

Następstwem tej zasady jest: uważaj na używanie liczb zmiennoprzecinkowych w danych finansowych lub walutowych.

Powiązana treść

Aby uzyskać więcej informacji na temat przechowywania liczb zmiennoprzecinkowych w formacie binarnym, sprawdź narzędzie float.exposed .
Aby dowiedzieć się więcej o liczbach zmiennoprzecinkowych i zaokrąglaniu błędów, floating-point-gui.de i fabiensanglard.net ma przystępne przewodniki na ten temat.

Formaty kompatybilne z NaN i Inf

IEEE 754 dodatkowo obsługują pewne wartości specjalne:

  • Inf, które reprezentują nieskończoność. Inf jest ze znakiem i może być dodatnia (+Inf) lub ujemna (-Inf).
  • NaN, co oznacza „Not a Number”. Istnieje kilka różnych rodzajów NaN (których nie będziemy tutaj omawiać).
  • Zero ze znakiem, co oznacza, że ​​istnieją oddzielne reprezentacje „zera dodatniego” (+0,0) i „zera ujemnego” (-0,0).

Formaty, które nie są kompatybilne ze standardem IEEE 754, mogą nie obsługiwać niektórych (lub żadnej) z tych wartości. W takich przypadkach kod, który używa lub generuje te wartości specjalne, spowoduje zachowanie zdefiniowane w implementacji.

Oto program pokazujący wszystkie trzy:

#include <iostream>

int main()
{
    double zero { 0.0 };
    
    double posinf { 5.0 / zero }; // positive infinity
    std::cout << posinf << '\n';

    double neginf { -5.0 / zero }; // negative infinity
    std::cout << neginf << '\n';

    double z1 { 0.0 / posinf }; // positive zero
    std::cout << z1 << '\n';

    double z2 { -0.0 / posinf }; // negative zero
    std::cout << z2 << '\n';

    double nan { zero / zero }; // not a number (mathematically invalid)
    std::cout << nan << '\n';

    return 0;
}

I wyniki przy użyciu Clang:

inf
-inf
0
-0
nan

Zauważ, że wyniki drukowania Inf i NaN są specyficzne dla platformy, więc wyniki mogą się różnić (np. Visual Studio drukuje ostatni wynik jako -nan(ind)).

Najlepsza praktyka

Unikaj dzielenia by 0.0, nawet jeśli Twój kompilator to obsługuje.

Wnioski

Podsumowując, dwie rzeczy, o których powinieneś pamiętać na temat liczb zmiennoprzecinkowych:

  1. Liczby zmiennoprzecinkowe są przydatne do przechowywania bardzo dużych lub bardzo małych liczb, w tym tych ze składnikami ułamkowymi.
  2. Liczby zmiennoprzecinkowe często obarczone są małymi błędami zaokrągleń, nawet jeśli liczba ma mniej cyfr znaczących niż precyzja są tak małe i dlatego, że liczby są obcinane w celu uzyskania wyniku. Jednak porównania liczb zmiennoprzecinkowych mogą nie dać oczekiwanych wyników. Wykonywanie operacji matematycznych na tych wartościach spowoduje zwiększenie błędów zaokrągleń.
guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
698 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze