75 votes

Pourquoi long long 2147483647 + 1 = -2147483648 ?

Pourquoi ce code n'imprime-t-il pas le même nombre ? :

long long a, b;
a = 2147483647 + 1;
b = 2147483648;
printf("%lld\n", a);
printf("%lld\n", b);

Je sais que le nombre maximum de la variable int est 2147483647 parce que la variable int est de 4 octets. Mais comme je le sais, la variable long long est de 8 octets, mais pourquoi ce code agit-il ainsi ?

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).

129voto

Paul Sanders Points 7378

2147483647 + 1 est évaluée comme la somme de deux ints et déborde donc.

2147483648 est trop grand pour tenir dans un int et est donc considéré par le compilateur comme un long (ou un long long dans MSVC). Il ne déborde donc pas.

Pour effectuer la sommation en tant que long long utiliser le suffixe de constante approprié, c'est-à-dire

a = 2147483647LL + 1;

0 votes

Les commentaires ne sont pas destinés à une discussion prolongée. déplacé vers chat .

17voto

Peter Cordes Points 1375

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'uneaetbsontstatic 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 .

6voto

Jim Klimov Points 9

Bonne question. Comme d'autres l'ont dit, les chiffres sont par défaut int , de sorte que votre opération pour a agit sur deux int et les débordements. J'ai essayé de reproduire ce phénomène, et d'étendre un peu le système pour convertir le nombre en long long puis ajoutez la variable 1 à elle, comme l'a fait le c exemple ci-dessous :

$ cat test.c 
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>

void main() {
  long long a, b, c;

  a = 2147483647 + 1;
  b = 2147483648;

  c = 2147483647;
  c = c + 1;

  printf("%lld\n", a);
  printf("%lld\n", b);
  printf("%lld\n", c);
}

Le compilateur met en garde contre les débordements BTW, et vous devriez normalement compiler le code de production avec l'option -Werror -Wall pour éviter de telles mésaventures :

$ gcc -m64 test.c -o test
test.c: In function 'main':
test.c:8:16: warning: integer overflow in expression [-Woverflow]
 a = 2147483647 + 1;
                ^

Enfin, les résultats des tests sont conformes aux attentes ( int débordement dans le premier cas, long long int en deuxième et troisième position) :

$ ./test 
-2147483648
2147483648
2147483648

Une autre version de gcc met encore plus en garde :

test.c: In function ‘main’:
test.c:8:16: warning: integer overflow in expression [-Woverflow]
 a = 2147483647 + 1;
                ^
test.c:9:1: warning: this decimal constant is unsigned only in ISO C90
 b = 2147483648;
 ^

Il convient également de noter que, techniquement, les int et long et ses variantes dépendent de l'architecture, de sorte que la longueur des bits peut varier. Pour les types de taille prévisible, il est préférable d'utiliser int64_t , uint32_t et ainsi de suite, qui sont généralement définis dans les compilateurs modernes et les en-têtes de système, de sorte que, quel que soit le niveau de bits pour lequel votre application est conçue, les types de données restent prévisibles. Notez également que l'impression et l'analyse de ces valeurs sont aggravées par des macros telles que PRIu64 etc.

-3voto

Manish Kumar Points 1

Parce que la plage d'int en C/C++ est -2147483648 à +2147483647 .

Ainsi, lorsque vous ajoutez 1 il dépasse la limite maximale de int .

Pour une meilleure compréhension, supposons que l'ensemble des int met en place un cercle dans l'ordre approprié :

2147483647 + 1 == -2147483648

2147483647 + 2 == -2147483647

Si vous voulez surmonter ce problème, essayez d'utiliser long long au lieu de int .

0 votes

Le fonctionnement n'est garanti qu'avec gcc -fwrapv pour définir le comportement d'un dépassement de capacité signé.

0 votes

Il s'agit d'une description exacte de l'enveloppement signé en complément à 2 sur 32 bits, qui est en pratique ce que font les compilateurs C actuels. Mais il n'est pas exact de dire que le La plage d'un int en C/C++ est ... sans aucun qualificatif. Les implémentations C pour les microcontrôleurs et les DSP à 16 bits int encore très répandue, et C exige seulement que la gamme soit au moins -32767 .. 32767 et qu'il s'agit d'un complément à 2, d'un complément à 1 ou d'un signe/magnitude. fr.cppreference.com/w/cpp/language/types

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