Pourquoi ces constructions utilisent - elles un comportement non défini pré-et post-increment?

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
724
demandé sur jww 2009-06-04 13:17:52
la source

14 ответов

C a le concept de comportement non défini, i.e. certaines constructions de langage sont syntaxiquement valides, mais vous ne pouvez pas prédire le comportement lorsque le code est lancé.

pour autant que je sache, la norme ne dit pas explicitement pourquoi le concept de comportement non défini existe. Dans mon esprit, c'est simplement parce que les concepteurs de langage voulaient qu'il y ait une certaine marge de manœuvre dans la sémantique, au lieu d'exiger que toutes les implémentations gèrent le débordement d'entier de la même manière, ce qui imposerait très probablement des coûts de performance sérieux, ils ont laissé le comportement non défini de sorte que si vous écrivez du code qui provoque le débordement d'entier, tout peut arriver.

Donc, avec cela à l'esprit, pourquoi ces "problèmes"? Le langage dit clairement que certaines choses conduisent à comportement non défini . Il n'y a pas de problème, il n'est pas "doit". Si le comportement non défini change lorsque l'une des variables impliquées est déclaré volatile , cela ne prouve ni ne change rien. C'est Non défini ; vous ne pouvez pas raisonner sur le comportement.

votre exemple le plus intéressant, celui avec

u = (u++);

