4.12 -- Wprowadzenie do konwersji typów i static_cast

Niejawna konwersja typu

Rozważ następujący program:

#include <iostream>

void print(double x) // print takes a double parameter
{
	std::cout << x << '\n';
}

int main()
{
	print(5); // what happens when we pass an int value?

	return 0;
}

W powyższym przykładzie funkcja print() ma parametr typu double ale osoba wywołująca przekazuje wartość 5 która wynosi type int. Co się dzieje w tym przypadku?

W większości przypadków C++ umożliwi nam konwersję wartości jednego typu podstawowego na inny typ podstawowy. Proces konwersji danych z jednego typu na inny nazywa się konwersja typu. Zatem argument int 5 zostanie przekonwertowana na wartość double 5.0 a następnie skopiowany do funkcji parametru x. Słowo kluczowe print() wypisuje tę wartość, co daje następujący wynik:

5

Przypomnienie

Domyślnie wartości zmiennoprzecinkowe, których część dziesiętna wynosi 0, są drukowane bez miejsc dziesiętnych (np. 5.0 drukuje jako 5).

Gdy kompilator wpisze konwersję w naszym imieniu bez naszej wyraźnej prośby, wywołujemy ta niejawna konwersja typu ilustruje to -- nigdzie wyraźnie nie mówimy kompilatorowi, aby przekonwertował wartość całkowitą 5 na wartość podwójną 5.0. Raczej funkcja oczekuje wartości podwójnej i przekazujemy argument będący liczbą całkowitą. Kompilator zauważy niezgodność i niejawnie przekonwertuje liczbę całkowitą na a double.

Oto podobny przykład, w którym naszym argumentem jest zmienna int zamiast literału int:

#include <iostream>

void print(double x) // print takes a double parameter
{
	std::cout << x << '\n';
}

int main()
{
	int y { 5 };
	print(y); // y is of type int

	return 0;
}

Działa to identycznie jak powyżej. Wartość przechowywana w zmiennej int y (5) zostanie przekonwertowana na wartość double 5.0, a następnie skopiowana do parametru x.

Konwersja typu wartości daje nową wartość

Konwersja typu proces nie modyfikuje wartości (lub obiektu) dostarczającego dane do konwersji. Zamiast tego proces konwersji wykorzystuje te dane jako dane wejściowe i generuje przekonwertowany wynik.

Kluczowa informacja

Konwersja typu wartości na inny typ wartości zachowuje się podobnie do wywołania funkcji, której typ zwracany jest zgodny z docelowym typem konwersji. Dane do konwersji przekazywane są jako argument, a przekonwertowany wynik jest zwracany (w obiekcie tymczasowym) do wykorzystania przez obiekt wywołujący.

W powyższym przykładzie konwersja nie powoduje zmiany zmiennej y z typu int Do double ani wartości y z 5 Do 5.0. Zamiast tego konwersja wykorzystuje wartość y (5) jako dane wejściowe i zwraca obiekt tymczasowy typu double o wartości 5.0. Ten obiekt tymczasowy jest następnie przekazywany do funkcji print.

Dla zaawansowanych czytelników

Niektóre zaawansowane konwersje typów (np. te obejmujące const_cast lub reinterpret_cast) nie zwracają obiektów tymczasowych, lecz zamiast tego reinterpretują typ istniejącej wartości lub obiektu.

Ostrzeżenia dotyczące niejawnej konwersji typu

Chociaż niejawna konwersja typu jest wystarczająca w większości przypadków, gdy konieczna jest konwersja typu, w kilku przypadkach tak nie jest. Rozważmy następujący program, podobny do powyższego przykładu:

#include <iostream>

void print(int x) // print now takes an int parameter
{
	std::cout << x << '\n';
}

int main()
{
	print(5.5); // warning: we're passing in a double value

	return 0;
}

W tym programie zmieniliśmy print() aby przyjąć parametr int , a wywołanie funkcji print() jest teraz przekazywane double wartości 5.5. Podobnie jak powyżej, kompilator użyje niejawnej konwersji typu, aby przekonwertować podwójną wartość 5.5 na wartość typu int, aby można ją było przekazać do funkcji print().

W przeciwieństwie do początkowego przykładu, podczas kompilacji tego programu kompilator wygeneruje pewnego rodzaju ostrzeżenie o możliwej utracie danych. A ponieważ masz włączoną opcję „traktuj ostrzeżenia jako błędy” (tak robisz, prawda?), Twój kompilator przerwie proces kompilacji.

