Le cast en C : conversion explicite et conversion implicite #
En C, une valeur change de type bien plus souvent que le programmeur ne l’écrit. Le compilateur convertit tout seul dans une addition, dans un appel de fonction, dans une affectation ; le cast ne fait qu’ajouter une conversion explicite, demandée à la main. Comprendre le cast en C, c’est donc surtout savoir quelles conversions se produisent sans qu’on les demande, et lesquelles la norme définit vraiment.
(type) expression et force une conversion. Une conversion implicite est appliquée par le compilateur selon le contexte (promotions entières, conversions arithmétiques usuelles, affectation, passage d’argument). Les deux suivent les mêmes règles de conversion : le cast ne crée pas un mécanisme à part, il déclenche explicitement celui qui existe déjà.Les fondamentaux : (type) expression et conversions implicites #
La syntaxe canonique du cast est (type) expression. Elle ne modifie pas l’objet d’origine : elle produit une nouvelle valeur du type demandé. Tout le reste du sujet tient dans deux familles de conversions implicites que la norme décrit précisément.
Les promotions entières s’appliquent d’abord aux types de rang inférieur ou égal à int. La norme ISO C les formule ainsi : « If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. » Elle ajoute que « the integer promotions preserve value including sign ». Autrement dit, un char ou un short ne participe jamais directement à un calcul : il est d’abord promu.
À lire Comprendre le cast en C : conversions de type explicites et applications
Viennent ensuite les conversions arithmétiques usuelles, qui déterminent un type commun aux deux opérandes d’un opérateur arithmétique. L’ordre compte : si l’un des opérandes est un long double, on converge vers long double ; sinon vers double ; sinon vers float. Si aucun opérande n’est flottant, les promotions entières sont appliquées aux deux opérandes, puis les règles de type commun entier s’appliquent.
C’est ce mécanisme qui explique le piège le plus classique du langage, la division entière :
#include <stdio.h>
int main(void)
{
int a = 7, b = 2;
printf("a / b = %f\n", (double)(a / b));
printf("(double)a / b = %f\n", (double)a / b);
return 0;
}
a / b = 3.000000(double)a / b = 3.500000
Dans le premier cas, a et b sont deux int : la division est entière et le résultat vaut 3 avant toute conversion vers double. Dans le second, le cast fait de a un double ; les conversions arithmétiques usuelles alignent alors b sur double, et la division est flottante. Depuis C99, la division entière est tronquée vers zéro.
Ce que la norme définit, et ce qu’elle laisse indéfini #
C’est ici que la plupart des articles sur le cast induisent en erreur. Trois expressions circulent comme si elles étaient synonymes — indéfini, non spécifié, défini par l’implémentation — alors qu’elles désignent trois régimes distincts. Un comportement défini par l’implémentation est documenté par le compilateur et reste prévisible sur une plateforme donnée ; un comportement indéfini ne donne aucune garantie et autorise le compilateur à optimiser le code comme si le cas ne pouvait pas survenir.
À lire Aide au codage CIM 10 : Comprendre la classification et ses applications
| Conversion | Régime | Ce que dit la source |
|---|---|---|
| Entier → type non signé trop étroit | Défini | La valeur est ramenée dans l’intervalle du type par ajouts ou soustractions répétés de 2^N : arithmétique modulo. |
| Entier → type signé, valeur non représentable | Défini par l’implémentation | Le résultat est défini par l’implémentation, qui peut aussi lever un signal. |
| Dépassement en arithmétique signée | Indéfini | Le résultat peut boucler, provoquer un piège, ou être supprimé par l’optimiseur. |
| Flottant → entier | Troncature, puis indéfini | La partie fractionnaire est écartée (troncature vers zéro) ; si la partie entière n’est pas représentable, le comportement est indéfini. |
double → float | Défini par l’implémentation | Valeur inchangée si elle est représentable exactement ; sinon arrondi à la valeur voisine, la direction d’arrondi étant définie par l’implémentation. |
La distinction la plus utile en pratique est la première ligne contre la troisième. Convertir un entier négatif vers un type non signé n’a rien d’accidentel : la norme ISO C prévoit que « the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type ». Le résultat est parfaitement déterminé. En revanche, dépasser la capacité d’un type signé au cours d’un calcul est un comportement indéfini — ce n’est pas la même opération, et c’est elle qui casse les programmes.
Pour la conversion flottant vers entier, la norme est tout aussi nette : « the fractional part is discarded (i.e., the value is truncated toward zero). If the value of the integral part cannot be represented by the integer type, the behavior is undefined. » La troncature va donc vers zéro, et non vers le bas : -3.7 devient -3, pas -4.
#include <stdio.h>
int main(void)
{
unsigned char u = (unsigned char)-1;
printf("(unsigned char)-1 = %u\n", u);
printf("(int)-3.7 = %d\n", (int)-3.7);
printf("(int)3.7 = %d\n", (int)3.7);
return 0;
}
(unsigned char)-1 = 255(int)-3.7 = -3(int)3.7 = 3
La première ligne mérite une précision : la valeur obtenue est UCHAR_MAX, c’est-à-dire le modulo décrit par la norme. Elle vaut 255 lorsque l’octet fait 8 bits, ce qui est le cas courant mais reste une propriété de l’implémentation, exposée par CHAR_BIT.
Bonnes pratiques : largeurs de types, pointeurs, lisibilité #
La première règle est de ne jamais écrire une taille de type en dur. La norme ne fixe pas que int occupe quatre octets ni long huit : elle impose des largeurs minimales et laisse le reste à l’implémentation. Les valeurs réelles se lisent dans <limits.h> et se mesurent avec sizeof :
À lire Logitech BCC950 Drivers : Fonctionnement et Optimisation pour la Visio Conférence
#include <stdio.h>
#include <limits.h>
int main(void)
{
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("sizeof(int) = %zu\n", sizeof(int));
printf("INT_MAX = %d\n", INT_MAX);
return 0;
}
Ce programme n’a pas de sortie universelle : c’est précisément ce qu’il démontre. Un code portable interroge <limits.h> plutôt que de supposer une largeur, et raisonne sur INT_MAX plutôt que sur une constante recopiée.
Deuxième point, souvent mal compris : le cast du retour de malloc n’est pas nécessaire en C. La fonction renvoie un void *, et la norme prévoit qu’un pointeur vers void « can be implicitly converted to and from any pointer to object type ». L’écriture int *tab = malloc(10 * sizeof(int)); est donc correcte telle quelle. Ajouter (int *) ne renforce aucune sécurité mémoire ; c’est une habitude venue du C++, où la conversion implicite n’existe pas. Ce qui compte réellement ici, c’est de tester le retour, malloc renvoyant un pointeur nul en cas d’échec.
Troisième point : un cast est une affirmation adressée au compilateur. En écrivant (short)x, on lui demande de ne plus signaler une conversion qu’il aurait diagnostiquée. C’est utile quand la réduction est voulue et maîtrisée ; c’est un moyen commode de faire taire un avertissement légitime dans le cas contraire. D’où trois réflexes utiles : compiler avec les avertissements activés, vérifier avant le cast que la valeur tient dans le type cible, et commenter tout cast dont la raison n’est pas évidente à la lecture.
Enfin, les conversions entre types de pointeurs relèvent d’un régime encore plus strict que les conversions arithmétiques. La norme précise que la conversion d’un pointeur vers un type entier a un résultat défini par l’implémentation, et que le comportement devient indéfini si le résultat n’est pas représentable dans le type entier visé. Un cast de pointeur n’est donc jamais anodin.
À lire VBA List : Fonctionnalités clés et applications pour automatiser Excel
À retenir #
- Le cast
(type) expressiondéclenche explicitement une conversion que le langage applique déjà implicitement dans beaucoup de contextes. - Les promotions entières s’appliquent avant les conversions arithmétiques usuelles ; un
charou unshortne calcule jamais dans son propre type. - Convertir vers un type non signé est défini (modulo). Convertir vers un type signé hors intervalle est défini par l’implémentation. Dépasser la capacité d’un type signé dans un calcul est indéfini.
- Flottant vers entier : troncature vers zéro, et comportement indéfini si la partie entière ne tient pas dans le type cible.
- Aucune taille de type n’est à écrire en dur :
<limits.h>etsizeoffont foi.
Questions fréquentes
Cast et transtypage, est-ce la même chose ?
(type) expression.Le cast modifie-t-il la variable d’origine ?
Pourquoi 7 / 2 donne-t-il 3 et non 3.5 ?
double suffit à rendre l’opération flottante, les conversions arithmétiques usuelles alignant l’autre opérande.Convertir un nombre négatif en type non signé est-il un bug ?
Faut-il caster le retour de malloc ?
void * se convertit implicitement vers tout pointeur d’objet. Le cast est seulement nécessaire si le même code doit être compilé en C++.- ISO/IEC JTC1/SC22/WG14 — C: Approved standards : la norme du langage est ISO/IEC 9899 ; le brouillon public N1570 (12 avril 2011) correspond à C11 et contient les clauses de conversion citées ici.
- Implicit conversions — cppreference.com : promotions entières, conversions arithmétiques usuelles, conversions entre entiers, flottants et pointeurs.
- Integers implementation — Using the GNU Compiler Collection (GCC) : le choix documenté par GCC pour les conversions vers un type signé hors intervalle — réduction modulo 2^N, sans signal levé.