Menu

Rzutowanie typów w C: konwersje niejawne i jawne rzutowanie

C bez przerwy konwertuje wartości między typami: czasem dlatego, że prosisz o to rzutowaniem, a częściej samo z siebie. Oto zasady promocji, składnia jawnego rzutowania i konwersje, które po cichu gubią dane.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

C bez przerwy konwertuje wartości między typami. Część tych konwersji zapisujesz sam za pomocą rzutowania, ale większość kompilator wykonuje po cichu według zasad, których nie wybierasz. Wiedza o tym, która jest która, to różnica między „dlaczego moja średnia to zawsze 3?” a kodem, który robi to, co mówi.

Konwersja niejawna

Za każdym razem, gdy wartość jednego typu trafia w miejsce oczekujące innego, C ją konwertuje:

Konwersje, które nie mogą zgubić informacji (int na double, char na int, short na long), są rozszerzające i zawsze bezpieczne. Konwersje w drugą stronę są zawężające i mogą gubić dane: powyższe 3.9 stało się 3 bez żadnego ostrzeżenia, chyba że poprosisz o nie przez -Wconversion.

Zwykłe konwersje arytmetyczne

Gdy operator dwuargumentowy ma operandy różnych typów, C przed wykonaniem działania konwertuje je do wspólnego typu. Drabina, od dołu:

  1. Wszystko mniejsze niż int (char, short, _Bool) jest promowane do int. To promocja całkowita i dzieje się zawsze jako pierwsza.
  2. Jeśli któraś strona to long double, druga staje się long double.
  3. W przeciwnym razie, jeśli któraś to double, druga staje się double.
  4. W przeciwnym razie, jeśli któraś to float, druga staje się float.
  5. W przeciwnym razie, wśród typów całkowitych wygrywa ten o wyższej randze, a przy remisie wygrywa typ bez znaku.

Ta ostatnia zasada powoduje prawdziwe błędy. Pozostałe są intuicyjne.

Promocja całkowita wyjaśnia, dlaczego arytmetyka na char nie przepełnia się tak, jak można by się spodziewać, oraz dlaczego zapisanie wyniku z powrotem w char już tak.

Jawne rzutowanie

Rzutowanie to typ docelowy w nawiasach:

(double)x
(int)3.9
(char)65
(unsigned int)n

Dotyczy wyrażenia tuż za nim i wiąże bardzo mocno: mocniej niż *, / czy +.

Pierwsza linia dzieli jak liczby całkowite (co daje 3), a potem zamienia 3 na 3.0, czyli za późno. Druga zamienia total na double przed dzieleniem, więc operator / widzi double i int, promuje int i wykonuje dzielenie zmiennoprzecinkowe, które daje 3.5.

Rzutowanie jednego operandu wystarczy. Zwykłe konwersje arytmetyczne zajmą się drugim.

Naprawa dzielenia całkowitego

To najczęstszy powód, by napisać rzutowanie w C:

Linia z procentami jest pouczająca: passed / n to 3 / 5, czyli 0 w liczbach całkowitych, a 0 * 100 to 0. Mnożenie przed dzieleniem (100 * passed / n) naprawia to nawet bez rzutowania, bo 300 / 5 dzieli się bez reszty, ale to działa tylko wtedy, gdy liczby współpracują. Rzutowanie to niezawodna poprawka.

Obcinanie, a nie zaokrąglanie

Rzutowanie wartości zmiennoprzecinkowej na liczbę całkowitą odrzuca część ułamkową. Obcina w stronę zera, nie zaokrągla:

Jeśli budujesz to na własnej maszynie, pamiętaj, że math.h na Linuksie wymaga -lm przy linkowaniu.

Jeszcze jedno zagrożenie: konwersja wartości zmiennoprzecinkowej zbyt dużej dla typu całkowitego to niezdefiniowane zachowanie, a nie zawinięcie. (int)1e20 może dać cokolwiek. Gdy wartość nie jest pod twoją kontrolą, sprawdź zakres przed rzutowaniem.

char i int

char w C jest małą liczbą całkowitą przechowującą kod znaku. Konwersja między nimi to codzienność:

digit - '0' to standardowy idiom zamiany znaku cyfry na jej wartość i działa, bo dziesięć znaków cyfr ma gwarancję, że są kolejne. Dla liter wybieraj toupper() i tolower() z ctype.h zamiast arytmetyki z + 32: to przesunięcie to fakt z ASCII, a nie gwarancja C.

Pokrewna pułapka: funkcje z ctype.h, takie jak isdigit i toupper, przyjmują int, który musi być albo EOF, albo wartością dającą się przedstawić jako unsigned char. Przekazanie ujemnego zwykłego char (co jest możliwe, bo zwykły char może mieć znak) to niezdefiniowane zachowanie. Rzutuj: isdigit((unsigned char)c).

Pułapka signed/unsigned