est un exemple de comportement non défini dans un livre de texte (voir L'entrée de Wikipedia sur sequence points ).

524
répondu unwind 2015-12-16 12:38:33
la source

Juste compiler et démonter votre ligne de code, si vous êtes si incliné de savoir exactement comment il est que vous obtenez ce que vous obtenez.

C'est ce que j'obtiens sur ma machine, avec ce que je pense qui se passe:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    "151900920"x10,%esp
   0x00000006 <+6>:   movl   "151900920"x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   "151900920"x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   "151900920"x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(I... supposons que l'instruction 0x00000014 soit une sorte d'optimisation du compilateur?)

73
répondu badp 2014-07-02 03:21:50
la source

je pense que les parties pertinentes de la norme C99 sont 6,5 Expressions, §2

entre le point de séquence précédent et le point de séquence suivant un objet doit avoir sa valeur stockée modifié plus d'une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lu uniquement pour déterminer la valeur à stocker.

et 6.5.16 opérateurs d'Affectation, §4:

l'ordre de l'évaluation des opérandes est pas spécifié. Si une tentative est faite pour modifier le résultat de l'opérateur d'affectation ou d'y accéder après le prochain point de séquence, l' le comportement est indéfini.

55
répondu Christoph 2009-06-04 13:42:43
la source

le comportement ne peut pas vraiment être expliqué parce qu'il invoque à la fois comportement non spécifié et comportement non défini , de sorte que nous ne pouvons pas faire de prédictions générales sur ce code, bien que si vous lisez Olve Maud's travaux tels que Deep C et non spécifié et non défini parfois, vous pouvez faire de bonnes suppositions dans des cas très spécifiques avec un compilateur spécifique et environnement mais s'il vous plaît ne faites pas cela n'importe où près de la production.

passe ainsi à comportement non spécifié , dans projet de norme C99 section 6.5 paragraphe 3 dit ( emphasis mine ):

le groupement des opérateurs et des opérandes est indiqué par la syntaxe.74) sauf indication contraire plus tard (pour la fonction call (), &&, ||, ?1519360920" l'ordre d'évaluation des sous-expressions et l'ordre dans lequel les effets secondaires se produisent sont tous deux non spécifiés.

donc quand nous avons une ligne comme celle-ci:

i = i++ + ++i;

nous ne savons pas si i++ ou ++i sera évalué en premier. Ceci est principalement pour donner au compilateur de meilleures options pour l'optimisation .

nous ont aussi comportement non défini ici aussi puisque le programme modifie des variables( i , u , etc..) plus d'une fois entre et . Extrait du paragraphe du projet de norme 6.5 2 ( emphasis mine ):

entre le point de séquence précédent et le point de séquence suivant, un objet doit avoir sa valeur stockée modifié à le plus souvent par l'évaluation d'une expression. En outre, la valeur antérieure ne doit être lu que pour déterminer la valeur à stocker .

il cite les exemples de codes suivants comme étant non définis:

i = ++i + 1;
a[i++] = i; 

dans tous ces exemples le code tente de modifier un objet plus d'une fois dans le même point de séquence, ce qui se terminera par le ; dans chacun de ces cas:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

comportement non spécifié est défini dans le projet de norme C99 dans la section 3.4.4 comme:

utilisation d'une valeur non spécifiée, ou d'un autre comportement lorsque cette norme internationale prévoit deux ou plusieurs possibilités et n'impose aucune autre exigence sur laquelle est choisi instance

et non définis le comportement est défini dans la section 3.4.3 :

comportement, lors de l'utilisation d'une construction de programme non transposable ou erronée ou de données erronées, pour lesquelles la présente Norme internationale n'impose aucune prescription

et note que:

les comportements possibles non définis vont de l'ignorance totale de la situation avec des résultats imprévisibles, au comportement pendant de traduction ou d'exécution d'un programme dans un documentées de façon caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), à la terminaison d'une traduction ou d'exécution (avec l'émission d'un message de diagnostic).

44
répondu Shafik Yaghmour 2017-05-23 15:34:44
la source

la plupart des réponses citées dans la norme C soulignent que le comportement de ces constructions n'est pas défini. Pour comprendre pourquoi le comportement de ces constructions ne sont pas définis , comprenons ces termes d'abord à la lumière de la norme C11:

séquencé: (5.1.2.3)

compte tenu de deux évaluations A et B , si A est après B , l'exécution de A doit précéder celle de B .

Non:

si A n'est pas séquencé avant ou après B , alors A et B ne sont pas séquencés.

les évaluations peuvent être une de deux choses:

  • calculs de valeur , qui calculent le résultat d'une expression; et
  • effets secondaires , qui sont des modifications d'objets.

Point De Séquence:

la présence d'un point de séquence entre l'évaluation des expressions A et B implique que chaque calcul de valeur et effet secondaire associé à A est séquencée avant toute valeur de calcul et effet secondaire associé à B .

vient maintenant à la question, pour les expressions comme

int i = 1;
i = i++;

standard dit que:

6.5 Expressions:

si un effet secondaire sur un objet scalaire n'est pas égalé par rapport à soit un effet secondaire différent sur le même objet scalaire ou un calcul de valeur utilisant la valeur du même objet scalaire, le comportement est non défini . [...]

par conséquent, L'expression ci-dessus invoque UB parce que deux effets secondaires sur le même objet i n'ont pas d'importance relative l'un par rapport à l'autre. Que signifie qu'il n'est pas séquencé si l'effet secondaire par assignation à i sera fait avant ou après l'effet secondaire par ++ .

Selon que l'affectation se produit avant ou après l'incrément, différents résultats seront produits et c'est celui du cas de comportement non défini .

permet de renommer le i à la gauche de la cession être il et à la droite de la cession (en l'expression i++ ) ir , puis l'expression de l'être comme

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

un point important concernant L'opérateur Postfix ++ est que:

ce n'est pas parce que le ++ vient après la variable que l'accroissement se produit tard . L'incrément peut se produire dès le compilateur aime aussi longtemps que le compilateur s'assure que la valeur originale est utilisée .

signifie que l'expression il = ir++ peut être évaluée comme

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

ou

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

résultant en deux résultats différents 1 et 2 qui dépend de la séquence d'effets secondaires par assignation et ++ et donc invoque UB.

42
répondu haccks 2017-05-23 15:18:24
la source

une autre façon de répondre à cela, plutôt que de s'enliser dans des détails obscurs de points de séquence et de comportements non définis, est simplement de demander, que sont-ils censés signifier? Qu'est-ce que le programmeur essayait de faire?

le premier fragment demandé, i = i++ + ++i , est assez clairement insensé dans mon livre. Personne n'aurait jamais l'écrire dans un programme réel, il n'est pas évident ce qu'il fait, il n'est pas concevable algorithme quelqu'un aurait pu essayer de coder ce qui aurait donné lieu à cette séquence d'opérations inventée. Et comme ce n'est pas évident pour toi et moi ce que c'est supposé faire, c'est très bien dans mon livre si le compilateur ne peut pas comprendre ce que c'est supposé faire, non plus.

Le deuxième fragment, i = i++ , est un peu plus facile à comprendre. Quelqu'un essaie clairement d'incrémenter i, et d'attribuer le résultat à I. Mais il y a deux façons de faire ceci en C. La façon la plus basique d'ajouter 1 à i, et d'attribuer le résultat à i, est la même dans presque n'importe quel langage de programmation:

i = i + 1

C, bien sûr, a un raccourci pratique:

i++

cela signifie"ajouter 1 à i, et attribuer le résultat à i". Donc, si nous construisons un mélange des deux, en écrivant

i = i++

ce que nous sommes vraiment en train de dire est "ajouter 1 à i, et assignez le résultat à i, et assignez le résultat de je". Nous sommes confus, donc ça ne me dérange pas trop si le compilateur est confus aussi.

de façon réaliste, la seule fois où ces expressions folles sont écrites, c'est quand les gens les utilisent comme des exemples artificiels de la façon dont ++ est supposé fonctionner. Et bien sûr, il est important de comprendre comment ++ fonctionne. Mais une règle pratique pour utiliser ++ est, "si ce n'est pas évident ce qu'une expression utilisant ++ signifie, Ne l'écrivez pas."

nous avions l'habitude de passer d'innombrables heures sur comp.lang.C en discutant d'expressions comme celles-ci et pourquoi elles ne sont pas définies. Deux de mes plus longues réponses, qui tentent vraiment d'expliquer pourquoi, sont archivées sur le web:

29
répondu Steve Summit 2018-05-27 00:27:39
la source

bien qu'il soit peu probable qu'un compilateur ou un processeur le fasse, il serait légal, selon la norme C, que le compilateur implémente "i++" avec la séquence:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

bien que je ne pense pas que les processeurs prennent en charge le matériel pour permettre une telle chose d'être fait efficacement, on peut facilement imaginer des situations où un tel comportement rendrait le code multi-threadé plus facile (par exemple, il garantirait que si deux threads essayer d'effectuer le ci-dessus séquence simultanément, i incrémenté par deux), et il n'est pas totalement inconcevable que le futur processeur pourrait fournir une fonctionnalité de quelque chose comme ça.

si le compilateur devait écrire i++ comme indiqué ci-dessus (légal en vertu de la norme) et devait intercaler les instructions ci-dessus tout au long de l'évaluation de l'expression globale (également légal), et s'il ne se produisait pas de remarquer que l'une des autres instructions se trouvait à accéder i , il serait possible (et légal) pour le compilateur de générer une séquence d'instructions qui serait bloquée. Bien sûr, un compilateur détecterait presque certainement le problème dans le cas où la même variable i est utilisée aux deux endroits, mais si une routine accepte les références à deux pointeurs p et q , et utilise (*p) et (*q) dans l'expression ci-dessus (plutôt que d'utiliser i deux fois) le compilateur ne serait pas requis de reconnaître ou éviter l'impasse qui se produirait si l'adresse du même objet était transmise à la fois pour p et q .

22
répondu supercat 2017-10-23 23:06:36
la source

souvent cette question est liée comme un duplicata de questions liées au code comme

printf("%d %d\n", i, i++);

ou

printf("%d %d\n", ++i, i++);

ou variantes similaires.

bien qu'il s'agisse également de comportement non défini comme nous l'avons déjà dit, il existe des différences subtiles lorsque printf() est en cause lorsqu'on le compare à un énoncé tel que:

   x = i++ + i++;

Dans la déclaration suivante:

printf("%d %d\n", ++i, i++);

le de l'ordre d'évaluation d'arguments printf() est non spécifié . Cela signifie que les expressions i++ et ++i peuvent être évaluées dans n'importe quel ordre. C11 standard a quelques descriptions pertinentes à ce sujet:

Annexe J, comportements non spécifiés

l'ordre dans lequel le désignateur de fonction, les arguments, et les sous-expressions dans les arguments sont évalués dans un appel de fonction (6.5.2.2).

3.4.4, comportement non spécifié

utilisation d'une valeur non spécifiée, ou d'un autre comportement où cette La norme internationale offre deux possibilités ou plus et impose aucune autre exigence sur laquelle est choisi dans n'importe quel cas.

exemple un exemple de comportement non spécifié est l'ordre dans lequel les arguments d'une fonction sont évalués.

le comportement non précisé en soi n'est pas un problème. Prenons l'exemple suivant:

printf("%d %d\n", ++x, y++);

cela aussi a comportement non spécifié parce que l'ordre de l'évaluation de ++x et y++ n'est pas précisée. Mais c'est une déclaration parfaitement légale et valide. Il y a Non comportement non défini dans cette déclaration. Parce que les modifications ( ++x et y++ ) sont faites à distinct objets.

ce qui rend la déclaration suivante

printf("%d %d\n", ++i, i++);

comme comportement non défini est le fait que ces deux les expressions modifient le même objet i sans un point de séquence 1519370920".


un autre détail est que le virgule impliqué dans l'appel printf() est un séparateur , pas le opérateur de virgule 1519370920" .

C'est un important distinction parce que le comma operator introduit un sequence point entre l'évaluation de leurs opérandes, qui rend le suivant Juridique:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

l'opérateur comma évalue ses opérandes de gauche à droite et ne donne que la valeur de la dernière opérande. Ainsi, dans j = (++i, i++); , ++i incréments i à 6 et i++ donne une valeur ancienne de i ( 6 ) qui est affectée à j . Puis i devient 7 en raison de post-incrément.

donc si le virgule dans l'appel de fonction devait être un opérateur de virgule Alors

printf("%d %d\n", ++i, i++);

ne sera pas un problème. Mais il invoque comportement non défini parce que le virgule ici est un séparateur .


Pour ceux qui sont nouveaux à la comportement indéfini bénéficier de la lecture de Ce que Chaque Programmeur C Devrait Connaître un Comportement Indéfini pour comprendre le concept et de nombreuses autres variantes de comportement indéfini dans C.

Ce post: Indéfini, indéterminé et de mise en œuvre définies par le comportement est également pertinente.

18
répondu P.P. 2017-05-23 14:55:11
la source

la norme C stipule qu'une variable ne doit être assignée au plus qu'une seule fois entre deux points de séquence. Par exemple, un point semi-colon est un point de séquence.

Ainsi chaque déclaration de la forme:

i = i++;
i = i++ + ++i;

et ainsi de suite violer cette règle. La norme dit aussi que le comportement est indéfini et non spécifié. Certains compilateurs les détectent et produisent des résultats, mais ce n'est pas conforme à la norme.

cependant, deux variables différentes peuvent être incrémentées entre deux points de séquence.

while(*src++ = *dst++);

ce qui précède est une pratique courante de codage lors de la copie/analyse de chaînes.

13
répondu Nikhil Vidhani 2014-09-11 16:36:41
la source

alors que la syntaxe des expressions comme a = a++ ou a++ + a++ est légale, le comportement de ces constructions est Non défini parce qu'un doit dans la norme C n'est pas obéi. c99 6.5p2 :

  1. entre le point de séquence précédent et le point de séquence suivant, un objet doit avoir la valeur stockée modifié plus d'une fois par l'évaluation d'une expression. [72] en outre, la valeur antérieure ne doit être lue que pour déterminer la valeur à stocker [73]

avec note de bas de page 73 précisant en outre que

  1. ce paragraphe rend des expressions non définies telles que

    i = ++i + 1;
    a[i++] = i;
    

    tout en permettant

    i = i + 1;
    a[i] = i;
    

les divers points de séquence sont énumérés à l'Annexe C de C11 (et c99 ):

  1. les points de séquence suivants sont décrits au 5.1.2.3:

    • entre les évaluations de l'indicateur de fonction et les arguments réels dans un appel de fonction et le appel réel. (6.5.2.2).
    • entre les évaluations du premier et du second opérandes des opérateurs suivants: logical et & & (6.5.13); logical OR || (6.5.14); virgule , (6.5.17).
    • entre les évaluations du premier opérande du conditionnel ? : opérateur et celui des deuxième et troisième opérandes qui est évalué (6.5.15).
    • La fin d'un complet de demande de déclaration: declarators (6.7.6);
    • Entre l'évaluation d'une expression complète et à la prochaine pleine expression à évaluer. Les expressions suivantes sont des expressions complètes: un initialiseur qui ne fait pas partie d'un composé littéral (6.7.9); l'expression dans une instruction d'expression (6.8.3); l'expression de commande d'une instruction de sélection (Si ou interrupteur) (6.8.4); l'expression de commande d'une instruction while or do (6.8.5); chacune des expressions (optionnelles) d'une instruction for (6.8.5.3); l'expression (optionnelle) dans une instruction de retour (6.8.6.4).
    • immédiatement avant le retour d'une fonction de bibliothèque (7.1.4).
    • après les actions associées à chaque spécificateur de conversion de fonction d'entrée/sortie formaté (7.21.6, 7.29.2).
    • Immédiatement avant et immédiatement après chaque appel à une fonction de comparaison, et aussi entre l'appel d'une fonction de comparaison et le mouvement des objets passés comme arguments à cet appel (7.22.5).

le libellé du même paragraphe dans C11 est le suivant:

  1. si un effet secondaire sur un objet scalaire n'est pas égalé par rapport à un effet secondaire différent sur le même objet scalaire ou à un calcul de valeur utilisant la valeur du même objet scalaire, le comportement n'est pas défini. S'il y a plusieurs ordres admissibles des sous-expressions d'un expression, le comportement n'est pas défini si un tel effet secondaire non consécutif se produit dans l'une des commandes.84)

vous pouvez détecter de telles erreurs dans un programme en utilisant par exemple une version récente de GCC avec -Wall et -Werror , puis GCC refusera catégoriquement de compiler votre programme. Ce qui suit est la sortie de gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

La partie importante est de savoir ce qu'est un point de séquence -- et ce qui est un point de séquence et ce que n'est pas . Par exemple, l'opérateur comma est un point de séquence, donc

j = (i ++, ++ i);

est bien défini, et augmentera i par un, donnant l'ancienne valeur, jeter cette valeur; puis à comma opérateur, régler les effets secondaires; et puis augmenter i par un, et la valeur résultante devient la valeur de l'expression-i.e. c'est juste une façon d'écrire j = (i += 2) qui est encore une façon "intelligente" d'écrire

i += 2;
j = i;

cependant ,le , dans les listes d'arguments de fonction est pas un opérateur de virgule, Et il n'y a pas de point de séquence entre les évaluations des arguments distincts; au lieu de leurs évaluations sont sans importance à l'égard de l'autre; donc la fonction appel

int i = 0;
printf("%d %d\n", i++, ++i, i);

a comportement non défini parce que il n'y a pas de point de séquence entre les évaluations de i++ et ++i dans les arguments de fonction , et la valeur de i est donc modifiée deux fois, par i++ et ++i , entre le point de séquence précédent et le point de séquence suivant.

10
répondu Antti Haapala 2018-09-07 15:49:14
la source

In https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c quelqu'un a demandé à propos d'une déclaration comme:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

qui imprime 7... L'OP s'attendait à ce qu'il imprime 6.

les incréments ++i ne sont pas garantis à tous avant le reste des calculs. En fait, différents compilateurs obtiendront des résultats différents ici. Dans l'exemple que vous avez fourni, le premier 2 ++i exécuté, alors les valeurs de k[] ont été lues, puis le dernier ++i puis k[] .

num = k[i+1]+k[i+2] + k[i+3];
i += 3

compilateurs modernes vont optimiser ce très bien. En fait, peut-être mieux que le code que vous avez écrit à l'origine (en supposant qu'il ait fonctionné comme vous l'aviez espéré).

9
répondu TomOnTime 2017-05-23 14:47:30
la source

une bonne explication de ce qui se passe dans ce type de calcul est fournie dans le document n1188 de le site ISO W14 .

j'explique les idées.

la principale règle de la norme ISO 9899 qui s'applique dans cette situation est la règle 6.5p2.

entre le point de séquence précédent et le point de séquence suivant un objet doit avoir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure ne doit être lue que pour déterminer la valeur à stocker.

les points de séquence dans une expression comme i=i++ sont avant i= et après i++ .

dans le papier que j'ai cité ci-dessus il est expliqué que vous pouvez comprendre le programme comme étant formé par de petites boîtes, chaque boîte contenant les instructions entre 2 séquence consécutive point. Les points de séquence sont définis à l'annexe C de la norme, dans le cas de i=i++ , il y a deux points de séquence qui délimitent une expression complète. Une telle expression est syntaxiquement équivalente avec une entrée de expression-statement dans la forme Backus-Naur de la grammaire (une grammaire est fournie dans l'annexe A de la norme).

ainsi l'ordre des instructions à l'intérieur d'une boîte n'a pas d'ordre clair.

i=i++

peut être interprété comme

tmp = i
i=i+1
i = tmp

ou comme

tmp = i
i = tmp
i=i+1

parce que toutes les deux ces formes pour interpréter le code i=i++ sont valides et parce que les deux génèrent des réponses différentes, le comportement est indéfini.

ainsi un point de séquence peut être vu par le début et la fin de chaque boîte qui compose le programme [les boîtes sont des unités atomiques en C] et à l'intérieur d'une boîte l'ordre des instructions n'est pas défini dans tous les cas. Changer cet ordre peut changer le résultat parfois.

EDIT:

autre bonne source pour expliquer ces ambiguïtés sont les entrées de C-faq site (également publié comme un livre ), à savoir ici et ici et ici .

5
répondu alinsoar 2017-11-24 15:15:14
la source

la raison est que le programme exécute un behavior Non défini. Le problème réside dans l'ordre d'évaluation, car aucun point de séquence n'est requis selon la norme C++98 ( aucune opération n'est séquencée avant ou après une autre selon la terminologie C++11).

cependant si vous vous en tenez à un compilateur, vous trouverez le comportement persistant, tant que vous n'ajoutez pas d'appels de fonction ou de pointeurs, ce qui rendrait le comportement plus confus.

  • ainsi D'abord le CCG: En utilisant Nuwen MinGW 15 GCC 7.1 vous obtiendrez:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2
    

    }

