web-dev-qa-db-fra.com

Pourquoi ces constructions utilisent-elles des comportements indéfinis avant et après incrémentation?

#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\n", ++w, w); // shouldn't this print 1 1

   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?
}
768
PiX

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

Autant que je sache, la norme ne dit pas explicitement pourquoi le concept de comportement non défini existe. Dans mon esprit, c’est tout simplement parce que les concepteurs de langage souhaitaient avoir une marge de manœuvre dans la sémantique, au lieu d’exiger que toutes les implémentations gèrent le dépassement d’entier de la même manière, ce qui imposerait très probablement de graves coûts de performance. indéfini, de sorte que tout peut arriver si vous écrivez du code causant un débordement d’entier.

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

Votre exemple le plus intéressant, celui avec

u = (u++);

est un exemple textuel de comportement indéfini (voir l'entrée de Wikipedia sur séquence sequence ).

547
unwind

Il suffit de compiler et de désassembler votre ligne de code, si vous êtes si enclin à savoir exactement comment vous obtenez ce que vous obtenez.

Voici ce que j'ai 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    $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?)

76
badp

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 suivant, la valeur stockée d'un objet doit être modifiée au plus une fois par l'évaluation d'une expression. De plus, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker.

et 6.5.16 Opérateurs d'assignation, §4:

L'ordre d'évaluation des opérandes est indéterminé. Si vous tentez de modifier le résultat d'un opérateur d'affectation ou d'y accéder après le point de séquence suivant, le comportement n'est pas défini.

59
Christoph

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

Séquencé: (5.1.2.3)

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

sans séquence:

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 l'une des deux choses suivantes:

  • 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 tout calcul de valeur et associé à A est séquencé avant chaque calcul de valeur et associé à B.

J'en viens 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 n'est pas séquencé par rapport à , soit un autre effet secondaire 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 ne sont pas séquencés l'un par rapport à l'autre. Cela signifie qu’il n’est pas ordonné si l’effet secondaire par affectation à i sera effectué avant ou après l’effet secondaire par ++.
Selon que l'affectation a lieu avant ou après l'incrément, différents résultats seront produits et c'est l'un des cas de comportement non défini .

Permet de renommer la i à gauche de l'affectation soit il et à droite de l'affectation (dans l'expression i++) soit ir, alors l'expression soit semblable à

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

n point important concernant Postfix ++ l'opérateur est que:

simplement parce que le ++ vient après la variable ne signifie pas que l'incrément se produit tardivement . L'incrémentation peut avoir lieu dès que le compilateur aime tant que le compilateur s'assure que la valeur d'origine est utilisée .

Cela signifie que l'expression il = ir++ pourrait être évaluée en tant que

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  

il en résulte deux résultats différents 1 et 2 qui dépendent de la séquence des effets secondaires par affectation et ++ et donc qui invoquent UB.

49
haccks

Le comportement ne peut pas vraiment être expliqué car il appelle à la fois comportement non spécifié et comportement non défini , nous ne pouvons donc pas prédire de manière générale ce code, même si vous lisez Olve Maudal , tel que Deep C et nspecified and Undefined vous pouvez parfois deviner de bonnes suppositions avec un compilateur spécifique et environnement, mais s'il vous plaît ne le faites pas n'importe où près de la production.

Passons donc à un comportement non spécifié , dans projet de norme c99 section6.5 paragraphe 3 dit ( l'emphase mine ):

Le groupement d’opérateurs et d’opérandes est indiqué par la syntaxe.74) Sauf indication contraire (pour les opérateurs function-call (), &&, ||,?: Et virgule), L'ordre d'évaluation des sous-expressions et l'ordre dans lequel les effets secondaires se produisent sont indéterminés.

Alors, quand on a 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 également ici un comportement indéfini puisque le programme modifie des variables (i, u, etc ..) plusieurs fois entre points de séquence . Extrait du projet de section standard 6.5 paragraph 2 ( l'emphase mine ):

Entre la séquence précédente et la séquence suivante, la valeur stockée d'un objet doit être modifiée au plus une fois par l'évaluation d'une expression. En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker .

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

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

Dans tous ces exemples, le code tente de modifier un objet plusieurs fois dans le même point de séquence, qui se terminera par le ; dans chacun des cas suivants:

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

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

utilisation d'une valeur non spécifiée ou autre comportement lorsque la présente Norme internationale prévoit deux possibilités ou plus et n'impose aucune exigence supplémentaire, ce qui est le cas

et le comportement non dé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 lequel la présente Norme internationale n'impose aucune exigence

et note que:

Le comportement non défini possible peut aller d’ignorer complètement la situation avec des résultats imprévisibles, de se comporter pendant la traduction ou l’exécution du programme d’une manière documentée caractéristique de l’environnement (avec ou sans émission d’un message de diagnostic), en mettant fin à la traduction ou à l’exécution (avec la publication). d'un message de diagnostic).

49
Shafik Yaghmour

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 ce qu'ils sont censés vouloir dire Qu'est-ce que le programmeur essayait de faire?

Le premier fragment interrogé, i = i++ + ++i, est assez clairement fou dans mon livre. Personne ne l'écrirait jamais dans un vrai programme, ce n'est pas évident, il n'y a pas d'algorithme concevable que quelqu'un aurait pu essayer de coder, ce qui aurait abouti à cette séquence d'opérations artificielle particulière. Et puisqu'il n'est pas évident pour vous et moi de comprendre ce que cela est supposé faire, c'est bien dans mon livre si le compilateur ne peut pas comprendre ce qu'il est censé 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'assigner le résultat à i. Mais il y a plusieurs façons de le faire en C. Le moyen le plus simple d’ajouter 1 à i et d’attribuer le résultat à i est le même dans presque tous les langages de programmation:

i = i + 1

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

i++

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

i = i++

ce que nous disons en réalité, c’est "ajoute 1 à i, assigne le résultat à i et assigne le résultat à i". Nous sommes confus, donc cela ne me dérange pas trop si le compilateur devient confus aussi.

En réalité, ces expressions loufoques ne sont écrites que lorsque les gens les utilisent comme exemples artificiels de la manière dont ++ est censé fonctionner. Et bien sûr, il est important de comprendre le fonctionnement de ++. Mais une règle pratique pour utiliser ++ est la suivante: "S'il n'est pas évident de comprendre une expression utilisant ++, ne l'écrivez pas".

Nous avions l'habitude de passer d'innombrables heures à comp.lang.c à discuter d'expressions comme celles-ci et pourquoi elles n'étaient pas définies. Deux de mes réponses les plus longues, qui tentent de vraiment expliquer pourquoi, sont archivées sur le Web:

Voir aussi question 3.8 et le reste des questions de la section section de la liste C FAQ .

31
Steve Summit

Cette question est souvent liée à un double des questions liées au code tel que

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

ou

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

ou des variantes similaires.

Bien que cela soit aussi un comportement indéfini comme déjà indiqué, il existe des différences subtiles lorsque printf() est impliqué lors de la comparaison avec une instruction telle que:

x = i++ + i++;

Dans la déclaration suivante:

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

l'ordre d'évaluation d'arguments dans printf() est non spécifié . Cela signifie que les expressions i++ et ++i pourraient ê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 l'indicateur de fonction, les arguments et les sous-expressions dans les arguments est évalué 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 autre comportement lorsque la présente Norme internationale offre deux possibilités ou plus et n'impose aucune exigence supplémentaire, ce qui est choisi en aucune circonstance.

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

Le comportement non spécifié n'est pas un problème en soi. Considérons cet exemple:

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

Cela aussi a un comportement non spécifié parce que l'ordre d'évaluation de ++x et y++ n'est pas spécifié. Mais c'est une déclaration parfaitement légale et valide. Il y a aucun comportement non défini dans cette instruction. Parce que les modifications (++x et y++) sont effectuées sur des objets distincts .

Que rend la déclaration suivante

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

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


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

C'est une distinction importante car l'opérateur de virgule introduit un point de séquence entre l'évaluation de leurs opérandes, ce qui rend les valeurs légales suivantes: :

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

Donc, si la 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 appelle un comportement indéfini car la virgule est un séparateur .


Pour ceux qui découvrent un comportement non défini , il serait utile de lire Ce que tout programmeur C devrait savoir sur le comportement non défini pour comprendre le concept et de nombreuses autres variantes de non défini comportement en C.

Cet article: comportement indéfini, non spécifié et défini par l'implémentation est également pertinent.

23
P.P.

Bien qu'il soit peu probable que des compilateurs et des processeurs le fassent, il serait légal, en vertu de 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 de se faire efficacement, on peut facilement imaginer des situations dans lesquelles un tel comportement faciliterait le code multithread (par exemple, cela garantirait que si deux threads essayaient d'exécuter ce qui précède séquence simultanément, i serait incrémenté de deux) et il n’est pas totalement inconcevable qu’un processeur futur fournisse une fonctionnalité similaire.

Si le compilateur écrivait i++ comme indiqué ci-dessus (légal selon la norme) et intercalait les instructions ci-dessus tout au long de l'évaluation de l'expression globale (également légal), et s'il ne se produisait pas Parmi les autres instructions utilisées pour accéder à i, il serait possible (et légal) pour le compilateur de générer une séquence d'instructions qui aboutirait à un blocage. Pour être sûr, un compilateur détectera 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 tenu de reconnaître ni d'éviter l'impasse qui se produirait si l'adresse du même objet était passé pour p et q.

22
supercat

Bien que la syntaxe des expressions telles que a = a++ ou a++ + a++ soit légale, le comportement de ces constructions est non défini car a shall en standard C n’est pas obéi. C99 6.5p2 :

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

Avec note de bas de page 7 clarifiant davantage que

  1. Ce paragraphe rend des expressions d’instruction non définies telles que

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

    tout en permettant

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

Les différents points de séquence sont énumérés à l'annexe C de C11 (et C99 ):

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

    • Entre les évaluations du désignateur de fonction et les arguments réels d'un appel de fonction et l'appel réel. (6.5.2.2).
    • Entre les évaluations du premier et du second opérandes des opérateurs suivants: ET logique (logique) && (6.5.13); logique OR || (6.5.14); virgule (6.5.17).
    • Entre les évaluations du premier opérande du conditionnel? : opérateur et quels que soient les deuxième et troisième opérandes évalués (6.5.15).
    • La fin d'un déclarateur complet: déclarateurs (6.7.6);
    • Entre l'évaluation d'une expression complète et la prochaine expression complète à évaluer. Les expressions suivantes sont des expressions complètes: un initialiseur qui ne fait pas partie d'un littéral composé (6.7.9); l'expression dans une déclaration d'expression (6.8.3); l'expression de contrôle d'une instruction de sélection (if ou switch) (6.8.4); l'expression de contrôle d'une déclaration while ou do (6.8.5); chacune des expressions (facultatives) d'une instruction for (6.8.5.3); l'expression (facultative) dans une instruction return (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, ainsi qu'entre tout appel à une fonction de comparaison et tout mouvement des objets transmis en tant qu'arguments à cet appel (7.22.5).

Le libellé de la même paragraphe en C11 est:

  1. Si un effet secondaire sur un objet scalaire n'est pas séquencé 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 est indéfini. S'il existe plusieurs classements autorisés des sous-expressions d'une expression, le comportement n'est pas défini si un tel effet secondaire non séquencé se produit dans l'un des classements.84)

Vous pouvez détecter de telles erreurs dans un programme, par exemple en utilisant une version récente de GCC avec -Wall et -Werror, puis GCC refusera carrément 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 qu'est-ce qu'un point de séquence - et ) qu'est-ce que est-il un point de séquence et que n'est pas . Par exemple, l'opérateur de virgule est un point de séquence.

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

est bien défini et incrémentera i de un, donnant l'ancienne valeur, écartera cette valeur; ensuite, chez l’opérateur par virgule, réglez les effets secondaires; puis incrémentez i de un, et la valeur résultante devient la valeur de l’expression - c’est-à-dire qu’il s’agit simplement d’une manière artificielle d’écrire j = (i += 2) qui est encore une manière "intelligente" d’écrire

i += 2;
j = i;

Cependant, la liste d'arguments de fonction , in est un opérateur de virgule et et il n'y a pas de point de séquence entre les évaluations d'arguments distincts; au lieu de cela, leurs évaluations ne sont pas séquencées les unes par rapport aux autres; donc l'appel de fonction

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

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

14
Antti Haapala

La norme C stipule qu’une variable ne doit être affectée qu’au maximum une fois entre deux points de séquence. Un point-virgule, par exemple, est un point de séquence.
Ainsi, chaque déclaration de la forme:

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

et ainsi de suite enfreignent cette règle. La norme indique également 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 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 de codage courante lors de la copie/analyse de chaînes.

14
Nikhil Vidhani

Dans https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c quelqu'un a posé une question à propos d'une déclaration du type:

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 ... le PO s'attend à ce qu'il en imprime 6.

Les incréments ++i ne sont pas tous garantis complets avant le reste des calculs. En fait, différents compilateurs obtiendront des résultats différents ici. Dans l'exemple que vous avez fourni, les 2 premiers ++i ont été exécutés, puis 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

Les compilateurs modernes optimiseront cela très bien. En fait, peut-être mieux que le code que vous avez écrit à l'origine (en supposant qu'il fonctionne comme vous l'espériez).

10
TomOnTime

Votre question n'était probablement pas: "Pourquoi ces constructions ont-elles un comportement indéfini en C?". Votre question était probablement: "Pourquoi ce code (avec ++) ne m'a-t-il pas donné la valeur à laquelle je m'attendais?", Et quelqu'un a marqué votre question comme un doublon et vous a envoyé ici.

Cette réponse tente de répondre à la question suivante: pourquoi votre code ne vous a-t-il pas donné la réponse attendue et comment apprendre à reconnaître (et à éviter) les expressions qui ne fonctionneront pas comme prévu .

Je suppose que vous avez déjà entendu la définition de base des opérateurs ++ et -- de C, et en quoi la forme de préfixe ++x diffère de la forme postfixée x++. Mais ces opérateurs sont difficiles à penser, alors pour vous assurer de bien comprendre, vous avez peut-être écrit un tout petit programme de test comportant quelque chose comme:

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

Mais, à votre grande surprise, ce programme ne vous a pas aidé à comprendre - il a imprimé une sortie étrange, inattendue, inexplicable, suggérant que peut-être que ++ fait quelque chose de complètement différent, pas du tout ce que vous pensiez que c'était le cas.

Ou peut-être que vous cherchez 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 casse-tête. Ce code n'a pas non plus de sens, surtout si vous l'exécutez - et si vous le compilez et que vous l'exécutez sous deux compilateurs différents, vous obtiendrez probablement deux réponses différentes! Quoi de neuf avec ça? Quelle réponse est correcte? (Et la réponse est que les deux sont, ou aucun d'entre eux sont.)