Wskazówka

Jeśli chcesz skompilować ten przykład, musisz tymczasowo wyłączyć opcję „traktuj ostrzeżenia jako błędy”. Zobacz lekcję 0.11 — Konfigurowanie kompilatora: poziomy ostrzeżeń i błędów , aby uzyskać więcej informacji na temat tego ustawienia.

Po skompilowaniu i uruchomieniu program ten wypisuje następujące informacje:

5

Zauważ, że chociaż przekazaliśmy wartość 5.5, program wydrukował 5. Ponieważ wartości całkowite nie mogą przechowywać ułamków, gdy wartość double 5.5 jest niejawnie konwertowana na int, składnik ułamkowy jest pomijany, a zachowywana jest tylko wartość całkowita.

Ponieważ konwersja wartości zmiennoprzecinkowej na wartość całkowitą powoduje usunięcie dowolnego składnika ułamkowego, kompilator ostrzeże nas, gdy wykona niejawną konwersję typu z wartości zmiennoprzecinkowej na wartość całkowitą. Dzieje się tak nawet wtedy, gdy przekazujemy wartość zmiennoprzecinkową bez składnika ułamkowego, np. 5.0 --w tym konkretnym przypadku podczas konwersji na wartość całkowitą 5 nie następuje żadna rzeczywista utrata wartości, ale kompilator może nadal ostrzegać nas, że konwersja jest niebezpieczna.

Kluczowa informacja

Niektóre konwersje typów (takie jak a char na int) zawsze zachowują konwertowaną wartość, podczas gdy inne (takie jak double Do int) może spowodować zmianę wartości podczas konwersji. Niebezpieczne konwersje niejawne zazwyczaj generują ostrzeżenie kompilatora lub (w przypadku inicjalizacji nawiasów klamrowych) błąd.

Jest to jeden z głównych powodów, dla których inicjowanie nawiasów jest preferowaną formą inicjalizacji. Inicjalizacja nawiasów gwarantuje, że nie będziemy próbować inicjować zmiennej za pomocą inicjatora, który straci wartość, gdy zostanie niejawnie skonwertowany na typ:

int main()
{
    double d { 5 }; // okay: int to double is safe
    int x { 5.5 }; // error: double to int not safe

    return 0;
}

Powiązana treść

Niejawna konwersja typu to obszerny temat. Zgłębiamy ten temat bardziej szczegółowo w przyszłych lekcjach, zaczynając od lekcji 10.1 -- Niejawna konwersja typów.

Wprowadzenie do jawnej konwersji typów za pomocą operatora static_cast

Wracając do naszego najnowszego print() przykładu, co by było, gdybyśmy celowo chcieli przekazać wartość double do funkcji przyjmującej liczbę całkowitą (wiedząc, że przekonwertowana wartość spowoduje utratę ułamka komponent?) Wyłączenie opcji „traktuj ostrzeżenia jako błędy” tylko po to, aby nasz program się skompilował, jest złym pomysłem, ponieważ wtedy przy każdej kompilacji będziemy otrzymywać ostrzeżenia (które szybko nauczymy się ignorować) i ryzykujemy przeoczenie ostrzeżeń o poważniejszych problemach.

C++ obsługuje drugą metodę konwersji typów, zwaną jawną konwersją typów. Jawna konwersja typu pozwól nam (programiście) wyraźnie powiedzieć kompilatorowi, aby przekonwertował wartość z jednego typu na inny, i to bierzemy pełną odpowiedzialność za wynik tej konwersji. Jeśli taka konwersja spowoduje utratę wartości, kompilator nas nie ostrzeże.

Aby wykonać jawną konwersję typu, w większości przypadków użyjemy operatora static_cast . Składnia static cast wygląda trochę zabawnie:

static_cast<new_type>(expression)

static_cast pobiera wartość z wyrażenia jako dane wejściowe i zwraca tę wartość przekonwertowaną na typ określony przez new_type (np. int, bool, char, double).

Kluczowa informacja

Za każdym razem, gdy zobaczysz składnię C++ (z wyłączeniem preprocesora), która korzysta z nawiasy kątowe (<>), rzeczą pomiędzy nawiasami kątowymi najprawdopodobniej będzie typ. Zwykle C++ radzi sobie z kodem, który wymaga sparametryzowanego typu.