Comment fonctionne GCC? il évalue les expressions secondaires dans un ordre de gauche à droite pour le côté droit (RHS) , puis attribue la valeur au côté gauche (LHS) . C'est exactement comme Java et C# se comportent et définissent leurs standards. (Oui, le logiciel équivalent en Java et C# a comportements définis). Il évalue chaque sous-expression une par une dans la déclaration RHS dans un ordre de gauche à droite; pour chaque sous-expression: le ++c (Pré-incrément) est évalué d'abord puis la valeur c est utilisé pour l'opération, puis le post increment c++).

selon GCC C++: opérateurs

dans GCC c++, la priorité des opérateurs contrôle l'ordre évaluation des différents opérateurs

le code équivalent dans le comportement défini C++ comme GCC comprend:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

puis nous allons à studio visuel . Visual Studio 2015, vous obtenez:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

comment visual studio fonctionne, il prend une autre approche, il évalue toutes les expressions pré-incréments dans la première passe, puis utilise des variables valeurs dans les opérations dans la deuxième passe, assigner de RHS à LHS dans la troisième passe, puis à la dernière passe, il évalue toutes les expressions de post-incrément en une seule passe.

donc l'équivalent dans le comportement défini C++ comme c visuel++ comprend:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

comme la documentation de Visual Studio états à la Priorité et l'Ordre de l'Évaluation :