Comme vous l'avez déjà entendu, toutes ces expressions sont non définies , ce qui signifie que le langage C ne donne aucune garantie sur ce qu'elles vont faire. C'est un résultat étrange et surprenant, car vous pensiez probablement que tout programme que vous pourriez écrire, tant qu'il serait compilé et exécuté, générerait une sortie unique et bien définie. Mais dans le cas d'un comportement indéfini, ce n'est pas le cas.

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

Pour les expressions dont nous parlons, ce qui les rend non définies, c'est quand il y a trop de choses à la fois, quand on ne sait pas dans quel ordre les choses vont se passer, mais quand l'ordre compte pour le résultat que l'on obtient.

Revenons aux 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, le compilateur calcule-t-il la valeur de x en premier, ou x++, ou peut-être ++x? Mais il s'avère que nous ne savons pas . Aucune règle en C n'indique que les arguments d'une fonction sont évalués de gauche à droite, de droite à gauche ou dans un autre ordre. Nous ne pouvons donc pas dire si le compilateur fera x en premier, puis ++x, puis x++, ou x++ puis ++x puis x , ou un autre ordre. Mais l'ordre a clairement son importance car, selon l'ordre utilisé par le compilateur, nous aurons clairement des résultats différents imprimés par printf.

Qu'en est-il de cette expression folle?

