Question Pourquoi ces constructions (utilisant ++) un comportement non défini en C?


#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?
}

711
2018-06-04 09:17


origine


Réponses:


C a le concept de comportement indéfini, c'est-à-dire que certaines constructions de langage sont syntaxiquement valides mais vous ne pouvez pas prédire le comportement lorsque le code est exécuté.

Pour autant que je sache, la norme ne dit pas explicitement Pourquoi le concept de comportement indé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, au lieu d'exiger que toutes les implémentations gèrent le débordement d'entier exactement de la même manière, ce qui risque fort d'entraîner de sérieux coûts. indéfini de sorte que si vous écrivez du code qui provoque un débordement d'entier, tout peut arriver.

Donc, dans cet esprit, pourquoi ces «problèmes»? La langue dit clairement que certaines choses mènent à comportement indéfini. Il n'y a pas de problème, il n'y a pas de "devoir" impliqué. Si le comportement indéfini change quand l'une des variables impliquées est déclarée volatile, cela ne prouve ou ne change rien. C'est indé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 (voir l'article de Wikipédia sur points de séquence).


513
2018-06-04 09:20



Compilez et désassemblez simplement votre ligne de code, si vous êtes si enclin à savoir exactement comment vous obtenez ce que vous obtenez.

C'est ce que je reçois sur ma machine, avec ce que je pense:

$ 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    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-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   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Je suppose que l'instruction 0x00000014 était une sorte d'optimisation du compilateur?)


75
2018-05-24 13:26



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 suivant, un objet doit avoir sa valeur stockée   modifié au plus 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 d'évaluation des opérandes n'est pas spécifié. Si l'on tente de modifier   le résultat d'un opérateur d'affectation ou pour y accéder après le point de séquence suivant, le   le comportement est indéfini.


55
2018-06-04 09:35



Le comportement ne peut pas vraiment être expliqué car il invoque à la fois comportement non spécifié et comportement indéfini, donc nous ne pouvons pas faire de prédictions générales sur ce code, même si vous lisez Olve Maudal's travail tel que Deep C et Non spécifié et indéfini Parfois, vous pouvez faire de bonnes suppositions dans des cas très spécifiques avec un compilateur et un environnement spécifiques, mais ne le faites pas à proximité de la production.

Donc, passer à comportement non spécifié, dans projet de norme c99 section6.5 paragraphe 3 dit(l'accent mien):

Le regroupement des opérateurs et des opérandes est indiqué par la syntaxe.74) Sauf indication contraire   plus tard (pour les opérateurs function-call (), &&, ||,?: et virgule), l'ordre d'évaluation des sous-expressions et l'ordre dans lequel les effets secondaires ont lieu sont tous deux non spécifiés.

Alors quand nous avons une ligne comme celle-ci:

i = i++ + ++i;

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

Nous avons aussi comportement indéfini ici aussi bien que le programme est en train de modifier les variables (i, u, etc.) plus d'une fois entre points de séquence. Du projet de section standard 6.5 paragraphe 2(l'accent mien):

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

il cite les exemples de code suivants comme n'étant pas 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 ; 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 la présente Norme internationale   deux possibilités ou plus et n'impose aucune autre condition sur laquelle est choisi   exemple

et comportement indéfini est défini dans la section 3.4.3 comme:

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

et note que:

Un comportement indéfini possible peut ignorer complètement la situation avec des résultats imprévisibles, se comporter pendant la traduction ou l'exécution du programme de manière documentée caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), terminer une traduction ou une exécution (avec l'émission d'un message de diagnostic).


42
2017-08-15 19:25



La plupart des réponses ici citées de la norme C soulignant que le comportement de ces constructions est indéfini. Comprendre pourquoi le comportement de ces constructions est indéfini, comprenons d'abord ces termes à la lumière de la norme C11:

Séquencé: (5.1.2.3)

Étant donné deux évaluations A et B, si A est séquencé avant B, puis l'exécution de A précède l'exécution de B.

Non séquencé:

Si A n'est pas séquencé avant ou après B, puis A et B sont non séquencés.

Les évaluations peuvent être l'une de deux choses:

  • calculs de valeur, qui élaborent 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 la valeur et effet secondaire associé à A est séquencé avant chaque calcul de la valeur et effet secondaire associé à B.

Maintenant, à la question, pour les expressions comme

int i = 1;
i = i++;

La norme dit que:

6.5 Expressions:

Si un effet secondaire sur un objet scalaire est non séquencé par rapport ànon plus 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 indéfini. [...]

Par conséquent, l'expression ci-dessus appelle UB car deux effets secondaires sur le même objet i est non séquencé l'un par rapport à l'autre. Cela signifie qu'il n'est pas séquencé si l'effet secondaire par affectation à i sera fait avant ou après l'effet secondaire par ++.
Selon que l'affectation se produit avant ou après l'incrément, des résultats différents seront produits et c'est celui du cas de comportement indéfini.