lorsque plusieurs opérateurs apparaissent ensemble, ils ont la même priorité et sont évalués selon leur associativité. Les opérateurs le tableau est décrit dans les sections commençant par les opérateurs Postfix.

3
répondu Muhammad Annaqeeb 2017-06-11 04:17:46
la source

votre question n'était probablement pas, "pourquoi ces constructions comportement non défini en C?". Votre question était probablement, " pourquoi ce code (en utilisant ++ ) ne m'a pas donné la valeur à laquelle je m'attendais?"et quelqu'un a marqué votre question comme un doublon, et vous a envoyé ici.

Ce réponse tente de répondre à cette question: pourquoi ton code ne pas vous donner la réponse que vous attendiez, et comment pouvez-vous apprendre à reconnaître (et éviter) les expressions qui ne pas fonctionner comme prévu.

je suppose que vous avez déjà entendu la définition de base des opérateurs c ++ et -- , et comment le formulaire de préfixe ++x diffère du formulaire de postfix x++ . Mais ces opérateurs sont difficiles à penser, donc pour être sûr que vous avez compris, peut-être que vous avez écrit un petit programme de test minuscule impliquant quelque chose comme

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

mais, à votre surprise, ce programme a fait pas vous aide à comprendre -- il a imprimé une sortie étrange, inattendue, inexplicable, suggérant que peut-être ++ fait quelque chose de complètement différent, pas du tout ce que vous pensiez qu'il a fait.

