98 votes

Règles implicites de promotion de type

Cet article est destiné à être utilisé comme une FAQ concernant la promotion implicite des entiers en C, en particulier la promotion implicite causée par les conversions arithmétiques habituelles et/ou les promotions d'entiers.

Exemple 1)
Pourquoi cela donne-t-il un grand 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 plus grand que 0" ?

unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
  puts("-1 is larger than 0");

Exemple 3)
Pourquoi le fait de changer le type dans l'exemple ci-dessus en short régler 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 de 32 ou 64 bits avec un short de 16 bits).

3 votes

Je suggère de documenter les hypothèses pour les exemples, par exemple, l'exemple 3 suppose que short est plus étroite que int (ou en d'autres termes, elle suppose que int peut représenter toutes les valeurs de unsigned short ).

0 votes

Attendez une seconde, l'OP est le même gars qui a répondu à la question ? Il est dit que Lundin a demandé, la meilleure réponse est aussi celle de Lundin lol.

4 votes

@savram Oui, l'intention est d'écrire une entrée de FAQ. Le partage des connaissances de cette manière convient à l'OS - la prochaine fois que vous posez une question, notez la case à cocher "répondre à votre propre question". Mais bien sûr, la question est toujours traitée comme n'importe quelle autre question et d'autres peuvent également publier des réponses. (Et vous ne gagnez pas de réputation en acceptant votre propre réponse).

136voto

Lundin Points 21616

C a été conçu pour changer implicitement et silencieusement les types d'entiers des opérandes utilisés dans les expressions. Il existe plusieurs cas où le langage force le compilateur à changer les opérandes vers un type plus grand, ou à changer leur signature.

Le but est d'éviter les débordements accidentels pendant l'arithmétique, mais aussi de permettre à des opérandes de signe différent de coexister dans la même expression.

Malheureusement, les règles de promotion de type implicite causent beaucoup plus de mal que de bien, au point qu'elles pourraient être l'un des plus grands 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 bogues très subtils.

En général, on voit des scénarios où le programmeur dit "il suffit de faire un casting avec le type x et ça marche" - mais il ne sait pas pourquoi. Ou encore, ces bogues se manifestent sous la forme d'un phénomène rare et intermittent dans un code apparemment simple et direct. La promotion implicite est particulièrement gênante dans le code faisant des manipulations de bits, puisque la plupart des opérateurs de type bit-wise en C ont un comportement mal défini lorsqu'ils reçoivent un opérande signé.


Types de nombres entiers et rang de conversion

Les types d'entiers en C sont char , short , int , long , long long y enum .
_Bool / bool est également traité comme un type entier lorsqu'il s'agit de promotions de type.

Tous les nombres entiers ont une valeur rang de conversion . C11 6.3.1.1, j'insiste sur les parties les plus importantes :

Chaque type d'entier possède un rang de conversion d'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 d'entier signé doit être supérieur au rang de tout type d'entier signé de moindre précision.
- Le rang de long long int est supérieur au rang de long int qui doit être supérieur au rang de int qui doit être supérieur au rang de short int qui doit être supérieur au rang de signed char .
- Le rang de tout type d'entier non signé doit être égal au rang du type d'entier signé correspondant, le cas échéant.

- Le rang de tout type d'entier standard doit être supérieur au rang de tout type d'entier étendu de même largeur.
- Le rang des caractères doit être égal au rang des caractères signés et des caractères non signés.
- Le rang de _Bool doit être inférieur à celui de tous les autres types d'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 sont également classés ici, 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.

En outre, l'article C11 6.3.1.1 précise les types qui sont considérés comme les types de petits nombres entiers (terme non officiel) :

Les éléments suivants peuvent être utilisés dans une expression chaque fois qu'un élément int o unsigned int mai être utilisés :

- Un objet ou une expression avec un type entier (autre que int o unsigned int ) dont le rang de conversion en entier est inférieur ou égal au rang de int y unsigned int .