x = x++ + ++x;

Le problème de 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, stocke la nouvelle valeur dans x et renvoie le ancienne valeur de x; (2) la partie ++x tente d'ajouter 1 à x, de stocker la nouvelle valeur dans x et de renvoyer la nouvelle valeur de x; et (3) la partie x = tente d'attribuer la somme des deux autres à x. Laquelle de ces trois tentatives de mission "gagnera"? Laquelle des trois valeurs sera effectivement affectée à x? Encore une fois, et peut-être étonnamment, il n’ya pas de règle en C à nous dire.

Vous pouvez imaginer que la priorité, l'associativité ou l'évaluation de gauche à droite vous indiquent l'ordre dans lequel les choses se passent, mais ce n'est pas le cas. Vous ne me croirez peut-être pas, mais je vous en prie, prenez ma Parole et je le répète: la préséance et l’associativité ne déterminent pas tous les aspects de l’ordre d’évaluation d’une expression en C. En particulier, s’il existe plusieurs expressions dans une même expression. différents endroits où nous essayons d’attribuer une nouvelle valeur à quelque chose comme x, à la priorité et à l’associativité ne pas nous indique laquelle de ces tentatives a lieu en premier, ou en dernier, ou quoi que ce soit .


Donc, avec tout cet arrière-plan et cette introduction, 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 bonnes:

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 la suivante: comment savoir quelles expressions sont bien définies et quelles expressions ne sont pas définies?