ou, peut-être que vous regardez une expression difficile à comprendre comme

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

peut-être que quelqu'un vous a donné ce code comme un puzzle. Ce code n'a pas non plus de sens, surtout si vous l'exécutez -- et si vous le compilez et l'exécutez sous deux différents compilateurs, vous êtes susceptible d'obtenir deux réponses différentes! Qu'est-ce qui? La réponse correcte? (Et la réponse est que les deux d'entre eux sont, ou aucun d'eux.)

comme vous l'avez entendu maintenant, toutes ces expressions sont Non défini , ce qui signifie que le langage C ne fait aucune garantie sur ce qu'ils feront. C'est une étrange et surprenant, parce que vous pensiez sans doute que n'importe quel programme que vous pourriez écrire, tant qu'il compilé et exécuté, générerait un produit unique, bien défini. Mais dans le cas d'un comportement indéfini, qui n'est pas pareil.

Qu'est-ce qui fait qu'une expression n'est pas définie? Les expressions ++ et -- ne sont-elles pas toujours définies? Bien sûr que non: ce sont des opérateurs utiles, et si vous les utilisez correctement, ils sont parfaitement bien définis.

pour les expressions on parle de ce qui les rend indéfini c'est quand il y a trop de choses qui se passent tout de suite, quand nous ne sommes pas sûrs de ce qui se passera dans l'ordre, mais quand l'ordre importe au résultat, nous obtenons.