Krok 5 drabiny konwersji, czyli wygrana typu bez znaku przy remisie, daje najbardziej zaskakujące porównanie w C:

-1 jest konwertowane na unsigned int, co odczytuje jego wzorzec bitów jako 4 294 967 295. To więcej niż 1, więc porównanie jest fałszywe.

Ta sama konwersja sprawia, że pętle kręcą się w nieskończoność:

/* BLAD: i jest bez znaku, wiec i >= 0 jest zawsze prawda. Gdy i wynosi 0, i-- sie zawija. */
for (size_t i = n - 1; i >= 0; i--) { ... }

I że sprawdzanie długości zawodzi:

/* BLAD: strlen zwraca size_t (bez znaku). Jesli napis jest krotszy
   niz 5, len - 5 zawija sie do ogromnej liczby i test przechodzi. */
if (strlen(s) - 5 > 0) { ... }

Przepisz to jako if (strlen(s) > 5), a odejmowanie w ogóle nie nastąpi.

Obrona: trzymaj liczniki i indeksy w jednym rodzaju znakowości, kompiluj z -Wsign-compare (zawarte w -Wextra), a gdy musisz je mieszać, rzutuj jawnie po upewnieniu się, że wartość nie może być ujemna.

Rzutowanie wskaźników

Rzutowanie konwertuje też między typami wskaźników i tu niesie realne ryzyko, bo zmienia sposób, w jaki pamięć jest interpretowana, a nie same bajty.

Na maszynie little-endian to wypisuje 01 00 00 00. Podglądanie reprezentacji obiektu przez unsigned char * to jedno z niewielu rzutowań wskaźników, które standard wprost dopuszcza.

Większość innych rzutowań wskaźników nie jest dopuszczona. Odczyt int przez float * łamie regułę strict aliasing i jest niezdefiniowanym zachowaniem, choć się kompiluje. Do ponownej interpretacji bajtów użyj zamiast tego memcpy.

Dwie konwencje warte poznania. void * konwertuje się w C na dowolny typ wskaźnika na obiekt i z powrotem bez rzutowania, i dlatego nie należy rzutować wyniku malloc:

int *arr = malloc(n * sizeof *arr);        /* poprawne C */
int *arr = (int *)malloc(n * sizeof *arr); /* zbedne; ukrywa brakujacy naglowek */

Rzutowanie jest wymagane w C++ i dlatego tyle kodu je zawiera. W C może ukryć błąd zapomnianego <stdlib.h>.

Natomiast printf("%p", ...) oczekuje void *, więc tam argumenty wskaźnikowe naprawdę wymagają rzutowania: printf("%p", (void *)p).

Kiedy rzutowanie to zła odpowiedź

Rzutowanie ucisza kompilator. Czasem kompilator miał rację.

long big = 5000000000L;
int  small = (int)big;    /* rzutowanie ukrywa realna utrate danych */

Jeśli wartość naprawdę się mieści, rzutowanie dokumentuje, że to sprawdzono. Jeśli może się nie mieścić, rzutowanie zamieniło ostrzeżenie w cichą złą odpowiedź. Zanim je napiszesz, zastanów się, czy poprawką nie jest zmiana typu zmiennej: double zamiast int, size_t zamiast int, long long zamiast long. Rzutowanie to właściwe narzędzie głównie wtedy, gdy dwa poprawne typy muszą się spotkać w jednej operacji, jak w (double)sum / count.

Najczęściej zadawane pytania

Jak rzutować w C?

Umieść typ docelowy w nawiasach przed wartością: (double)x, (int)3.9, (char)65. Rzutowanie dotyczy wyrażenia tuż za nim, więc (double)a / b najpierw konwertuje a, a potem dzieli, natomiast (double)(a / b) dzieli jak liczby całkowite i konwertuje dopiero wynik.

Jak zamienić int na float w C?

Przypisanie robi to niejawnie: double d = 5; zapisuje 5.0. Wewnątrz wyrażenia często potrzebne jest jawne rzutowanie: (double)total / count wymusza dzielenie zmiennoprzecinkowe zamiast całkowitego.

Co się dzieje przy rzutowaniu float na int w C?

Część ułamkowa jest odrzucana: obcinana w stronę zera, nigdy zaokrąglana. (int)3.9 to 3, a (int)-3.9 to -3. Aby zaokrąglić, dla liczb dodatnich dodaj 0.5 przed rzutowaniem albo użyj round(), floor() lub ceil() z math.h.

Dlaczego porównanie int ze znakiem i bez znaku daje złą odpowiedź?

Zwykłe konwersje arytmetyczne w C zamieniają wartość ze znakiem na wartość bez znaku, więc -1 < 1u jest fałszem: -1 staje się ogromną liczbą dodatnią. Trzymaj liczniki i rozmiary w jednym rodzaju znakowości albo rzutuj jawnie po sprawdzeniu, że wartość nie może być ujemna.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