Comme je l'ai dit plus tôt, les expressions non définies sont celles où il y a trop d'activités en même temps, où vous ne pouvez pas savoir avec certitude dans quel ordre les choses se passent et où l'ordre est important:

  1. Si une variable est modifiée (affectée à) à plusieurs endroits différents, comment savoir quelle modification a lieu en premier?
  2. Si une variable est modifiée à un endroit et que sa valeur est utilisée à un autre endroit, comment savoir si elle utilise l'ancienne ou la nouvelle valeur?

A titre d'exemple de # 1, dans l'expression

x = x++ + ++x;

il y a trois tentatives pour modifier `x.

A titre d'exemple de # 2, dans l'expression

y = x + x++;

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

Voilà donc la réponse: assurez-vous que dans toute expression que vous écrivez, chaque variable est modifiée au maximum une fois, et si une variable est modifiée, vous ne tentez pas également d'utiliser la valeur de cette variable ailleurs.

5
Steve Summit

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

J'explique les idées.

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

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

Les points de séquence dans une expression comme i=i++ se trouvent avant i= et après i++.

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

L'ordre des instructions à l'intérieur d'une boîte n'a donc 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 ces formes pour interpréter le code i=i++ sont valides et qu'elles génèrent des réponses différentes, le comportement est indéfini.

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

MODIFIER:

Les entrées de c-faq site (également publié sous forme de livre ), à savoir ici et ici et ici .

4
alinsoar

La raison en est que le programme exécute un comportement indé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 l'autre selon la terminologie C++ 11).

Toutefois, si vous vous en tenez à un compilateur, le comportement persiste tant que vous n'ajoutez pas d'appels de fonction ou de pointeurs, ce qui rendrait le comportement plus désordonné.

  • Alors tout d’abord le GCC: 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 sous-expressions de gauche à droite pour le côté droit (RHS), puis attribue la valeur au côté gauche (LHS). C’est exactement ainsi que Java et C # se comportent et définissent leurs normes. (Oui, le logiciel équivalent dans Java et C # a des 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é en premier, puis la valeur c est utilisée pour l'opération, puis le post-incrément c ++).

selon GCC C++: opérateurs

Dans GCC C++, la priorité des opérateurs contrôle l'ordre dans lequel les opérateurs individuels sont évalués.

le code équivalent dans le comportement défini C++ tel que GCC le 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
}

Ensuite, nous allons à Visual Studio . 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-t-il, il adopte une autre approche, il évalue toutes les expressions pré-incrémentées lors du premier passage, puis utilise les valeurs des variables dans les opérations du second passage, assigne de RHS à LHS lors du troisième passage, puis évalue tous les expressions post-incrémentation en un seul passage.

Donc, l'équivalent dans le comportement défini C++ que Visual C++ 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 indiqué dans la documentation de Visual Studio à l'adresse priorité et ordre d'évaluation :

Lorsque plusieurs opérateurs apparaissent ensemble, ils ont la même priorité et sont évalués en fonction de leur associativité. Les opérateurs du tableau sont décrits dans les sections commençant par Opérateurs Postfix.

3
Muhammad Annaqeeb