Ce que ce texte quelque peu cryptique signifie en pratique, c'est que _Bool , char y 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.


Les promotions entières

Chaque fois qu'un petit type d'entier est utilisé dans une expression, il est implicitement converti en int qui est toujours signé. C'est ce que l'on appelle le promotions entières o la règle de promotion des nombres entiers .

Formellement, la règle dit (C11 6.3.1.1) :

Si un int peut représenter toutes les valeurs du type original (limité par la largeur, pour un champ de bits), la valeur est convertie en un champ int ; sinon, il est converti en un unsigned int . On les appelle les promotions entières .

Cela signifie que tous les petits types d'entiers, quel que soit leur signe, sont implicitement convertis en (signés) int lorsqu'il est utilisé dans la plupart des expressions.

Ce texte est souvent compris à tort comme : "tous les petits types d'entiers signés sont convertis en int signés et tous les petits types d'entiers non signés sont convertis en int non signés". Ceci est incorrect. La partie non signée signifie seulement que si nous avons, par exemple, un type unsigned short l'opérande, et int se trouve avoir la même taille que short sur le système donné, alors le unsigned short est converti en unsigned int . Comme dans, rien de notable ne se passe vraiment. Mais au cas où short est un type plus petit que int il est toujours converti en (signé) int , que le raccourci soit signé ou non signé. !

La dure réalité causée par les promotions d'entiers signifie que presque aucune opération en C ne peut être effectuée sur de petits types comme char o short . Les opérations sont toujours effectuées sur int ou 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 seraient promus au rang de int et l'opération effectuée comme int . Mais le compilateur est autorisé à optimiser l'expression pour qu'elle soit effectivement exécutée comme une opération à 8 bits, comme on pourrait s'y attendre. Cependant, voici le problème : le compilateur est pas est autorisé à optimiser le changement implicite de signe causé par la promotion des entiers. Parce qu'il n'y a aucun moyen pour le compilateur de dire si le programmeur compte délibérément sur la promotion implicite pour se produire, ou si c'est involontaire.

C'est pourquoi l'exemple 1 de la question échoue. Les deux opérandes unsigned char sont promus au type int l'opération s'effectue sur le type int et le résultat de x - y est de type int . Ce qui signifie que nous obtenons -1 au lieu de 255 ce qui aurait pu être attendu. Le compilateur peut générer un code machine qui exécute le code avec des instructions de 8 bits au lieu des instructions de 10 bits. int mais il ne peut pas optimiser le changement de signature. Ce qui signifie que nous nous retrouvons avec un résultat négatif, qui à son tour donne un nombre bizarre lorsque printf("%u est invoquée. L'exemple 1 pourrait être corrigé en coulant le résultat de l'opération vers le type unsigned char .

À l'exception de quelques cas particuliers comme ++ y sizeof les promotions d'entiers s'appliquent à presque toutes les opérations en C, peu importe si des opérateurs unaires, binaires (ou ternaires) sont utilisés.


Les conversions arithmétiques habituelles

Lorsqu'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, si les opérandes sont de types différents, le C applique une conversion implicite d'un opérande au type de l'autre opérande. Les règles qui régissent cette opération sont nommées les conversions arithmétiques habituelles (parfois appelé de manière informelle "équilibrage"). Ils sont spécifiés dans l'article C11 6.3.18 :

(Pensez à cette règle comme à une longue règle imbriquée. if-else if et il sera peut-ê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 résultats d'une 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 domaine, en un type dont le type réel correspondant est le type réel commun. Sauf si 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 même que celui du résultat. le 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 des opérandes est long double l'autre opérande est converti, sans changement de domaine de type, en un type dont le type réel correspondant est long double .
  • Sinon, si le type réel correspondant de l'un ou l'autre 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 est double .
  • Sinon, si le type réel correspondant de l'un ou l'autre 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 autre conversion n'est nécessaire.
    • Sinon, si les deux opérandes sont de type entier signé ou de type non signé entiers non signés, l'opérande ayant le type de rang de conversion d'entier le moins élevé est converti au type de l'opérande de rang supérieur.
    • Sinon, si l'opérande de type entier non signé a un rang supérieur ou égal à égal au rang du type de l'autre opérande, alors l'opérande avec le type type entier signé est converti au type de l'opérande de type entier non signé. entier non signé.
    • Sinon, si le type de l'opérande avec le type entier signé peut représenter toutes les valeurs du type de l'opérande avec le type entier non signé, alors l'opérande de type entier non signé est converti en type d'opérande de type entier signé. l'opérande de type entier signé.
    • Sinon, les deux opérandes sont convertis en entiers non signés. correspondant au type de l'opérande de type entier signé.

Il convient de noter que les conversions arithmétiques habituelles s'appliquent à la fois aux variables à virgule flottante et aux variables entières. Dans le cas des entiers, nous pouvons également noter que les promotions d'entiers sont invoquées à partir des conversions arithmétiques habituelles. Et ensuite, lorsque les deux opérandes ont au moins le rang de int les opérateurs sont équilibrés au même type, avec la même signature.

C'est la raison pour laquelle 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 Les promotions sur les nombres entiers ne s'appliquent donc pas. Les opérandes ne sont pas du même type. a est unsigned int y 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 l'information du signe et se retrouve sous la forme d'une grande valeur.

La raison pour laquelle le changement de type en short dans l'exemple 3 résout le problème, c'est parce que short est un petit type d'entier. Ce qui signifie que les deux opérandes sont des entiers promus au type int qui est signé. Après la promotion des entiers, les deux opérandes ont le même type ( int ), aucune autre conversion n'est nécessaire. Et alors l'opération peut être effectuée sur un type signé comme prévu.

6voto

Lusha Li Points 51

Conformément au 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);
}

