Menu

Casting in C: conversioni implicite e cast espliciti

Il C converte continuamente tra tipi, a volte perché l'hai chiesto con un cast e più spesso di sua iniziativa. Ecco le regole di promozione, la sintassi del cast esplicito e le conversioni che perdono dati in silenzio.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Il C converte continuamente i valori da un tipo all'altro. Alcune di queste conversioni le scrivi tu con un cast; la maggior parte le esegue il compilatore in silenzio, secondo regole che non hai scelto tu. Sapere quali sono quali fa la differenza tra "perché la mia media fa sempre 3?" e codice che fa quello che dice.

Conversione implicita

Ogni volta che un valore di un tipo incontra un contesto che se ne aspetta un altro, il C lo converte:

Le conversioni che non possono perdere informazioni (int in double, char in int, short in long) sono di ampliamento e sono sempre sicure. Quelle nella direzione opposta sono di restringimento e possono perdere dati: il 3.9 qui sopra è diventato 3 senza alcun avviso, a meno che tu non lo chieda con -Wconversion.

Le conversioni aritmetiche abituali

Quando un operatore binario ha operandi di tipi diversi, il C li converte in un tipo comune prima di fare il calcolo. La scala, dal basso verso l'alto:

  1. Tutto ciò che è più piccolo di int (char, short, _Bool) viene promosso a int. È la promozione degli interi, e avviene prima di tutto, sempre.
  2. Se uno dei due lati è long double, l'altro diventa long double.
  3. Altrimenti, se uno dei due è double, l'altro diventa double.
  4. Altrimenti, se uno dei due è float, l'altro diventa float.
  5. Altrimenti, tra i tipi interi, vince quello di rango più alto, e a parità di rango vince il senza segno.

Quest'ultima regola è quella che causa bug veri. Le altre sono intuitive.

La promozione degli interi è il motivo per cui l'aritmetica sui char non va in overflow come ti aspetteresti, e anche il motivo per cui rimettere il risultato in un char sì.

Cast espliciti

Un cast è il tipo di destinazione tra parentesi:

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

Si applica all'espressione che lo segue immediatamente, e lega molto forte: più di *, / o +.

La prima riga divide tra interi (ottenendo 3) e poi converte 3 in 3.0: troppo tardi. La seconda converte total in double prima della divisione, così l'operatore / vede un double e un int, promuove l'int ed esegue una divisione in virgola mobile.

Fare il cast di un solo operando basta. Le conversioni aritmetiche abituali pensano all'altro.

Correggere la divisione intera

È in assoluto il motivo più comune per scrivere un cast in C:

La riga della percentuale è istruttiva: passed / n è 3 / 5, che tra interi vale 0, e 0 * 100 fa 0. Moltiplicare prima di dividere (100 * passed / n) risolve anche senza cast, perché 300 / 5 è esatto, ma funziona solo quando i numeri collaborano. Il cast è la correzione affidabile.

Troncamento, non arrotondamento

Fare il cast di un valore in virgola mobile a un intero scarta la parte frazionaria. Tronca verso lo zero, non arrotonda:

Se lo compili sulla tua macchina, ricorda che su Linux math.h richiede -lm in fase di linking.

Un altro pericolo: convertire un valore in virgola mobile troppo grande per il tipo intero è comportamento indefinito, non un giro a capo. (int)1e20 può produrre qualsiasi cosa. Controlla l'intervallo prima del cast quando il valore non è sotto il tuo controllo.

char e int

Un char in C è un piccolo intero che contiene il codice di un carattere. Convertire tra i due è lavoro quotidiano:

digit - '0' è l'idioma standard per trasformare un carattere cifra nel suo valore, e funziona perché le dieci cifre sono garantite consecutive. Per le lettere, preferisci toupper() e tolower() di ctype.h all'aritmetica con + 32: lo scarto è un fatto dell'ASCII, non una garanzia del C.

Una trappola collegata: le funzioni di ctype.h come isdigit e toupper ricevono un int che deve essere EOF oppure rappresentabile come unsigned char. Passare un char semplice negativo (possibile, dato che un char semplice può avere il segno) è indefinito. Fai il cast: isdigit((unsigned char)c).

La trappola signed/unsigned

Il passo 5 della scala di conversione, cioè che a parità vince il senza segno, produce il confronto più sorprendente del C:

