Ce débordement d'un entier signé est un comportement non défini, comme toujours en C/C++.
Ce que tout programmeur C devrait savoir sur les comportements indéfinis
À moins que vous ne compiliez avec gcc -fwrapv
ou équivalent pour que le dépassement de capacité des entiers signés soit bien défini comme un complément à 2. Avec gcc -fwrapv
ou toute autre implémentation qui définit le débordement d'un entier = enveloppement, l'enveloppement que vous avez pu voir en pratique est bien défini et découle d'autres règles de l'ISO C pour les types de littéraux entiers et l'évaluation des expressions.
T var = expression
ne convertit qu'implicitement l'expression au type T
après en évaluant l'expression selon les règles standard. Comme (T)(expression)
pas comme (int64_t)2147483647 + (int64_t)1
.
Un compilateur aurait pu choisir de supposer que ce chemin d'exécution n'est jamais atteint et émettre une instruction illégale ou autre. L'implémentation du complément à 2 en cas de dépassement de capacité dans les expressions constantes est simplement un choix que certains ou la plupart des compilateurs font.
La norme ISO C spécifie que un littéral numérique est de type int
à moins que la valeur ne soit trop grande pour être prise en compte (il peut être long ou long long, ou non signé pour l'hexagone ), ou si une surcharge de taille est utilisée. Les règles habituelles de promotion des entiers s'appliquent alors aux opérateurs binaires tels que +
et *
qu'elle fasse ou non partie d'une expression constante à la compilation.
Il s'agit d'une règle simple et cohérente, facile à mettre en œuvre par les compilateurs, même dans les premiers temps du langage C, lorsque les compilateurs devaient fonctionner sur des machines limitées.
Ainsi, dans ISO C/C++ 2147483647 + 1
es comportement non défini sur les implémentations avec des int
. Le traiter comme int
(et donc l'enveloppement de la valeur en négatif signé) découle naturellement des règles de l'ISO C concernant le type que l'expression doit avoir et des règles d'évaluation normales pour le cas où il n'y a pas de débordement. Les compilateurs actuels ne choisissent pas de définir le comportement différemment.
Les normes ISO C/C++ ne le définissent pas, de sorte qu'une implémentation pourrait choisir littéralement n'importe quoi (y compris des démons nasaux) sans violer les normes C/C++. En pratique, ce comportement (envelopper + avertir) est l'un des moins répréhensibles et découle du fait que l'on considère le débordement d'un entier signé comme un enveloppement, ce qui se produit souvent dans la pratique au moment de l'exécution.
Par ailleurs, certains compilateurs proposent des options permettant d'effectuer des définir ce comportement s'applique officiellement à tous les cas, et pas seulement aux expressions constantes à la compilation. ( gcc -fwrapv
).
Les compilateurs mettent en garde contre ce problème
Les bons compilateurs signalent de nombreuses formes d'UB lorsqu'elles sont visibles au moment de la compilation, y compris celle-ci. GCC et clang avertissent même sans -Wall
. A partir de le compilateur Godbolt explorateur :
clang
<source>:5:20: warning: overflow in expression; result is -2147483648 with type 'int' [-Winteger-overflow]
a = 2147483647 + 1;
^
gcc
<source>: In function 'void foo()':
<source>:5:20: warning: integer overflow in expression of type 'int' results in '-2147483648' [-Woverflow]
5 | a = 2147483647 + 1;
| ~~~~~~~~~~~^~~
Cet avertissement est activé par défaut dans GCC depuis au moins GCC4.1 en 2006 (version la plus ancienne sur Godbolt), et dans clang depuis la version 3.3.
MSVC n'avertit que avec -Wall
qui, pour MSVC, est inutilement verbeux la plupart du temps, par exemple stdio.h
entraîne des tonnes d'avertissements tels que 'vfwprintf': unreferenced inline function has been removed
. L'avertissement de MSVC à ce sujet ressemble à ceci :
MSVC -Wall
<source>(5): warning C4307: '+': signed integral constant overflow
@HumanJHawkins a demandé pourquoi il a été conçu de cette manière :
Pour moi, cette question revient à se demander pourquoi le compilateur n'utilise pas également le plus petit type de données dans lequel le résultat d'une opération mathématique peut s'insérer. Avec les entiers littéraux, il serait possible de savoir au moment de la compilation qu'une erreur de dépassement de capacité est en train de se produire. Mais le compilateur ne prend pas la peine de le savoir et de le gérer. Comment cela se fait-il ?
"Les compilateurs détectent effectivement le dépassement de capacité et en avertissent les utilisateurs. Mais ils suivent les règles de l'ISO C qui disent int + int
a le type int
et que les littéraux numériques ont chacun le type int
. Les compilateurs choisissent simplement d'envelopper l'expression au lieu de l'élargir et de lui donner un type différent de celui auquel on s'attendrait. (Au lieu d'abandonner complètement à cause de l'UB).
L'enrobage est courant lorsque le dépassement de capacité signé se produit au moment de l'exécution, bien que les compilateurs optimisent agressivement les boucles int i
/ array[i]
à éviter de refaire l'extension de signe à chaque itération .
L'élargissement entraînerait sa propre série d'écueils (moins nombreux), tels que printf("%d %d\n", 2147483647 + 1, 2147483647);
ayant un comportement indéfini (et échouant en pratique sur les machines 32 bits) à cause d'une incompatibilité de type avec la chaîne de format. Dans le cas où 2147483647 + 1
implicitement promu à long long
vous avez besoin d'un %lld
chaîne de format. (Et cela ne fonctionnerait pas en pratique parce qu'un int de 64 bits est typiquement passé dans deux slots de passage d'arg sur une machine de 32 bits, de sorte que le 2e %d
verrait probablement la seconde moitié de la première long long
.)
Pour être honnête, il s'agit déjà d'un problème pour l'Union européenne. -2147483648
. En tant qu'expression dans le code source C/C++, elle est de type long
o long long
. Il est interprété comme 2147483648
séparément de l'unaire -
et 2147483648
ne tient pas dans un fichier signé de 32 bits. int
. Il possède donc le type le plus grand qui peut représenter la valeur.
Cependant, tout programme affecté par cet élargissement aurait eu UB (et probablement le wrapping) sans lui, et il est plus probable que l'élargissement fasse fonctionner le code. Il y a là un problème de philosophie de conception : trop de couches de "il se trouve que ça marche" et de comportements indulgents font qu'il est difficile de comprendre exactement pourquoi quelque chose fait et il est difficile de vérifier qu'il sera transférable à d'autres implémentations avec d'autres largeurs de caractères. Contrairement aux langages "sûrs" comme Java, le C est très peu sûr et présente différentes implémentations définies sur différentes plates-formes, mais de nombreux développeurs n'ont qu'une seule implémentation à tester. (Surtout avant l'apparition d'Internet et des tests d'intégration continue en ligne).
La norme ISO C ne définit pas le comportement, donc oui, un compilateur pourrait définir un nouveau comportement en tant qu'extension sans rompre la compatibilité avec les programmes sans UB. Mais à moins que tous le supportait, vous ne pouviez pas l'utiliser dans des programmes C portables. Je pourrais l'imaginer comme une extension GNU supportée par gcc/clang/ICC au moins.
En outre, une telle option serait quelque peu en contradiction avec -fwrapv
qui définit le comportement. Dans l'ensemble, je pense qu'il est peu probable qu'elle soit adoptée parce qu'il existe une syntaxe pratique pour spécifier le type d'un littéral ( 0x7fffffffUL + 1
vous donne une unsigned long
qui est garanti être assez large pour cette valeur en tant qu'entier non signé de 32 bits).
Mais considérons qu'il s'agit là d'un choix pour C en premier lieu, au lieu de la conception actuelle.
Une conception possible consisterait à déduire le type d'une expression constante entière à partir de sa valeur, calculée avec une précision arbitraire . Pourquoi une précision arbitraire au lieu de long long
o unsigned long long
? Celles-ci pourraient ne pas être assez grandes pour les parties intermédiaires de l'expression si la valeur finale est petite en raison de /
, >>
, -
o &
des opérateurs.
Ou une conception plus simple, comme celle du préprocesseur C, où les expressions en nombres entiers constants sont évaluées à une largeur fixe définie par l'implémentation, comme au moins 64 bits. (Mais alors assigner un type basé sur la valeur finale, ou basé sur la valeur temporaire la plus large dans une expression). Mais cela présente l'inconvénient évident, pour les premiers C sur des machines 16 bits, de rendre l'évaluation des expressions à la compilation plus lente que si le compilateur peut utiliser la largeur native des entiers de la machine en interne pour int
expressions.
Les expressions constantes entières sont déjà quelque peu spéciales en C, puisqu'elles doivent être évaluées au moment de la compilation dans certains contextes. , par exemple pour static int array[1024 * 1024 * 1024];
(où les multiplications déborderont sur les implémentations avec des int. 16 bits).
Il est évident que nous ne pouvons pas étendre efficacement la règle de promotion aux expressions non constantes ; si (a*b)/c
pourrait avoir à évaluer a*b
comme long long
au lieu de int
sur une machine 32 bits, la division nécessitera une précision étendue. (Par exemple, l'instruction de division 64 bits / 32 bits => 32 bits de x86 provoque un débordement du quotient au lieu de tronquer silencieusement le résultat. int
ne permettrait pas au compilateur d'optimiser correctement dans certains cas).
*Par ailleurs, voulons-nous vraiment que le comportement / la définition de `a bdépend de l'existence ou non d'une
aet
bsont
static const` ou non ?** Le fait que les règles d'évaluation au moment de la compilation correspondent aux règles applicables aux expressions non constantes semble être une bonne chose en général, même si cela laisse subsister ces pièges désagréables. Mais encore une fois, c'est quelque chose dont les bons compilateurs peuvent se méfier dans les expressions constantes.
D'autres cas plus courants de ce piège sont les suivants 1<<40
au lieu de 1ULL << 40
pour définir un indicateur de bit, ou en écrivant 1T comme 1024*1024*1024*1024
.
0 votes
L'intervalle pour un int est en fait [32,767, +32,767], l'intervalle pour un long int est [2,147,483,647, +2,147,483,647].
13 votes
@KerryCao sizeof(int) dépend du matériel, mais sur le matériel moderne, c'est généralement 4 octets. stackoverflow.com/questions/11438794/
0 votes
@KerryCao - jusqu'à la récente modification du complément 2s à la norme. (C'est une cible mouvante. Et très compliqué. Même avec le complément à 2s, le débordement d'une valeur signée est un comportement non défini).
3 votes
@Hoseong, Avez-vous vu des avertissements de compilation ?
5 votes
Notez que le dépassement de capacité signé était et est toujours un comportement indéfini en C++. Votre compilateur devrait vous avertir de ce problème. godbolt.org/z/krZBUa
8 votes
XKCD pertinent .
1 votes
@KerryCao - ce sont les minimum Les compilateurs sont autorisés à fournir des fourchettes plus larges, à condition de respecter les exigences suivantes
long
est au moins aussi large queint
,int
est au moins aussi large queshort
yshort
est au moins aussi large quechar
.3 votes
Pour obtenir la portée réelle d'un type,
#include <limits>
et regarderstd::numeric_limits<int>::min()
etstd::numeric_limits<int>::max()
. Remplacerint
avec le type d'entier qui vous intéresse. (Vous pouvez également utiliser ces deux fonctions avec des types à virgule flottante, maisstd::numeric_limits<double>::min()
a une définition peu intuitive).0 votes
Cela répond-il à votre question ? Pourquoi un débordement lors de la comparaison d'un long long avec un int
1 votes
@Leos313, gcc donne
int.c:5:16: warning: integer overflow in expression [-Woverflow]
. Mais bien sûr, cela ne dit pas pourquoi il déborde, même si tous les nombres tiennent dans unlong long
.2 votes
Comment une telle question peut-elle obtenir autant de votes positifs ? Les gens d'aujourd'hui ne comprennent-ils vraiment pas les nombres en complément à deux ?
1 votes
La question ne doit pas être doublement étiquetée, meta.stackoverflow.com/questions/374306/
1 votes
@ilkkachu
g++
sous Linux, il signe aussi la position :-)a.cpp:7:16: warning: integer overflow in expression [-Woverflow]
a = 2147483647 + 1;
~~~~~~~~~~~^~~
0 votes
@TomServo La question ne porte pas sur le complément à deux, mais sur la promotion automatique du type de données (ou l'absence de promotion, comme dans ce cas).
0 votes
a
eslong long int
pero2147483647
et1
ne le sont pas. Avant d'être affecté à la
, l'expression2147483647 + 1
est calculée en utilisant les types des valeurs qu'elle utilise et le type des deux nombres estint
.