Permet de renommer le i à gauche de l'affectation soit il et à droite de l'affectation (dans l'expression i++) être ir, alors l'expression est 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 Postfix ++ opérateur est que:

juste parce que le ++ vient après la variable ne signifie pas que l'augmentation arrive en retard. L'incrément peut arriver dès que le compilateur aime tant que le compilateur s'assure que la valeur d'origine est utilisée.

Cela signifie l'expression il = ir++ pourrait être évalué soit 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 des effets secondaires par affectation et ++ et invoque donc UB.


41
2018-06-27 00:27



Une autre façon de répondre à cette question, plutôt que de s'enliser dans les détails obscurs des points de séquence et du comportement indéfini, consiste simplement à demander, que sont-ils supposés vouloir dire?  Qu'est-ce que le programmeur essayait de faire?

Le premier fragment demandé à propos de, i = i++ + ++i, est assez clairement fou dans mon livre. Personne ne l'écrirait jamais dans un vrai programme, ce n'est pas évident ce qu'il fait, il n'y a aucun algorithme imaginable que quelqu'un aurait pu essayer de coder qui aurait abouti à cette séquence d'opérations artificielle particulière. Et comme ce n'est pas évident pour vous et moi de savoir ce qu'il est censé faire, c'est bien dans mon livre si le compilateur ne peut pas non plus comprendre ce qu'il est supposé faire.

Le deuxième fragment, i = i++, est un peu plus facile à comprendre. Quelqu'un essaye clairement d'incrémenter i et d'assigner le résultat à i. Mais il y a deux façons de le faire en C. La façon la plus simple d'ajouter 1 à i, et d'assigner le résultat à i, est la même dans presque tous les langages de programmation:

i = i + 1

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

i++

Cela signifie "ajouter 1 à i, et affecter le résultat à i". Donc, si nous construisons un méli-mélo des deux, en écrivant

i = i++

ce que nous disons vraiment, c'est "ajouter 1 à i, et assigner le résultat à i, et assigner le résultat à i". 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 est quand les gens les utilisent comme des exemples artificiels de la façon dont ++ est censé 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 que signifie une expression utilisant ++, ne l'écrivez pas."

Nous passions d'innombrables heures sur comp.lang.c à discuter des expressions comme celles-ci et Pourquoi ils sont indéfinis. Deux de mes réponses plus longues, qui essaient d'expliquer vraiment pourquoi, sont archivées sur le web:


27
2018-06-18 11:55



Bien qu'il soit peu probable que les compilateurs et les processeurs le fassent, il serait légal, selon la norme C, que le compilateur mette en œuvre "i ++" avec la séquence suivante:

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 efficacement, on peut facilement imaginer des situations où un tel comportement rendrait le code multi-thread plus facile (par exemple, si deux threads essayaient séquence simultanément, i serait incrémenté de deux) et il n'est pas totalement inconcevable qu'un futur processeur puisse fournir quelque chose comme ça.

Si le compilateur devait écrire i++ comme indiqué ci-dessus (légal sous la norme) et devaient s'entrecroiser les instructions ci-dessus tout au long de l'évaluation de l'expression globale (également légal), et si elle ne venait pas à remarquer que l'une des autres instructions est arrivé à accéder i, il serait possible (et légal) pour le compilateur de générer une séquence d'instructions qui bloquerait. Pour être sûr, un compilateur devrait presque certainement détecter le problème dans le cas où la même variable i est utilisé aux deux endroits, mais si une routine accepte des 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 obligé de reconnaître ou d'éviter l'interblocage qui se produirait si l'adresse du même objet était transmise pour les deux p et q.


22
2017-12-05 18:30



Souvent, cette question est liée en tant que doublon de questions liées au code comme

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

ou

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

ou des variantes similaires.

Alors que c'est aussi comportement indéfini comme indiqué déjà, il existe des différences subtiles lorsque printf() est impliqué lors de la comparaison à une déclaration comme:

   x = i++ + i++;

Dans la déclaration suivante:

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

la ordre d'évaluation des arguments dans printf() est non spécifié. Cela signifie, les expressions i++ et ++i pourrait être évalué dans n'importe quel ordre. Norme C11 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ées 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 lorsque   Norme internationale prévoit deux possibilités ou plus et impose   pas d'autres exigences sur lesquelles est choisi dans tous les cas.

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

le comportement non spécifié lui-même n'est pas un problème. Considérez cet exemple:

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

Cela aussi a comportement non spécifié parce que l'ordre d'évaluation de ++x et y++ est non spécifié. Mais c'est une déclaration parfaitement légale et valide. Il y a non comportement indéfini dans cette instruction. Parce que les modifications (++x et y++) sont faites pour distinct objets.

Qu'est-ce qui rend la déclaration suivante

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

comme comportement indéfini est le fait que ces deux expressions modifient la même objet i sans intervention point de séquence.


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

C'est une distinction importante car le opérateur virgule introduit un point de séquence entre l'évaluation de leurs opérandes, ce qui rend légal ce qui suit:

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 virgule évalue ses opérandes de gauche à droite et ne donne que la valeur du dernier opérande. Donc dans j = (++i, i++);, ++i incréments i à 6 et i++ donne l'ancienne valeur de i (6) qui est affecté à j. alors i devient 7 en raison de post-incrément.

Donc, si le virgule dans l'appel de la 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 indéfini parce que le virgule Voici une séparateur.


Pour ceux qui sont nouveaux à comportement indéfini bénéficierait de la lecture Ce que tout programmeur C devrait savoir à propos du comportement non défini  pour comprendre le concept et beaucoup d'autres variantes de comportement indéfini en C.

Ce post: Comportement indéfini, non spécifié et défini par l'implémentation est également pertinent.


16
2017-12-30 20:26



La norme C indique qu'une variable ne doit être affectée qu'une fois entre deux points de séquence. Un point-virgule est par exemple un point de séquence.
Donc, 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 indéterminé. Certains compilateurs les détectent et produisent des résultats, mais ce n'est pas le cas.

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
2017-09-11 12:36