Zaktualizujmy nasz poprzedni program za pomocą static_cast:

#include <iostream>

void print(int x)
{
	std::cout << x << '\n';
}

int main()
{
	print( static_cast<int>(5.5) ); // explicitly convert double value 5.5 to an int

	return 0;
}

Ponieważ teraz wyraźnie żądamy konwersji tej podwójnej wartości 5.5 na wartość int , kompilator nie wygeneruje ostrzeżenia o możliwej utracie danych podczas kompilacji (co oznacza, że możemy pozostawić „traktuj ostrzeżenia jako błędy” włączone).

Powiązana treść

C++ obsługuje inne typy rzutowania. Więcej o różnych typach rzutowania porozmawiamy w przyszłej lekcji 10.6 -- Jawna konwersja typów (casting) i static_cast.

Używanie static_cast do konwersji znaku na int

W lekcji na temat znaków 4.11 -- Znaki widzieliśmy, że wydrukowanie wartości znaku przy użyciu std::cout powoduje wydrukowanie wartości jako znaku:

#include <iostream>

int main()
{
    char ch{ 97 }; // 97 is ASCII code for 'a'
    std::cout << ch << '\n';

    return 0;
}

Wypisuje:

a

Jeśli chcemy wydrukować wartość całkowitą zamiast znaku, możemy to zrobić w ten sposób za pomocą static_cast do rzutowania wartości z char na int:

#include <iostream>

int main()
{
    char ch{ 97 }; // 97 is ASCII code for 'a'
    // print value of variable ch as an int
    std::cout << ch << " has value " << static_cast<int>(ch) << '\n';

    return 0;
}

Wypisuje:

a has value 97

Warto zauważyć, że argument do static_cast jest oceniany jako wyrażenie. Kiedy przekazujemy zmienną, jest ona oceniana w celu uzyskania jej wartości, a następnie wartość ta jest konwertowana na nowy typ. Sama zmienna to nie zmieniony poprzez rzutowanie jego wartości na nowy typ. W powyższym przypadku zmienna ch jest nadal znakiem i nadal utrzymuje tę samą wartość nawet po rzuceniu jej wartości na int.

konwersję znaku przy użyciu static_cast

Wartości całkowite ze znakiem można konwertować na wartości całkowite bez znaku i odwrotnie, używając rzutowania statycznego.

Jeśli konwertowana wartość może być reprezentowana w typie docelowym, przekonwertowana wartość pozostanie niezmieniona (tylko typ ulegnie zmianie). Na przykład:

#include <iostream>

int main()
{
    unsigned int u1 { 5 };
    // Convert value of u1 to a signed int 
    int s1 { static_cast<int>(u1) };
    std::cout << s1 << '\n'; // prints 5

    int s2 { 5 };
    // Convert value of s2 to an unsigned int
    unsigned int u2 { static_cast<unsigned int>(s2) };
    std::cout << u2 << '\n'; // prints 5

    return 0;
}

Wypisuje:

5
5

Ponieważ wartość 5 należy zarówno do int ze znakiem, jak i int bez znaku, wartość 5 można bez problemu przekonwertować na dowolny typ.

Jeśli konwertowana wartość nie może być reprezentowana w typie docelowym:

  • Jeśli typ docelowy jest bez znaku, wartość zostanie zawijana modulo. Zawijanie modulo omówimy w lekcji 4.5 -- Liczby całkowite bez znaku i dlaczego ich unikać.
  • Jeśli typ docelowy jest podpisany, wartość jest zdefiniowana w implementacji przed C++20 i będzie zawijana modulo od C++20.

Oto przykład konwersji dwóch wartości, których nie można przedstawić w typie docelowym (zakładając 32-bitowe liczby całkowite):

#include <iostream>

int main()
{
    int s { -1 };
    std::cout << static_cast<unsigned int>(s) << '\n'; // prints 4294967295 

    unsigned int u { 4294967295 }; // largest 32-bit unsigned int
    std::cout << static_cast<int>(u) << '\n'; // implementation-defined prior to C++20, -1 as of C++20
    
    return 0;
}

W przypadku C++20 daje to wynik:

4294967295
-1

Wartość int ze znakiem -1 nie może być reprezentowana jako int bez znaku. Wynik modulo jest zawijany do wartości int bez znaku 4294967295.