Puisque unsigned char est plus petit que int, nous appliquons la promotion entière sur eux, alors nous avons (int)x-(int)y = (int)(-1) et unsigned int (-1) = 4294967295.

Le résultat du code ci-dessus : (identique à ce que nous attendions)

4294967295
-1

Comment le réparer ?

J'ai essayé ce que le post précédent recommandait, mais ça ne marche pas vraiment. Voici le code basé sur le post précédent :

changer l'un d'entre eux en unsigned int

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 seulement la promotion d'entier à y. Nous obtenons alors (unsigned int)x-(int)y. Comme 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.

Le résultat 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);
}

changer les deux en unsigned int

int main(){
    unsigned int x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Puisque les deux sont des int non signés, aucune promotion d'entier n'est nécessaire. Par la convergence arithmétique habituelle (ils ont le même type), (unsigned int)x-(unsigned int)y = 4294967295.

Le résultat du code ci-dessus : (identique à ce que nous attendions) :

4294967295
-1

Une des façons possibles de corriger le code :(ajouter 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);
}

Puisque les deux sont des entiers, aucune promotion d'entier n'est nécessaire. Par la conversion arithmétique habituelle, nous obtenons (unsigned int)a+(unsigned int)b = 1+4294967294 = 4294967295.

Le résultat du code ci-dessus : (identique à ce que nous attendions)

-1 is larger than 0
4294967295

Comment le réparer ?

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 puisque a et b ont tous deux été convertis en int grâce à la promotion des entiers.

La sortie du code ci-dessus :

-1 is smaller than 0
-1

Si j'ai mélangé certains concepts, faites-le moi savoir. Merci.

2 votes

Votre solution à l'exemple 2 signed int c = a+b; ci-dessus invoque UB. Le type résultant de a+b est non signé, et la valeur calculée est hors de portée d'un entier signé.

2 votes

@Cheshar l'affectation hors gamme n'est pas UB

2 votes

De nombreux exemples de cette réponse provoquent des UB en utilisant le mauvais spécificateur de format, et font également une supposition injustifiée sur la taille d'un fichier de type int

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X