Explication récapitulative pour les variables signées et non signées en C?

J’ai lu un peu dans la spécification C que les variables non signées (en particulier unsigned short int ) effectuent certaines opérations dites de bouclage sur le dépassement d’entier bien que je ne puisse rien trouver sur les variables signées, sauf que je suis parti avec un comportement indéfini . Mon professeur m’a dit que leurs valeurs étaient également abordées (peut-être voulait-il simplement dire gcc) Je pensais que les bits étaient juste tronqués et que les bits que j’avais laissés me donnaient une valeur étrange?! Alors, est-ce que quelqu’un peut expliquer ce qu’est le bouclage et en quoi cela diffère-t-il de simplement tronquer des bits?

Les variables entières signées n’ont pas de comportement englobant en langage C. Un dépassement d’entier signé lors de calculs arithmétiques produit un comportement non défini . Notez que le compilateur GCC que vous avez mentionné est connu pour implémenter une sémantique de débordement ssortingcte dans les optimisations, ce qui signifie qu’il tire parti de la liberté offerte par de telles situations comportementales non définies: le compilateur GCC suppose que les valeurs entières signées ne sont jamais bouclées. Cela signifie que GCC est en fait l’un des compilateurs dans lequel vous ne pouvez pas compter sur le comportement englobant des types entiers signés.

Par exemple, le compilateur GCC peut supposer que pour la variable int i la condition suivante

 if (i > 0 && i + 1 > 0) 

est équivalent à un simple

 if (i > 0) 

C’est exactement ce que signifie une sémantique de débordement ssortingcte .

Les types entiers non signés implémentent l’arithmétique modulo. Le modulo est égal à 2^NN est le nombre de bits dans la représentation de valeur du type. Pour cette raison, les types entiers non signés semblent bien s’envelopper en cas de débordement.

Cependant, le langage C n’effectue jamais de calculs arithmétiques dans des domaines plus petits que celui de int / unsigned int . Tapez unsigned short int que vous mentionnez dans votre question sera généralement promu au type int dans les expressions avant le début des calculs (en supposant que la plage de unsigned short s’inscrit dans la plage d’ int .) Ce qui signifie que 1) les calculs avec unsigned short int seront préformés dans le domaine de int , les débordements se produisant lorsque int débordements, 2) les débordements lors de tels calculs conduiront à un comportement indéfini, et non à un comportement enveloppant.

Par exemple, ce code produit un enveloppement

 unsigned i = USHRT_MAX; i *= INT_MAX; /* <- unsigned arithmetic, overflows, wraps around */ 

tandis que ce code

 unsigned short i = USHRT_MAX; i *= INT_MAX; /* <- signed arithmetic, overflows, produces undefined behavior */ 

conduit à un comportement indéfini.

Si aucun dépassement de int ne se produit et que le résultat est reconverti en un type unsigned short int , il est à nouveau réduit de modulo 2^N , ce qui apparaîtra comme si la valeur avait été renvoyée.

Imaginez que vous ayez un type de données d’une largeur de 3 bits seulement. Cela vous permet de représenter 8 valeurs distinctes, comsockets entre 0 et 7. Si vous ajoutez 1 à 7, vous revenez à 0, car vous ne disposez pas de suffisamment de bits pour représenter la valeur 8 (1000).

Ce comportement est bien défini pour les types non signés. Il n’est pas bien défini pour les types signés, car il existe plusieurs méthodes pour représenter les valeurs signées et le résultat d’un débordement sera interprété différemment en fonction de cette méthode.

Magnitude du signe: le bit le plus haut représente le signe; 0 pour positif, 1 pour négatif. Si mon type a encore une largeur de trois bits, je peux alors représenter les valeurs signées comme suit:

 000 = 0 001 = 1 010 = 2 011 = 3 100 = -0 101 = -1 110 = -2 111 = -3 

Puisqu’un bit est utilisé pour le signe, je n’ai que deux bits pour coder une valeur de 0 à 3. Si j’ajoute 1 à 3, je déborderai avec -0 comme résultat. Oui, il y a deux représentations pour 0, une positive et une négative. Vous ne rencontrerez pas souvent de représentation de la magnitude de signe.

Complément de un: la valeur négative est l’inverse du bit de la valeur positive. Encore une fois, en utilisant le type à trois bits:

 000 = 0 001 = 1 010 = 2 011 = 3 100 = -3 101 = -2 110 = -1 111 = -0 

J’ai trois bits pour encoder mes valeurs, mais la plage est [-3, 3]. Si j’ajoute 1 à 3, je déborderai avec -3 comme résultat. Ceci est différent du résultat de magnitude de signe ci-dessus. De nouveau, il existe deux codages pour 0 utilisant cette méthode.

Complément à deux: la valeur négative est l’inverse de la valeur positive au bit, plus 1. Dans le système à trois bits:

 000 = 0 001 = 1 010 = 2 011 = 3 100 = -4 101 = -3 110 = -2 111 = -1 

Si j’ajoute 1 à 3, je déborderai avec -4, ce qui est différent des deux méthodes précédentes. Notez que nous avons une plage de valeurs légèrement plus grande [-4, 3] et une seule représentation pour 0.

Le complément à deux est probablement la méthode la plus courante de représentation des valeurs signées, mais ce n’est pas la seule. Par conséquent, le standard C ne peut donner aucune garantie quant à ce qui se passera lorsque vous dépassez un type entier signé. Cela laisse donc le comportement indéfini afin que le compilateur n’ait pas à interpréter plusieurs représentations.

Le comportement non défini découle de problèmes de portabilité précoces dans lesquels les types entiers signés pouvaient être représentés sous la forme signe & magnitude, complément à un ou à deux.

De nos jours, toutes les architectures représentent des entiers comme un complément à deux qui bouclent. Mais soyez prudent: étant donné que votre compilateur a raison de supposer que vous n’utiliserez pas de comportement indéfini, vous pourriez rencontrer des bugs étranges lorsque l’optimisation est activée.

Dans un entier signé de 8 bits, la définition intuitive de bouclage pourrait ressembler à aller de +127 à -128 – en binary du complément à deux: 0111111 (127) et 1000000 (-128). Comme vous pouvez le constater, il s’agit de la progression naturelle de l’incrémentation des données binarys – sans considérer qu’elles représentent un entier, signé ou non signé. Contre intuitivement, le débordement réel a lieu lors du passage de -1 (11111111) à 0 (00000000) dans l’enveloppe du nombre entier non signé.

Cela ne répond pas à la question plus profonde de savoir quel est le comportement correct quand un entier signé déborde parce qu’il n’y a pas de comportement “correct” selon la norme.