Conversione di Tipi in Linguaggio C
Nel linguaggio di programmazione C, la conversione dei tipi è un concetto fondamentale che permette di trasformare una variabile di un tipo in un'altra. Questo processo è essenziale per garantire che le operazioni aritmetiche e le assegnazioni tra variabili di tipi diversi avvengano correttamente. Le conversioni di tipi possono essere implicite, quando il compilatore le esegue automaticamente, o esplicite, quando il programmatore le specifica manualmente.
In questo articolo, esploreremo le regole di conversione implicita nel linguaggio C, con particolare attenzione alle conversioni aritmetiche e alle conversioni in fase di assegnamento. Inoltre, discuteremo le specifiche regole di conversione introdotte con lo standard C99. Comprendere queste regole è cruciale per scrivere codice C robusto ed efficiente, evitando errori comuni legati alla gestione dei tipi.
Conversioni implicite dei Tipi
I Computer hanno delle regole molto stringenti per quanto riguarda le operazioni aritmetiche.
Quando un processore deve effettuare un'operazione aritmetica, gli operandi devono essere della stessa dimensione, quindi avere lo stesso numero di bit, e devono essere memorizzati (o meglio codificati) allo stesso modo.
Un processore può, ad esempio, sommare direttamente due interi con segno a 16 bit. Ma non può sommare in maniera diretta un intero senza segno a 16 bit con un intero con segno a 16 bit, oppure un intero a 16 bit con un intero a 32 bit. O peggio ancora, non può sommare direttamente un intero a 32 bit con un numero in virgola mobile a 32 bit.
In linguaggio C, invece, abbiamo visto che possiamo mescolare nelle espressioni variabili di tipi diversi. Possiamo, infatti, combinare variabili intere, variabili in virgola mobile e variabili di tipo carattere in una singola espressione. Ad esempio:
int a = 5;
float b = 3.14;
char c = 'A';
float d = a + b;
int e = a + c;
Ma come riesce il compilatore C a gestire queste espressioni miste?
La risposta è che il compilatore, quando incontra un'espressione con operandi di tipi diversi, inserisce delle istruzioni di conversione per convertire gli operandi in un formato comune. In altre parole, senza che noi dobbiamo indicarlo esplicitamente, il compilatore C genera delle istruzioni di conversione in maniera tale che, successivamente, il processore possa eseguire l'operazione aritmetica.
Ad esempio, se proviamo a sommare un intero short
a 16 bit con un intero int
a 32, il compilatore C inserirà delle istruzioni di conversione per convertire l'intero short
in un intero int
a 32 bit. In questo modo, il processore potrà sommare i due interi senza problemi.
Analogamente, se proviamo a sommare un int
ed un float
, il compilatore C inserirà delle istruzioni di conversione per convertire l'intero in un numero in virgola mobile. In questo caso le istruzioni di conversione saranno molto più complesse in quanto la rappresentazione di un numero in virgola mobile è molto diversa da quella di un intero. Ma il compilatore C si occuperà di tutto ciò in maniera trasparente per noi.
Dal momento che il compilatore gestisce queste conversioni senza il coinvolgimento del programmatore e in modo del tutto trasparente, queste conversioni vengono chiamate conversioni implicite.
Conversione Implicita di tipo
In linguaggio C, la conversione implicita è una conversione del tipo di dati che avviene automaticamente da parte del compilatore senza che il programmatore debba esplicitamente richiederla.
Esiste anche un altro tipo di conversione, chiamata conversione esplicita, che vedremo nella prossima lezione. Si chiama così perché il programmatore deve esplicitamente indicare al compilatore che vuole convertire un tipo di dato in un altro.
Le regole di conversione implicita sono alquanto complesse purtroppo dato che il linguaggio C ha svariati tipi numerici. In questa lezione vedremo come funzionano.
Situazioni in cui si verificano le conversioni implicite
Le conversioni implicite effettuate dal C vengono applicate in queste quattro situazioni:
-
Quando gli operandi di un'operazione aritmetica non sono dello stesso tipo.
Ad esempio, quando sommiamo un intero ed un numero in virgola mobile. In questo caso, il compilatore C effettua le cosiddette conversioni aritmetiche.
-
Quando il tipo di un'espressione a destra di un'assegnazione è diverso dal tipo della variabile a sinistra dell'assegnazione.
Ad esempio, quando assegniamo un numero in virgola mobile ad una variabile intera. In questo caso, il compilatore C effettua le cosiddette conversioni in fase di assegnamento.
-
Quando passiamo un argomento ad una funzione di un tipo diverso dal tipo del parametro della funzione.
Ad esempio, quando passiamo un intero ad una funzione che accetta un numero in virgola mobile. In questo caso, il compilatore C effettua le cosiddette conversioni in fase di chiamata di funzione. Studieremo questa situazione nelle prossime lezioni quando andremo a studiare le funzioni in linguaggio C.
-
Quando restituiamo un valore da una funzione di un tipo diverso dal tipo di ritorno della funzione.
Ad esempio, quando restituiamo un numero in virgola mobile da una funzione che dovrebbe restituire un intero. In questo caso, il compilatore C effettua le cosiddette conversioni in fase di ritorno di funzione. Anche in questo caso, studieremo questa situazione nelle prossime lezioni.
Per il momento, ci concentreremo sulle prime due situazioni, ovvero le conversioni aritmetiche e le conversioni in fase di assegnamento.
Conversioni aritmetiche
Le conversioni aritmetiche in C sono applicate, tipicamente, quando si adoperano operatori binari tra operandi di tipo diverso. Ciò si applica a tutti i tipi binari, siano essi aritmetici, logici o relazionali.
Per capire la logica dietro le conversioni implicite aritmetiche, prendiamo un esempio:
int a = 5;
float b = 3.14;
a + b;
Abbiamo, in questo esempio, la somma tra un intero int
ed un numero in virgola mobile float
. Il compilatore C si trova di fronte alla prima situazione vista sopra: gli operandi di un'operazione aritmetica non sono dello stesso tipo.
Cosa fa il compilatore in questo caso?
Ci sono due possibili soluzioni:
-
Convertire il tipo
float
inint
, ottenendo due interi e sommandoli tra di loro.Questa è una soluzione possibile ma non è quella migliore. Infatti, se convertiamo il
float
inint
si possono verificare due problemi:- In primo luogo, andremmo a perdere la parte decimale del numero in virgola mobile. In questo caso, il numero
3.14
verrebbe convertito in3
, e quindi il risultato della somma sarebbe5 + 3 = 8
ottenendo una catastrofica perdita di precisione. - In secondo luogo, non è detto che un
float
sia rappresentabile con un numero intero. Prendiamo ad esempio il numero2.5e56
. Si tratta di un numero talmente grande che non può essere rappresentato con un intero a 32 bit. In questo caso, la conversione dafloat
aint
sarebbe impossibile.
- In primo luogo, andremmo a perdere la parte decimale del numero in virgola mobile. In questo caso, il numero
-
Convertire il tipo
int
infloat
, ottenendo due numeri in virgola mobile e sommandoli tra di loro.Questa è la soluzione migliore. Infatti, convertendo l'intero in un numero in virgola mobile, non andiamo a perdere nessuna informazione. Inoltre, un numero intero può essere sempre convertito in
float
a meno di una leggera perdita di cifre significative nel caso in cui l'intero sia troppo grande.
Il compilatore C, quindi, sceglie la seconda soluzione e converte l'intero int
in un numero in virgola mobile float
. In questo modo, il processore può sommare i due numeri in virgola mobile senza problemi.
Da ciò possiamo capire quale sia la strategia generale per la conversione implicita dei tipi aritmetici in C:
Strategia di Conversione Aritmetica Implicita
In linguaggio C, quando un operatore binario viene applicato a due operandi di tipo differente, il compilatore C converte gli operandi al tipo più piccolo in grado di contenerli entrambe.
In altre parole, il compilatore sceglie il tipo con il minor numero di byte in grado di rappresentare entrambe gli operandi con la minima perdita di informazione.
In generale, nella maggior parte dei casi, la conversione implicita si traduce in una estensione ad un tipo simile ma che occupa più byte. Ad esempio, sommare uno short
ad un int
consiste semplicemente nell'estendere lo short
aggiungendo byte a sinistra fino a raggiungere la dimensione di un int
. Del resto sia short
che int
sono tipi interi ed adoperano, a meno del numero di bit, la stessa rappresentazione in complemento a due.
Questo tipo di conversione prende il nome di Promozione dei Tipi:
Promozione dei Tipi
La promozione dei tipi è una conversione implicita che consiste nell'estendere un tipo di dato ad un tipo che occupa più byte ma che adopera la stessa rappresentazione.
Da notare che si parla di promozione quando i tipi sono simili, ovvero entrambi interi o entrambi in virgola mobile. Non si parla di promozione quando convertiamo un intero in un numero in virgola mobile, o viceversa, in quanto i due tipi sono molto diversi.
Inoltre, in alcuni casi promozione avviene sempre. Ad esempio, quando un operando è applicato a due short
o due char
, questi ultimi vengono sempre prima convertiti in int
e poi si applica l'operatore. Questo perché il processore lavora più velocemente con interi a 32 bit che con interi a 16 o 8 bit. Questo tipo di promozione prende il nome di Promozione Integrale.
Promozione Integrale
La promozione integrale è una conversione implicita che consiste nell'estendere un tipo di dato ad un tipo intero a 32 bit.
Essa viene sempre applicata quando si adoperano due tipi di dati char
o short
in un'espressione aritmetica.
Regole di Conversione Aritmetica
Adesso analizziamo le regole di conversione aritmetica applicate ad un'espressione composta da un operatore binario e due operandi di tipo diverso.
Possiamo dividere tali regole in due gruppi:
-
Quando uno o entrambi gli operandi sono in virgola mobile:
In tal caso si adopera lo schema che segue:
In altre parole, si ragiona così:
- Se un operando è di tipo
long double
, l'altro operando viene convertito inlong double
. - Altrimenti, se un operando è di tipo
double
, l'altro operando viene convertito indouble
. - Altrimenti, se un operando è di tipo
float
, l'altro operando viene convertito infloat
.
Queste tre regole coprono anche il caso misto. Infatti, se uno dei due operandi è di tipo
double
ma l'altro è di tipo intero, il compilatore converte l'intero indouble
e poi somma i due numeri in virgola mobile. - Se un operando è di tipo
-
Quando entrambi gli operandi sono interi:
In tal caso si adopera lo schema che segue:
Per prima cosa, si applica, se possibile, la promozione integrale vista sopra. In questo modo abbiamo la garanzia che nessuno dei due operandi sia di tipo
char
oshort
.Successivamente, si sceglie il tipo di intero più piccolo in grado di contenere entrambi gli operandi.
Conversione Aritmetica e tipi interi Senza Segno
Quando, in un'espressione aritmetica, si combinano tipi interi con segno e tipi senza segno, le regole si complicano leggermente.
Prendiamo l'esempio che segue:
unsigned int a = 5;
int b = 3;
a + b;
In questo caso, abbiamo un intero senza segno unsigned int
e un intero con segno int
. Entrambe usano lo stesso numero di bit. Quali regole di conversione vengono applicate in questo caso?
In tal caso accade qualcosa che, a prima vista, può sembrare contro-intuitivo. Infatti, il compilatore C converte l'intero con segno int
in un intero senza segno unsigned int
. In questo modo, il processore può sommare i due interi senza problemi.
Questa regola si applica però solo nel caso in cui i due tipi usino lo stesso numero di bit. Infatti, se sommiamo un unsigned int
e un long int
, il compilatore converte l'unsigned int
in un long int
e non viceversa in quanto l'unsigned int
è più piccolo del long int
.
Regola di Conversione Aritmetica per tipi interi Senza Segno
Quando, in un'espressione aritmetica, si combinano un intero con segno e un intero senza segno, il compilatore C converte l'intero con segno in un intero senza segno se i due tipi usano lo stesso numero di bit.
Nel caso in cui il tipo con segno abbia un numero di bit maggiore del tipo senza segno, il compilatore converte l'intero senza segno nell'intero con segno.
Questa conversione, però, può provocare errori molto difficili da scovare.
Infatti, se l'intero con segno è positivo, la conversione non provoca problemi. Ma se l'intero con segno è negativo, la conversione in un intero senza segno può portare a risultati inaspettati.
Per chiarire, prendiamo un esempio:
unsigned int a = 10;
int b = -5;
if (a > b) {
printf("a è maggiore di b\n");
} else {
printf("b è maggiore di a\n");
}
A prima vista, se osserviamo il codice scritto sopra potremmo pensare che l'output del programma sia sempre a è maggiore di b
.
Ma non accade questo.
Infatti, il compilatore converte l'intero con segno b
in un unsigned int
. Nel fare questo, però, quello che accade è che a b
viene sommato il valore
Ciò accade perché, l'ultimo bit di b
che viene adoperato per rappresentare il segno, quando viene convertito in un intero senza segno, viene interpretato come un bit di peso maggiore. In questo modo, il valore di b
viene interpretato come
Se un int
è rappresentato con 32 bit, allora ciò che accade è che nella conversione b
diventa:
Quindi, nel confronto tra a
e b
, b
risulterà maggiore di a
!
Evitare di combinare tipi con segno e senza segno in un'espressione aritmetica
Il consiglio che diamo è di evitare di combinare tipi con segno e senza segno in un'espressione aritmetica. Questo perché le conversioni implicite possono portare a risultati inaspettati.
Alcuni compilatori segnalano questa situazione come un warning. Ad esempio, il compilatore gcc
segnala:
warning: comparison between signed and unsigned
Se si riceve un warning di questo tipo, è bene rivedere il codice e cercare di evitare di combinare tipi con segno e senza segno.
Esempio riassuntivo di Conversione Aritmetica
Ricapitoliamo le regole di conversione aritmetica con il seguente esempio riassuntivo:
char c;
short int s;
int i;
unsigned int u;
long int l;
unsigned long int ul;
float f;
double d;
long double ld;
/* ... */
i = i + c; /* c è convertito in int */
i = i + s; /* s è convertito in int */
u = u + i; /* i è convertito in unsigned int */
l = l + u; /* u è convertito in long int */
ul = ul + l; /* l è convertito in unsigned long int */
f = f + ul; /* ul è convertito in float */
d = d + f; /* f è convertito in double */
ld = ld + d; /* d è convertito in long double */
In questo esempio, abbiamo una serie di variabili di tipi diversi. In ogni riga, abbiamo un'assegnazione di un'espressione aritmetica ad una variabile. In ogni espressione aritmetica, il compilatore C converte gli operandi in modo da poter eseguire l'operazione aritmetica.
Conversioni in fase di Assegnamento
Le regole viste sopra per convertire implicitamente i tipi aritmetici in un'espressione non si applicano durante l'assegnamento.
Il linguaggio C, invece, segue la seguente semplice regola:
Regola di Conversione in fase di Assegnamento
In un assegnamento in linguaggio C, l'espressione a destra dell'operatore di assegnamento viene convertita nel tipo della variabile a sinistra dell'assegnamento.
tipo x = espressione
L'espressione viene convertita nel tipo tipo
prima di essere assegnata alla variabile x
.
Si possono verificare, a questo punto, due casi:
-
La variabile è di un tipo in grado di contenere il risultato:
In tal caso la conversione avviene senza problemi. Ad esempio:
char c; int i; float f; double d; i = c; /* c viene convertito in int */ f = i; /* i viene convertito in float */ d = f; /* f viene convertito in double */
Nell'esempio di sopra, in ogni assegnamento la variabile di destinazione è sempre più grande del risultato. Ossia,
int
è più grande dichar
,float
è più grande diint
edouble
è più grande difloat
. Quindi le conversioni implicite non hanno ripercussioni. -
La variabile non è in grado di contenere il risultato:
Prendiamo un esempio in cui assegniamo un valore floating point ad un intero:
int i = 3.14;
In questo caso, il compilatore C converte il numero in virgola mobile
3.14
in un intero rimuovendo la parte frazionaria. Per cui, la variabilei
varrà3
.Attenzione che non si tratta di un'arrotondamento. Si tratta di un troncamento puro e semplice. Per cui:
int i = 8.9999;
La variabile
i
varrà8
e non sarà arrotondata a9
.Inoltre, si possono verificare situazioni in cui l'assegnamento è privo di significato oppure, nel caso peggiore, il risultato è al di fuori dell'intervallo rappresentabile da quel tipo.
Ad esempio:
char c = 10000;
In questo caso, il numero
10000
è troppo grande per essere rappresentato con unchar
che è in grado di rappresentare numeri da 0 a 255. Il risultato pertanto, sarà un troncamento di cifre significative e la variabilec
varrà16
.Questo perché in binario il numero
è: Ma il tipo
char
può contenere solo le prime 8 cifre binarie meno significative:Abbiamo ottenuto un numero completamente diverso!
Regole di Conversione implicita in C99
Le regole di conversione implicita si complicano leggermente nello standard C99.
La motivazione sta nel fatto che il C99 introduce sia il tipo booleano _Bool
che il nuovo tipo intero long long
. Inoltre aggiunge anche il tipo complesso che vedremo più avanti.
Per poter definire le regole di conversione, in C99 è stato introdotta la cosiddetta gerarchia di conversione dei tipi interi. Questa gerarchia definisce l'ordine di conversione tra i tipi interi ed è così definita dal livello più alto a quello più basso:
long long int
eunsigned long long int
long int
eunsigned long int
int
eunsigned int
short int
eunsigned short int
char
,signed char
eunsigned char
_Bool
La regola di Promozione Integrale viene così ridefinita in C99:
Promozione Integrale in C99
Tutti i tipi interi che si trovano nella gerarchia di conversione dei tipi interi al di sotto del livello di int
e unsigned int
vengono automaticamente promossi a:
int
se si riesce a rappresentare il valore con unint
;unsigned int
altrimenti.
Detto questo, possiamo ora definire le regole di conversione implicita in C99:
-
Se uno dei due operandi è di un tipo in virgola mobile valgono le stesse regole viste sopra:
- Se uno dei due operandi è di tipo
long double
, l'altro operando viene convertito inlong double
. - Altrimenti, se uno dei due operandi è di tipo
double
, l'altro operando viene convertito indouble
. - Altrimenti, se uno dei due operandi è di tipo
float
, l'altro operando viene convertito infloat
.
- Se uno dei due operandi è di tipo
-
Se entrambi gli operandi sono di tipo intero si applicano le seguenti regole:
Prima si effettua la promozione integrale se necessario.
Se i tipi dei due operandi sono, dopo il primo passo, uguali il processo di conversione termina.
Altrimenti, si usano le seguenti regole fermandosi alla prima che ha una corrispondenza:
- Se un operando è con segno mentre l'altro è senza segno, si converte l'operando con il tipo inferiore nella gerarchia nel tipo dell'operando con il tipo superiore.
- Se l'operando senza segno ha un tipo più alto nella gerarchia oppure è allo stesso livello dell'operando con segno, si converte l'operando con segno nel tipo dell'operando senza segno.
- Se il tipo dell'operando con segno può rappresentare tutti i valori del tipo dell'operando senza segno, si converte l'operando senza segno nel tipo dell'operando con segno.
- Altrimenti, si convertono entrambe gli operandi nel tipo senza segno corrispondente nello stesso livello della gerarchia del tipo con segno.
In Sintesi
In questa lezione abbiamo visto come il compilatore C gestisce le conversioni implicite dei tipi.
Abbiamo visto che:
- Il compilatore C inserisce delle istruzioni di conversione per convertire gli operandi in un formato comune quando incontra un'espressione con operandi di tipi diversi.
- Queste conversioni vengono chiamate conversioni implicite.
- Esistono quattro situazioni in cui si verificano le conversioni implicite: le conversioni aritmetiche, le conversioni in fase di assegnamento, le conversioni in fase di chiamata di funzione e le conversioni in fase di ritorno di funzione.
- Le conversioni aritmetiche sono applicate quando si adoperano operatori binari tra operandi di tipo diverso.
- La strategia di conversione aritmetica implicita in C consiste nel convertire gli operandi al tipo più piccolo in grado di contenerli entrambi.
- La promozione dei tipi è una conversione implicita che consiste nell'estendere un tipo di dato ad un tipo che occupa più byte ma che adopera la stessa rappresentazione.
- La promozione integrale è una conversione implicita che consiste nell'estendere un tipo di dato ad un tipo intero a 32 bit.
- Quando si combinano tipi con segno e senza segno in un'espressione aritmetica, il compilatore C converte l'intero con segno in un intero senza segno se i due tipi usano lo stesso numero di bit.
- Le regole di conversione in fase di assegnamento in C prevedono che l'espressione a destra dell'operatore di assegnamento venga convertita nel tipo della variabile a sinistra dell'assegnamento.
- In C99, le regole di conversione implicita si complicano leggermente a causa dell'introduzione del tipo booleano
_Bool
e del tipo interolong long
.
Nella prossima lezione vedremo come effettuare le conversioni esplicite dei tipi in linguaggio C: questo tipo di conversione prendono il nome di casting.