-1 viene convertito in unsigned int, che reinterpreta la sua sequenza di bit come 4.294.967.295. È maggiore di 1, quindi il confronto è falso.

La stessa conversione fa girare i cicli all'infinito:

/* BUG: i e senza segno, quindi i >= 0 e sempre vero. Quando i vale 0, i-- fa il giro. */
for (size_t i = n - 1; i >= 0; i--) { ... }

E fa fallire i controlli sulla lunghezza:

/* BUG: strlen restituisce size_t (senza segno). Se la stringa e piu corta
   di 5, len - 5 fa il giro fino a un numero enorme e il controllo passa. */
if (strlen(s) - 5 > 0) { ... }

Riscrivilo come if (strlen(s) > 5) e la sottrazione non avviene mai.

Le difese: tieni conteggi e indici con un solo tipo di segno ovunque, compila con -Wsign-compare (incluso in -Wextra) e, quando devi mescolare, fai un cast esplicito dopo aver stabilito che il valore non può essere negativo.

Cast tra puntatori

I cast convertono anche tra tipi di puntatore, e qui comportano un rischio reale, perché cambiano il modo in cui la memoria viene interpretata, non i byte stessi.

Su una macchina little-endian stampa 01 00 00 00. Ispezionare la rappresentazione di un oggetto tramite un unsigned char * è uno dei pochi cast tra puntatori che lo standard approva esplicitamente.

La maggior parte degli altri cast tra puntatori non lo è. Leggere un int tramite un float * viola la regola di strict aliasing ed è comportamento indefinito anche se compila; per reinterpretare i byte usa invece memcpy.

Due convenzioni da conoscere. In C void * si converte da e verso qualsiasi tipo di puntatore a oggetto senza cast, ed è per questo che non dovresti fare il cast del risultato di malloc:

int *arr = malloc(n * sizeof *arr);        /* C corretto */
int *arr = (int *)malloc(n * sizeof *arr); /* inutile; nasconde un header mancante */

In C++ il cast è obbligatorio, ed è per questo che tanto codice lo contiene. In C può nascondere l'errore di aver dimenticato <stdlib.h>.

E printf("%p", ...) si aspetta un void *, quindi lì gli argomenti puntatore hanno davvero bisogno di un cast: printf("%p", (void *)p).

Quando un cast è la risposta sbagliata

Un cast zittisce il compilatore. A volte il compilatore aveva ragione.

long big = 5000000000L;
int  small = (int)big;    /* il cast nasconde una vera perdita di dati */

Se il valore ci sta davvero, il cast documenta che hai controllato. Se potrebbe non starci, il cast ha trasformato un avviso in una risposta sbagliata silenziosa. Prima di scriverne uno, chiediti se la soluzione non sia cambiare il tipo di una variabile: double invece di int, size_t invece di int, long long invece di long. Un cast è lo strumento giusto soprattutto quando due tipi corretti devono incontrarsi per una singola operazione, come in (double)sum / count.

Domande frequenti

Come si fa un cast in C?

Metti il tipo di destinazione tra parentesi prima del valore: (double)x, (int)3.9, (char)65. Il cast si applica all'espressione che lo segue immediatamente, quindi (double)a / b converte prima a e poi divide, mentre (double)(a / b) divide tra interi e converte il risultato.

Come si converte un int in float in C?

L'assegnazione lo fa in modo implicito: double d = 5; memorizza 5.0. Dentro un'espressione spesso serve un cast esplicito: (double)total / count forza la divisione in virgola mobile invece della divisione intera.

Cosa succede quando fai il cast di un float a int in C?

La parte frazionaria viene scartata: si tronca verso lo zero, non si arrotonda mai. (int)3.9 vale 3 e (int)-3.9 vale -3. Per arrotondare, aggiungi 0.5 prima del cast per i positivi, oppure usa round(), floor() o ceil() di math.h.

Perché confrontare un int con segno e uno senza segno dà la risposta sbagliata?

Le conversioni aritmetiche abituali del C convertono il valore con segno in senza segno, quindi -1 < 1u è falso: -1 diventa un numero positivo enorme. Tieni conteggi e dimensioni con un solo tipo di segno, oppure fai un cast esplicito dopo aver verificato che il valore non può essere negativo.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA