W poprzedniej lekcji (10.2 -- Promocja zmiennoprzecinkowa i całkowa), omówiliśmy promocje numeryczne, które są konwersją określonych węższych typów liczbowych na szersze typy liczbowe (zwykle int lub double), które można efektywnie przetwarzać.
C++ obsługuje inny kategoria konwersji typów numerycznych, zwana konwersjami numerycznymi. Te konwersje numeryczne obejmują dodatkowe konwersje typów pomiędzy podstawowymi typami.
Kluczowa informacja
Każda konwersja typu objęta regułami promocji numerycznej (10.2 -- Promocja zmiennoprzecinkowa i całkowa) nazywana jest promocją numeryczną, a nie konwersją liczbową.
Istnieje pięć podstawowych typów konwersji liczbowych.
- Konwersja typ całkowity na dowolny inny typ całkowity (z wyłączeniem promocji całkowych):
short s = 3; // convert int to short
long l = 3; // convert int to long
char ch = s; // convert short to char
unsigned int u = 3; // convert int to unsigned int- Konwersja typu zmiennoprzecinkowego na dowolny inny typ zmiennoprzecinkowy (z wyłączeniem promocji zmiennoprzecinkowych):
float f = 3.0; // convert double to float
long double ld = 3.0; // convert double to long double- Konwersja typu zmiennoprzecinkowego na dowolny typ całkowity:
int i = 3.5; // convert double to int- Konwersja typu całkowitego na dowolny typ zmiennoprzecinkowy:
double d = 3; // convert int to double- Konwersja typu całkowitego lub typu zmiennoprzecinkowego na bool:
bool b1 = 3; // convert int to bool
bool b2 = 3.0; // convert double to boolNa marginesie…
Ponieważ inicjalizacja nawiasów ściśle zabrania niektórych typów konwersji numerycznych (więcej w następnej lekcji), w tej lekcji użyjemy inicjalizacji kopiowania (która nie ma żadnych takich ograniczeń), aby przykłady były proste.
Bezpieczne i niebezpieczne konwersje
W przeciwieństwie do promocji liczbowych (które zawsze zachowują wartość, a zatem „bezpieczne”), wiele konwersji liczbowych jest niebezpiecznych. Niebezpieczna konwersja to taka, w której co najmniej jedna wartość typu źródłowego nie może zostać przekonwertowana na taką samą wartość typu docelowego.
Konwersje numeryczne dzielą się na trzy ogólne kategorie bezpieczeństwa:
- Konwersje zachowujące wartość to bezpieczne konwersje liczbowe, w których typ docelowy może dokładnie reprezentować wszystkie możliwe wartości w źródle typu.
Dla przykład int Do long i short Do double są bezpiecznymi konwersjami, ponieważ wartość źródłowa zawsze może zostać przekonwertowana na taką samą wartość typu docelowego.
int main()
{
int n { 5 };
long l = n; // okay, produces long value 5
short s { 5 };
double d = s; // okay, produces double value 5.0
return 0;
}Kompilatory zazwyczaj nie generują ostrzeżeń w przypadku niejawnych konwersji zachowujących wartość.
Wartość przekonwertowana przy użyciu konwersji zachowującej wartość zawsze może zostać przekonwertowana z powrotem na typ źródłowy, co daje wartość równoważną wartości oryginalnej:
#include <iostream>
int main()
{
int n = static_cast<int>(static_cast<long>(3)); // convert int 3 to long and back
std::cout << n << '\n'; // prints 3
char c = static_cast<char>(static_cast<double>('c')); // convert 'c' to double and back
std::cout << c << '\n'; // prints 'c'
return 0;
}- Reinterpretacyjna konwersje to niebezpieczne konwersje liczbowe, w przypadku których przekonwertowana wartość może różnić się od wartości źródłowej, ale żadne dane nie są tracone. Konwersje ze znakiem/bez znaku należą do tej kategorii.
Na przykład podczas konwersji signed int na unsigned int:
int main()
{
int n1 { 5 };
unsigned int u1 { n1 }; // okay: will be converted to unsigned int 5 (value preserved)
int n2 { -5 };
unsigned int u2 { n2 }; // bad: will result in large integer outside range of signed int
return 0;
}W przypadku u1, wartość int ze znakiem 5 jest konwertowana na wartość int bez znaku 5. W ten sposób wartość jest w niej zachowywana case.
W przypadku u2, wartość int ze znakiem -5 jest konwertowana na int bez znaku. Ponieważ int bez znaku nie może reprezentować liczb ujemnych, wynik zostanie modulo zawinięty do dużej wartości całkowitej, która jest poza zakresem int ze znakiem. Wartość nie jest w tym przypadku zachowywana.
Takie zmiany wartości są zazwyczaj niepożądane i często powodują, że program będzie prezentował się nieoczekiwanie lub zgodnie z implementacją. zachowanie.
Powiązana treść
W lekcji omawiamy sposób konwertowania wartości spoza zakresu między typami ze znakiem i bez znaku 4.12 -- Wprowadzenie do konwersji typów i static_cast.
Ostrzeżenie
Mimo że konwersje reinterpretacyjne są niebezpieczne, większość kompilatorów domyślnie wyłącza ukryte ostrzeżenia o konwersji ze znakiem/bez znaku.
Dzieje się tak, ponieważ w niektórych obszarach współczesnego C++ (takich jak praca ze standardowymi tablicami bibliotek) konwersje ze znakiem/bez znaku mogą być trudne do uniknięcia. I praktycznie rzecz biorąc, większość takich konwersji w rzeczywistości nie skutkuje zmianą wartości. Dlatego włączenie takich ostrzeżeń może prowadzić do wielu fałszywych ostrzeżeń dla konwersji podpisanych/niepodpisanych, które w rzeczywistości są prawidłowe (zagłuszając uzasadnione ostrzeżenia).
Jeśli zdecydujesz się pozostawić takie ostrzeżenia wyłączone, zachowaj szczególną ostrożność przed niezamierzonymi konwersjami między tymi typami (szczególnie podczas przekazywania argumentu do funkcji przyjmującej parametr o przeciwnym znaku).
Wartości przekonwertowane przy użyciu konwersji reinterpretacyjnej można przekonwertować z powrotem na typ źródłowy, co daje wartość równoważną wartości oryginalnej (nawet jeśli początkowa konwersja wygenerowała wartość spoza zakresu typu źródłowego). W związku z tym konwersje reinterpretacyjne nie powodują utraty danych podczas procesu konwersji.
#include <iostream>
int main()
{
int u = static_cast<int>(static_cast<unsigned int>(-5)); // convert '-5' to unsigned and back
std::cout << u << '\n'; // prints -5
return 0;
}Dla zaawansowanych czytelników
Przed C++20 konwersja wartości bez znaku, która znajduje się poza zakresem wartości ze znakiem, jest technicznie zdefiniowanym zachowaniem (ze względu na to, że liczby całkowite ze znakiem mogą używać innej reprezentacji binarnej niż liczby całkowite bez znaku). W praktyce nie stanowiło to problemu w nowoczesnych systemach.
C++20 wymaga teraz, aby liczby całkowite ze znakiem korzystały z uzupełnienia do dwójki. W rezultacie zasady konwersji zostały zmienione w taki sposób, że powyższa jest teraz dobrze zdefiniowana jako konwersja reinterpretacyjna (konwersja poza zakresem spowoduje zawijanie modulo).
Zauważ, że chociaż takie konwersje są dobrze zdefiniowane, przepełnienie arytmetyczne ze znakiem (które występuje, gdy operacja arytmetyczna generuje wartość spoza zakresu, który można zapisać) jest nadal niezdefiniowanym zachowaniem.
- Strata konwersje to niebezpieczne konwersje liczbowe, podczas których dane mogą zostać utracone podczas konwersji.
Dla przykład double Do int to konwersja, która może spowodować utratę danych:
int i = 3.0; // okay: will be converted to int value 3 (value preserved)
int j = 3.5; // data lost: will be converted to int value 3 (fractional value 0.5 lost)Konwersja z double Do float może również spowodować utratę danych:
float f = 1.2; // okay: will be converted to float value 1.2 (value preserved)
float g = 1.23456789; // data lost: will be converted to float 1.23457 (precision lost)Konwersja wartości, która utraciła dane, z powrotem do typu źródłowego spowoduje otrzymanie wartości innej niż wartość pierwotna:
#include <iostream>
int main()
{
double d { static_cast<double>(static_cast<int>(3.5)) }; // convert double 3.5 to int and back
std::cout << d << '\n'; // prints 3
double d2 { static_cast<double>(static_cast<float>(1.23456789)) }; // convert double 1.23456789 to float and back
std::cout << d2 << '\n'; // prints 1.23457
return 0;
}Na przykład: jeżeli double wartości 3.5 jest konwertowana na int wartości 3, składnik ułamkowy 0.5 przepada. Gdy 3 jest konwertowany z powrotem na double, wynikiem jest 3.0, nie 3.5.
Kompilatory zazwyczaj wyświetlają ostrzeżenie (lub w niektórych przypadkach błąd), gdy w czasie wykonywania zostanie wykonana niejawna konwersja stratna.
Ostrzeżenie
Niektóre konwersje mogą należeć do różnych kategorii w zależności od platformy.
Dla przykład int Do double jest zazwyczaj bezpieczną konwersją, ponieważ int jest zwykle 4 bajty i double to zwykle 8 bajtów i w takich systemach wszystkie możliwe int wartości mogą być reprezentowane jako double. Istnieją jednak architektury, w których oba int i double mają po 8 bajtów. Na takich architekturach int Do double jest to konwersja stratna!
Możemy to zademonstrować, rzucając długą, długą wartość (która musi mieć co najmniej 64 bity) na podwojenie i z powrotem:
#include <iostream>
int main()
{
std::cout << static_cast<long long>(static_cast<double>(10000000000000001LL));
return 0;
}Wypisuje:
10000000000000000
Zauważ, że nasza ostatnia cyfra została utracona!
Należy w miarę możliwości unikać niebezpiecznych konwersji. Jednak nie zawsze jest to możliwe. Kiedy stosuje się niebezpieczne konwersje, najczęściej ma to miejsce wtedy, gdy:
- Możemy ograniczyć wartości do konwersji tylko do tych, które można przekonwertować na równe wartości. Na przykład
intmożna bezpiecznie przekonwertować naunsigned intjeśli możemy zagwarantować, żeintjest nieujemny. - Nie przeszkadza nam utrata niektórych danych, ponieważ są nieistotne. Na przykład konwersja
intdoboolpowoduje utratę danych, ale zazwyczaj nie mamy nic przeciwko, ponieważ sprawdzamy tylko, czyintma wartość0czy nie.
Więcej o konwersjach numerycznych
Specyficzne zasady konwersji liczbowych są skomplikowane i liczne, dlatego oto najważniejsze rzeczy do zapamiętania.
- W uniknąć przypadki, konwersja wartości na typ, którego zakres nie obsługuje tej wartości, doprowadzi do prawdopodobnie nieoczekiwanych wyników. Na przykład:
int main()
{
int i{ 30000 };
char c = i; // chars have range -128 to 127
std::cout << static_cast<int>(c) << '\n';
return 0;
}W tym przykładzie przypisaliśmy dużą liczbę całkowitą do zmiennej typu char (o zakresie od -128 do 127). Powoduje to przepełnienie znaku i daje nieoczekiwany wynik:
48
- Pamiętaj, że przepełnienie jest dobrze zdefiniowane dla wartości bez znaku i powoduje niezdefiniowane zachowanie dla wartości ze znakiem.
- Konwersja z większego typu całkowego lub zmiennoprzecinkowego na mniejszy typ z tej samej rodziny będzie zazwyczaj działać, o ile wartość mieści się w zakresie mniejszego typu. Na przykład:
int i{ 2 };
short s = i; // convert from int to short
std::cout << s << '\n';
double d{ 0.1234 };
float f = d;
std::cout << f << '\n';Daje to oczekiwany wynik:
2 0.1234
- W przypadku wartości zmiennoprzecinkowych może wystąpić pewne zaokrąglenie z powodu utraty precyzji w mniejszym typie. Na przykład:
float f = 0.123456789; // double value 0.123456789 has 9 significant digits, but float can only support about 7
std::cout << std::setprecision(9) << f << '\n'; // std::setprecision defined in iomanip headerW tym przypadku widzimy utratę precyzji, ponieważ float nie jest w stanie utrzymać tak dużej precyzji jak double:
0.123456791
- Konwersja liczby całkowitej na liczbę zmiennoprzecinkową zazwyczaj działa tak długo, jak wartość mieści się w zakresie typu zmiennoprzecinkowego. Na przykład:
int i{ 10 };
float f = i;
std::cout << f << '\n';Daje to oczekiwany wynik:
10
- Konwersja liczby zmiennoprzecinkowej na liczbę całkowitą działa tak długo, jak wartość mieści się w zakresie liczby całkowitej, ale wszelkie wartości ułamkowe zostaną utracone. Na przykład:
int i = 3.5;
std::cout << i << '\n';W tym przykładzie wartość ułamkowa (.5) zostaje utracona, pozostawiając następujący wynik:
3
Chociaż reguły konwersji numerycznej mogą wydawać się przerażające, w rzeczywistości kompilator ogólnie ostrzeże Cię, jeśli spróbujesz zrobić coś niebezpiecznego (z wyjątkiem niektórych konwersji ze znakiem/bez znaku).