reprenons les deux exemples que j'ai utilisés dans cette réponse. Quand j'ai écrit

printf("%d %d %d\n", x, ++x, x++);

la question Est, avant d'appeler printf , est-ce que le compilateur calcule la valeur de x d'abord, ou x++ , ou peut-être ++x ? Mais il s'avère que nous ne savons pas . Il n'y a pas de règle dans le C qui dit que les arguments d'une fonction sont évaluées de gauche à droite ou de droite à gauche, ou dans un autre ordre. Donc, nous ne pouvons pas dire si le compilateur fera x d'abord, puis ++x , puis x++ , ou x++ puis ++x puis x , ou quelque autre ordre. Mais l'ordre importe clairement, parce que selon l'ordre que le compilateur utilise, nous obtiendrons clairement des résultats différents imprimés par printf .

Qu'en est-il de ce fou d'expression?

x = x++ + ++x;

le problème avec cette expression est qu'elle contient trois tentatives différentes pour modifier la valeur de x: (1) la partie x++ tente d'ajouter 1 à x, stocker la nouvelle valeur dans x , et retourner l'ancienne valeur de x ; (2) la partie ++x tente d'ajouter 1 à x, stocker la nouvelle valeur dans x , et retourner la nouvelle valeur de x ; et (3) la partie x = tente d'attribuer la somme de l'autre deux de retour à X. Laquelle de ces trois tentatives sera "gagnante"? Laquelle des trois valeurs sera effectivement assignée à x ? Encore une fois, et peut-être étonnamment, il n'y a pas de règle en C pour nous le dire.