Wartość int bez znaku 4294967295 nie może być reprezentowana jako int ze znakiem. Przed wersją C++ 20 wynik jest zdefiniowany w implementacji (ale prawdopodobnie będzie to -1). Począwszy od C++ 20, wynik będzie modulo zawijany do -1.

Ostrzeżenie

Konwersja wartości całkowitej bez znaku na wartość całkowitą ze znakiem spowoduje zachowanie zdefiniowane w implementacji przed C++20, jeśli konwertowana wartość nie może być reprezentowana w typie ze znakiem.

std::int8_t i std::uint8_t prawdopodobnie zachowują się jak znaki, a nie liczby całkowite

Jak zauważono w lekcja 4.6 -- Liczby całkowite o stałej szerokości i size_t, większość kompilatorów definiuje i traktuje std::int8_t i std::uint8_t (oraz odpowiadające im typy szybkie i najmniej o stałej szerokości) odpowiednio identycznie z typami signed char i unsigned char . Teraz, gdy już omówiliśmy, czym są znaki, możemy pokazać, gdzie może to być problematyczne:

#include <cstdint>
#include <iostream>

int main()
{
    std::int8_t myInt{65};      // initialize myInt with value 65
    std::cout << myInt << '\n'; // you're probably expecting this to print 65

    return 0;
}

Ponieważ std::int8_t opisuje się jako int, możesz dać się oszukać, wierząc, że powyższy program wydrukuje wartość całkowitą 65. Jednak w większości systemów ten program zamiast tego wydrukuje A (traktując myInt jak signed char). Nie jest to jednak gwarantowane (w niektórych systemach może faktycznie zostać wydrukowane 65).

Jeśli chcesz mieć pewność, że std::int8_t lub std::uint8_t obiekt będzie traktowany jako liczba całkowita, możesz przekonwertować wartość na liczbę całkowitą za pomocą static_cast:

#include <cstdint>
#include <iostream>

int main()
{
    std::int8_t myInt{65};
    std::cout << static_cast<int>(myInt) << '\n'; // will always print 65

    return 0;
}

W przypadkach, gdy std::int8_t jest traktowany jako znak, dane wejściowe z konsoli również mogą powodować problemy:

#include <cstdint>
#include <iostream>

int main()
{
    std::cout << "Enter a number between 0 and 127: ";
    std::int8_t myInt{};
    std::cin >> myInt;

    std::cout << "You entered: " << static_cast<int>(myInt) << '\n';

    return 0;
}

Przykładowe uruchomienie tego programu:

Enter a number between 0 and 127: 35
You entered: 51

Oto co się dzieje. Kiedy std::int8_t jest traktowane jako znak, procedury wejściowe interpretują nasze dane wejściowe jako sekwencję znaków, a nie liczbę całkowitą. Więc kiedy wprowadzamy 35, tak naprawdę wprowadzamy dwa znaki, '3' i '5'. Ponieważ obiekt char może zawierać tylko jeden znak, '3' jest wyodrębniany ( '5' pozostaje w strumieniu wejściowym do ewentualnego wyodrębnienia później, ponieważ znak '3' ma punkt kodowy ASCII 51, wartość 51 jest przechowywana w myInt, którą następnie drukujemy jako int.

W przeciwieństwie do innych typów o stałej szerokości, zawsze będą one drukowane i wprowadzane jako wartości całkowite.

Czas quizu

Pytanie nr 1

Napisz krótki program, w którym użytkownik będzie proszony o wprowadzenie pojedynczego znaku.Wydrukuj wartość znaku i jego kod ASCII, używając static_cast.

programu wyniki powinny być zgodne z poniższym:

Enter a single character: a
You entered 'a', which has ASCII code 97.

Pokaż rozwiązanie

Pytanie nr 2

Zmodyfikuj program, który napisałeś na potrzeby quizu nr 1, aby używał niejawnej konwersji typów zamiast static_cast. Na ile różnych sposobów możesz to zrobić?

Uwaga: Powinieneś preferować konwersje jawne zamiast konwersji niejawnych, więc tak naprawdę nie rób tego w prawdziwych programach — ma to tylko na celu sprawdzenie, czy rozumiesz, gdzie mogą wystąpić konwersje niejawne.

Pokaż rozwiązanie

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:  
372 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze