Ce message est destiné à être utilisé comme un FAQ concernant la promotion d'entier implicite en C, en particulier la promotion implicite causée par les conversions arithmétiques habituelles et/ou les promotions d'entier.
Exemple 1)
Pourquoi cela donne-t-il un nombre entier étrange et non 255?
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
Exemple 2)
Pourquoi cela donne-t-il "-1 est supérieur à 0"?
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
Exemple 3)
Pourquoi changer le type dans l'exemple ci-dessus en short
résout le problème?
unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
puts("-1 is larger than 0"); // will not print
(Ces exemples étaient destinés à un ordinateur 32 ou 64 bits avec 16 bits courts.)
C a été conçu pour changer implicitement et silencieusement les types entiers des opérandes utilisés dans les expressions. Il existe plusieurs cas où le langage force le compilateur à changer les opérandes en un type plus grand, ou à changer leur signature.
La raison derrière cela est d'empêcher les débordements accidentels pendant l'arithmétique, mais aussi de permettre aux opérandes de signature différente de coexister dans la même expression.
Malheureusement, les règles pour la promotion de type implicite causent beaucoup plus de tort que de bien, au point où elles pourraient être l'un des plus gros défauts du langage C. Ces règles ne sont souvent même pas connues du programmeur C moyen et provoquent donc toutes sortes de bugs très subtils.
En règle générale, vous voyez des scénarios où le programmeur dit "simplement transtypé en type x et cela fonctionne" - mais ils ne savent pas pourquoi. Ou de tels bogues se manifestent comme un phénomène rare et intermittent venant de l'intérieur d'un code apparemment simple et direct. La promotion implicite est particulièrement gênante dans le code faisant des manipulations de bits, car la plupart des opérateurs au niveau du bit en C ont un comportement mal défini lorsqu'ils reçoivent un opérande signé.
Les types entiers en C sont char
, short
, int
, long
, long long
et enum
._Bool
/bool
est également traité comme un type entier lorsqu'il s'agit de promotions de type.
Tous les entiers ont un rang de conversion spécifié . C11 6.3.1.1, je mets l'accent sur les parties les plus importantes:
Chaque type entier a un rang de conversion entier défini comme suit:
- Deux types d'entiers signés ne doivent pas avoir le même rang, même s'ils ont la même représentation.
- Le rang d'un type entier signé doit être supérieur au rang de tout type entier signé avec moins de précision.
- Le rang delong long int
doit être supérieur au rang delong int
, qui doit être supérieur au rang deint
, qui doit être supérieur au rang deshort int
, qui doit être supérieur au rang designed char
.
- Le rang de tout type entier non signé doit être égal au rang du type entier signé correspondant, le cas échéant.
- Le rang de tout type entier standard doit être supérieur au rang de tout type entier étendu de même largeur.
.
- Le rang de _Bool doit être inférieur au rang de tous les autres types entiers standard.
- Le rang de tout type énuméré doit être égal au rang du type entier compatible (voir 6.7.2.2).
Les types de stdint.h
triez ici aussi, avec le même rang que le type auquel ils correspondent sur le système donné. Par exemple, int32_t
a le même rang que int
sur un système 32 bits.
De plus, C11 6.3.1.1 spécifie les types qui sont considérés comme les petits types entiers (pas un terme formel):
Les éléments suivants peuvent être utilisés dans une expression partout où un
int
ouunsigned int
peut être utilisé:- Un objet ou une expression avec un type entier (autre que
int
ouunsigned int
) dont le rang de conversion entier est inférieur ou égal au rang deint
etunsigned int
.
Ce que ce texte quelque peu cryptique signifie dans la pratique, c'est que _Bool
, char
et short
(et aussi int8_t
, uint8_t
etc) sont les "petits types entiers". Ceux-ci sont traités de manière spéciale et soumis à une promotion implicite, comme expliqué ci-dessous.
Chaque fois qu'un petit type entier est utilisé dans une expression, il est implicitement converti en int
qui est toujours signé. Ceci est connu sous le nom de promotions entières ou la règle de promotion entière .
Formellement, la règle dit (C11 6.3.1.1):
Si un
int
peut représenter toutes les valeurs du type d'origine (limité par la largeur, pour un champ de bits), la valeur est convertie enint
; sinon, il est converti enunsigned int
. Celles-ci sont appelées promotions entières .
Cela signifie que tous les petits types entiers, quelle que soit la signature, sont implicitement convertis en (signé) int
lorsqu'ils sont utilisés dans la plupart des expressions.
Ce texte est souvent mal compris: "tous les petits types entiers signés sont convertis en entier signé et tous les petits types entiers non signés sont convertis en entier non signé". Ceci est une erreur. La partie non signée ici signifie seulement que si nous avons par exemple un unsigned short
opérande et int
se trouve avoir la même taille que short
sur le système donné, puis le unsigned short
l'opérande est converti en unsigned int
. Comme dans, rien de remarquable ne se passe vraiment. Mais dans le cas où short
est un type plus petit que int
, il est toujours converti en (signé) int
, quel que soit le court a été signé ou non signé !
La dure réalité causée par les promotions entières signifie que presque aucune opération en C ne peut être effectuée sur de petits types comme char
ou short
. Les opérations sont toujours effectuées sur int
ou sur des types plus grands.
Cela peut sembler absurde, mais heureusement, le compilateur est autorisé à optimiser le code. Par exemple, une expression contenant deux unsigned char
les opérandes obtiendraient les opérandes promus en int
et l'opération effectuée en tant que int
. Mais le compilateur est autorisé à optimiser l'expression pour qu'elle soit réellement exécutée comme une opération 8 bits, comme on pourrait s'y attendre. Cependant, voici le problème: le compilateur n'est pas autorisé à optimiser le changement implicite de signature provoqué par la promotion d'entiers. Parce qu'il n'y a aucun moyen pour le compilateur de savoir si le programmeur s'appuie délibérément sur une promotion implicite pour se produire, ou si elle n'est pas intentionnelle.
C'est pourquoi l'exemple 1 de la question échoue. Les deux opérandes char non signés sont promus au type int
, l'opération est effectuée sur le type int
, et le résultat de x - y
est de type int
. Cela signifie que nous obtenons -1
au lieu de 255
qui aurait pu être attendu. Le compilateur peut générer du code machine qui exécute le code avec des instructions 8 bits au lieu de int
, mais il ne peut pas optimiser le changement de signature. Cela signifie que nous nous retrouvons avec un résultat négatif, ce qui entraîne à son tour un nombre étrange lorsque printf("%u
est invoqué. L'exemple 1 peut être corrigé en redirigeant le résultat de l'opération vers le type unsigned char
.
À l'exception de quelques cas spéciaux comme ++
et sizeof
, les promotions entières s'appliquent à presque toutes les opérations en C, peu importe si des opérateurs unaires, binaires (ou ternaires) sont utilisés.
Chaque fois qu'une opération binaire (une opération avec 2 opérandes) est effectuée en C, les deux opérandes de l'opérateur doivent être du même type. Par conséquent, dans le cas où les opérandes sont de types différents, C impose une conversion implicite d'un opérande au type de l'autre opérande. Les règles pour ce faire sont nommées les conversions artihmétiques habituelles (parfois appelées de manière informelle "équilibrage"). Ceux-ci sont spécifiés en C11 6.3.18:
(Considérez cette règle comme une longue, imbriquée if-else if
déclaration et il pourrait être plus facile à lire :))
6.3.1.8 Conversions arithmétiques habituelles
De nombreux opérateurs qui attendent des opérandes de type arithmétique provoquent des conversions et produisent des types de résultats de manière similaire. Le but est de déterminer un type réel commun pour les opérandes et le résultat. Pour les opérandes spécifiés, chaque opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant est le type réel commun. Sauf indication contraire explicite, le type réel commun est également le type réel correspondant du résultat, dont le domaine de type est le domaine de type des opérandes s'ils sont identiques, et complexe sinon. Ce modèle est appelé les conversions arithmétiques habituelles :
- Tout d'abord, si le type réel correspondant de l'un ou l'autre opérande est
long double
, l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant estlong double
.- Sinon, si le type réel correspondant de l'un des opérandes est
double
, l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant estdouble
.- Sinon, si le type réel correspondant de l'un des opérandes est
float
, l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant est float.Sinon, les promotions entières sont effectuées sur les deux opérandes. Ensuite, les règles suivantes sont appliquées aux opérandes promus:
- Si les deux opérandes ont le même type, aucune conversion supplémentaire n'est nécessaire.
- Sinon, si les deux opérandes ont des types entiers signés ou les deux ont des types entiers non signés, l'opérande avec le type de rang de conversion d'entier inférieur est converti en type d'opérande avec un rang plus élevé.
- Sinon, si l'opérande qui a le type entier non signé a un rang supérieur ou égal au rang du type de l'autre opérande, alors l'opérande avec le type entier signé est converti en type de l'opérande avec le type entier non signé.
- Sinon, si le type de l'opérande de type entier signé peut représenter toutes les valeurs du type de l'opérande de type entier non signé, alors l'opérande de type entier non signé est converti en type d'opérande de type entier signé.
- Sinon, les deux opérandes sont convertis en un type entier non signé correspondant au type de l'opérande avec un type entier signé.
Il convient de noter ici que les conversions arithmétiques habituelles s'appliquent aux variables à virgule flottante et aux variables entières. Dans le cas d'entiers, on peut également noter que les promotions d'entiers sont invoquées à partir des conversions arithmétiques habituelles. Et après cela, lorsque les deux opérandes ont au moins le rang de int
, les opérateurs sont équilibrés sur le même type, avec la même signature.
C'est la raison pourquoi a + b
dans l'exemple 2 donne un résultat étrange. Les deux opérandes sont des entiers et ils sont au moins de rang int
, donc les promotions entières ne s'appliquent pas. Les opérandes ne sont pas du même type - a
is unsigned int
et b
est signed int
. Par conséquent, l'opérateur b
est temporairement converti en type unsigned int
. Au cours de cette conversion, il perd les informations de signe et se retrouve comme une grande valeur.
La raison pour laquelle changer le type en short
dans l'exemple 3 résout le problème, parce que short
est un petit type entier. Cela signifie que les deux opérandes sont des nombres entiers promus au type int
qui est signé. Après la promotion d'entiers, les deux opérandes ont le même type (int
), aucune conversion supplémentaire n'est nécessaire. Et puis l'opération peut être effectuée sur un type signé comme prévu.
Selon le post précédent, je veux donner plus d'informations sur chaque exemple.
Exemple 1)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
Étant donné que le caractère non signé est plus petit que int, nous leur appliquons la promotion entière, puis nous avons (int) x- (int) y = (int) (- 1) et unsigned int (-1) = 4294967295.
La sortie du code ci-dessus: (identique à ce que nous attendions)
4294967295
-1
Comment y remédier?
J'ai essayé ce que le post précédent recommandait, mais cela ne fonctionne pas vraiment. Voici le code basé sur le post précédent:
changez l'un d'eux en entier non signé
int main(){
unsigned int x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
Puisque x est déjà un entier non signé, nous appliquons uniquement la promotion d'entier à y. Ensuite, nous obtenons (unsigned int) x- (int) y. Puisqu'ils n'ont toujours pas le même type, nous appliquons les conversions arithmétiques habituelles, nous obtenons (unsigned int) x- (unsigned int) y = 4294967295.
La sortie du code ci-dessus: (identique à ce que nous attendions):
4294967295
-1
De même, le code suivant obtient le même résultat:
int main(){
unsigned char x = 0;
unsigned int y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
changez les deux en entier non signé
int main(){
unsigned int x = 0;
unsigned int y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
Étant donné que les deux ne sont pas signés int, aucune promotion entière n'est nécessaire. Par la convergence arithmétique habituelle (ont le même type), (entier non signé) x- (entier non signé) y = 4294967295.
La sortie du code ci-dessus: (identique à ce que nous attendions):
4294967295
-1
Une des façons possibles de corriger le code: (ajoutez un type cast à la fin)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
unsigned char z = x-y;
printf("%u\n", z);
}
La sortie du code ci-dessus:
4294967295
-1
255
Exemple 2)
int main(){
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
printf("%u\n", a+b);
}
Étant donné que les deux sont des entiers, aucune promotion d'entier n'est nécessaire. Par la conversion arithmétique habituelle, nous obtenons (entier non signé) a + (entier non signé) b = 1 + 4294967294 = 4294967295.
La sortie du code ci-dessus: (identique à ce que nous attendions)
-1 is larger than 0
4294967295
Comment y remédier?
int main(){
unsigned int a = 1;
signed int b = -2;
signed int c = a+b;
if(c < 0)
puts("-1 is smaller than 0");
printf("%d\n", c);
}
La sortie du code ci-dessus:
-1 is smaller than 0
-1
Exemple 3)
int main(){
unsigned short a = 1;
signed short b = -2;
if(a + b < 0)
puts("-1 is smaller than 0");
printf("%d\n", a+b);
}
Le dernier exemple a résolu le problème car a et b étaient tous deux convertis en int en raison de la promotion entière.
La sortie du code ci-dessus:
-1 is smaller than 0
-1
Si j'ai mélangé certains concepts, faites-le moi savoir. Merci ~