vous pourriez imaginer que la préséance ou l'associativité ou l'évaluation de gauche à droite vous dit ce qui se passe dans l'ordre, mais ils ne le font pas. Vous ne me croyez peut-être pas, mais s'il vous plaît prenez ma parole pour elle, et je vais le dire à nouveau: la préséance et l'associativité ne sont pas déterminez chaque aspect de l'ordre d'évaluation D'une expression en C. en particulier, si dans une expression il y a plusieurs taches différentes où nous essayons d'attribuer une nouvelle valeur à quelque chose comme x , priorité et associativité ne pas dites-nous laquelle de ces tentatives se produit en premier, ou dernier, ou n'importe quoi.


donc avec tout ce fond et l'introduction hors du chemin, si vous voulez vous assurer que tous vos programmes sont bien définis, quelles expressions pouvez-vous écrire, et lesquelles ne pouvez-vous pas écrire?

ces expressions sont toutes bien:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

ces expressions sont toutes non définies:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

et la dernière question Est, Comment Pouvez-vous dire quelles expressions sont bien définies, et quelles expressions sont non définies?

comme je l'ai dit plus tôt, les expressions non définies sont celles où il y a trop de choses qui se passent à la fois, où vous ne pouvez pas être sûr de ce qui se passe dans l'ordre, et où l'ordre compte:

  1. S'il y a une variable qui est modifiée (assignée à) dans deux ou plusieurs endroits différents, comment savez-vous quelle modification se produit en premier?
  2. S'il y a une variable qui est modifiée à un endroit, et dont la valeur est utilisée à un autre endroit, Comment savez-vous si elle utilise l'ancienne ou la nouvelle valeur?

comme exemple de #1, dans l'expression

x = x++ + ++x;

il y a trois tentatives pour modifier `X.

comme exemple de #2, dans l'expression

y = x + x++;

nous utilisons tous les deux la valeur de x , et la modifions.

donc c'est la réponse: assurez-vous que dans n'importe quelle expression vous écrivez, chaque variable est modifiée au plus une fois, et si une variable est modifiée, vous ne tentez pas non plus d'utiliser la valeur de cette variable ailleurs.

2
répondu Steve Summit 2018-08-16 14:54:35
la source