Pourquoi ces constructions utilisent-elles un comportement non défini avant et après l'incrémentation?

Jun 04 2009
#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?
}

Réponses

573 unwind Jun 04 2009 at 16:20

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

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 dans la sémantique, 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 imposerait très probablement de sérieux coûts de performance, ils ont juste laissé le comportement undefined de sorte que si vous écrivez du code qui provoque un débordement d'entier, tout peut arriver.

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

Votre exemple le plus intéressant, celui avec

u = (u++);

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

76 badp May 24 2010 at 20:26

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

Voici ce que je reçois sur ma machine, avec ce que je pense qu'il 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?)

66 Christoph Jun 04 2009 at 16:35

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 voir sa valeur stockée 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.

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

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

61 haccks Jun 27 2015 at 07:27

La plupart des réponses ici citées à partir de la norme C soulignant 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 de la norme C11:

Séquence: (5.1.2.3)

Étant donné deux évaluations quelconques Aet B, si elle Aest séquencée avant B, l'exécution de Adoit précéder l'exécution de B.

Non séquencé:

Si An'est pas séquencé avant ou après B, alors Aet ne Bsont 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 Aet Bimplique que chaque calcul de valeur et effet secondaire associé à Aest séquencé avant chaque calcul de valeur et effet secondaire associé à B.

Venons-en 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ée par rapport à soit un effet secondaire différent sur le même objet scalaire ou un calcul de valeur en utilisant la valeur d'un 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 ne isont pas séquencés l'un par rapport à l'autre. Cela signifie qu'il n'est pas séquencé si l'effet secondaire par affectation à isera effectué avant ou après l'effet secondaire par ++.
Selon que l'affectation se produit avant ou après l'incrémentation, différents résultats seront produits et c'est le cas du comportement indéfini .

Permet de renommer le ià gauche de l'affectation être ilet à droite de l'affectation (dans l'expression i++) être ir, alors l'expression ressemble à

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:

juste parce que le ++vient après la variable ne signifie pas que l'incrément se produit en retard . L'incrémentation peut se produire dès que le compilateur le souhaite tant que le compilateur s'assure que la valeur d'origine est utilisée .

Cela signifie que l'expression il = ir++peut être évaluée 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 1et 2qui dépend de la séquence des effets secondaires par affectation et ++et donc invoque UB.

54 ShafikYaghmour Aug 16 2013 at 02:25

Le comportement ne peut pas vraiment être expliqué car il invoque à la fois un comportement non spécifié et un comportement non défini , nous ne pouvons donc pas faire de prédictions générales sur ce code, bien que si vous lisez le travail d' Olve Maudal tel que Deep C et Unspecified and Undefined, vous pouvez parfois faire du bien devine dans des cas très spécifiques avec un compilateur et un environnement spécifiques, mais veuillez ne pas le faire à proximité de la production.

Passant donc à un comportement non spécifié , dans le projet de section standard c99 , le 6.5paragraphe 3 dit (c'est moi qui souligne ):

Le groupement des opérateurs et des opérandes est indiqué par la syntaxe.74) Sauf indication contraire (pour les opérateurs d'appel de fonction (), &&, ||,?:, Et virgule), l'ordre d'évaluation des sous-expressions et l'ordre dans quels effets secondaires surviennent ne sont pas précisés.

Donc, quand nous avons une ligne comme celle-ci:

i = i++ + ++i;

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

Nous avons aussi un comportement non défini ici aussi puisque le programme est en train de modifier les variables ( i, u, etc ..) plus d'une fois entre les points de séquence . Extrait du 6.5paragraphe 2 du projet de section standard (c'est moi qui souligne ):

Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée 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 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;
^   ^       ^

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

utilisation d'une valeur non spécifiée, ou d'un autre comportement lorsque la présente Norme internationale offre au moins deux possibilités et n'impose aucune autre exigence sur laquelle est choisie en tout état de cause

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

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 indéfini possible va de l'ignorance totale de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans l'émission d'un message de diagnostic), à la fin d'une traduction ou d'une exécution (avec l'émission d'un message de diagnostic).

38 SteveSummit Jun 18 2015 at 18:55

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

Le premier fragment interrogé i = i++ + ++i, est clairement insensé 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 pas d'algorithme concevable 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 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 censé faire.

Le deuxième fragment, i = i++est un peu plus facile à comprendre. Quelqu'un essaie clairement d'incrémenter i et de réattribuer le résultat à i. Mais il y a plusieurs façons de faire cela en C.La façon la plus élémentaire d'ajouter 1 à i et d'attribuer 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 attribuer 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 attribuer le résultat à i, et attribuer le résultat à i". Nous sommes confus, donc cela ne me dérange pas trop si le compilateur est également confus.

En réalité, la seule fois où ces expressions folles sont écrites, c'est lorsque 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 la signification d'une expression utilisant ++ n'est pas évidente, ne l'écrivez pas."

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

  • Pourquoi la norme ne définit-elle pas leur rôle?
  • La priorité des opérateurs ne détermine-t-elle pas l'ordre d'évaluation?

Voir aussi question 3.8 et le reste des questions à l' article 3 de la liste C FAQ .

27 P.P Dec 31 2015 at 03:26

Souvent, cette question est liée comme un 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.

Bien qu'il s'agisse également d' un comportement non défini, comme indiqué déjà, il existe des différences subtiles lorsqu'il printf()est impliqué lors de la comparaison à une déclaration telle que:

x = i++ + i++;

Dans la déclaration suivante:

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

l' ordre d'évaluation des arguments dans printf()n'est pas spécifié . Cela signifie, des expressions i++et ++ipourrait être évalué dans n'importe quel ordre. La 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é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 lorsque la présente Norme internationale offre deux ou plusieurs possibilités et n'impose aucune autre exigence sur laquelle est choisie en aucun 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 spécifié en lui-même n'est PAS un problème. Prenons cet exemple:

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

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

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

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

car un comportement indéfini est le fait que ces deux expressions modifient le même objet isans point de séquence intermédiaire .


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

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

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 renvoie uniquement la valeur du dernier opérande. Donc j = (++i, i++);, par ++iincréments ide 6et i++rendements ancienne valeur i( 6) qui est attribué à j. Devient ialors 7due à la post-incrémentation.

Donc, si la virgule dans l'appel de fonction devait être un opérateur virgule, alors

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

ne sera pas un problème. Mais il invoque un comportement indéfini car la virgule est ici un séparateur .


Pour ceux qui ne connaissent pas le 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 comportement non défini en C.

Ce message: Un comportement non défini, non spécifié et défini par l'implémentation est également pertinent.

23 supercat Dec 06 2012 at 01:30

Bien qu'il soit peu probable que des compilateurs et des processeurs le fassent réellement, il serait légal, selon la norme C, pour le compilateur d'implémenter "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 manière efficace, on peut facilement imaginer des situations où un tel comportement faciliterait le code multithread (par exemple, il garantirait que si deux threads essaient d'exécuter ce qui précède séquence simultanée, iserait incrémentée de deux) et il n'est pas totalement inconcevable qu'un futur processeur puisse fournir une fonctionnalité comme celle-là.

Si le compilateur devait écrire i++comme indiqué ci-dessus (légal selon la norme) et entrecouper les instructions ci-dessus tout au long de l'évaluation de l'expression globale (également légale), et s'il ne s'est pas rendu compte que l'une des autres instructions s'est produite pour y accéder i, il serait possible (et légal) pour le compilateur de générer une séquence d'instructions qui provoquerait un blocage. Pour être sûr, un compilateur détectera presque certainement le problème dans le cas où la même variable iest utilisée aux deux endroits, mais si une routine accepte des références à deux pointeurs pet q, et utilise (*p)et (*q)dans l'expression ci-dessus (plutôt que d'utiliser ideux fois) le Le compilateur ne serait pas obligé de reconnaître ou d'éviter le blocage qui se produirait si l'adresse du même objet était transmise à la fois pour pet q.

18 AnttiHaapala Mar 26 2017 at 21:58

Alors que la syntaxe des expressions comme a = a++ou a++ + a++est légale, le comportement de ces constructions n'est pas défini car un doit en C standard n'est pas respecté. C99 6.5p2 :

  1. Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée 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 à stocker [73]

Avec la note de bas de page 73 précisant 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 en 5.1.2.3:

    • Entre les évaluations de l'indicateur de fonction et les arguments réels dans un appel de fonction et l'appel réel. (6.5.2.2).
    • Entre les évaluations des premier et deuxième opérandes des opérateurs suivants: ET logique && (6.5.13); OU logique || (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 évalué (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 complètes: un initialiseur qui ne fait pas partie d'un littéral composé (6.7.9); l'expression dans une instruction d'expression (6.8.3); l'expression de contrôle d'une instruction de sélection (if ou switch) (6.8.4); l'expression dominante d'une instruction 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 passés en arguments à cet appel (7.22.5).

Le libellé du 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 n'est pas défini. S'il y a plusieurs ordres 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 ordres.84)

Vous pouvez détecter de telles erreurs dans un programme en utilisant par exemple une version récente de GCC avec -Wallet -Werror, puis GCC refusera carrément de compiler votre programme. Voici 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

L'important est de savoir ce qu'est un point de séquence - et ce qu'est un point de séquence et ce qui ne l'est pas . Par exemple, l' opérateur virgule est un point de séquence, donc

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

est bien défini, et incrémentera ide un, ce qui donnera l'ancienne valeur, rejettera cette valeur; puis à l'opérateur virgule, réglez les effets secondaires; puis incrémenter ide un, et la valeur résultante devient la valeur de l'expression - c'est-à-dire que ce n'est qu'une manière artificielle d'écrire j = (i += 2)qui est encore une manière "intelligente" d'écrire

i += 2;
j = i;

Cependant, les ,listes d'arguments in function ne sont pas un opérateur virgule et il n'y a pas de point de séquence entre les évaluations d'arguments distincts; au contraire, 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 un comportement indéfini car il n'y a pas de point de séquence entre les évaluations de i++et ++idans les arguments de fonction , et la valeur de iest donc modifiée deux fois, par les deux i++et ++i, entre le point de séquence précédent et suivant.

14 NikhilVidhani Sep 11 2014 at 19:36

La norme C dit qu'une variable ne doit être affectée au plus qu'une fois entre deux points de séquence. Un point-virgule par exemple est un point de séquence.
Donc, chaque déclaration du formulaire:

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

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

11 TomOnTime Apr 08 2015 at 10:20

Dans https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c quelqu'un a posé une question sur 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 ++iincréments ne sont pas garantis pour tous être terminés 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 ++iexécuté, les valeurs de k[]ont été lus, la dernière ++ialors k[].

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

Les compilateurs modernes optimiseront très bien cela. 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é).

6 SteveSummit Aug 16 2018 at 18:54

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

Cette réponse tente de répondre à cette question: pourquoi votre code ne vous a-t-il pas donné la réponse attendue, et comment pouvez-vous 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 C ++et des --opérateurs, et en quoi la forme de préfixe ++xdiffère de la forme de suffixe x++. Mais il est difficile de penser à ces opérateurs, donc pour être sûr de bien comprendre, vous avez peut-être écrit un tout petit programme de test impliquant 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 et inexplicable, suggérant que peut-être ++fait quelque chose de complètement différent, pas du tout ce que vous pensiez faire.

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 casse-tête. Ce code n'a également aucun sens, surtout si vous l'exécutez - et si vous le compilez et l'exécutez sous deux compilateurs différents, vous obtiendrez probablement deux réponses différentes! Qu'est-ce qui se passe avec ça? Quelle réponse est correcte? (Et la réponse est que les deux le sont, ou qu'aucun d'eux ne l'est.)

Comme vous l'avez déjà entendu, toutes ces expressions ne sont pas 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 est 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 indéfinie? Les expressions impliquent-elles ++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 bien définis.

Pour les expressions dont nous parlons, ce qui les rend indéfinies, c'est quand il se passe trop de choses à la fois, quand nous ne savons pas dans quel ordre les choses vont se passer, mais quand l'ordre compte pour le résultat que nous obtenons.

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, est-ce que le compilateur calcule la valeur de xfirst, or x++, ou peut ++x- être ? Mais il s'avère que nous ne savons pas . Il n'y a pas de règle en C qui dit que les arguments d'une fonction sont évalués de gauche à droite, ou de droite à gauche, ou dans un autre ordre. Donc , nous ne pouvons pas dire si le compilateur fera d' xabord, puis ++x, puis x++, ou x++alors ++xalors x, ou d' un autre ordre. Mais l'ordre compte clairement, car en fonction de l'ordre utilisé par le compilateur, nous obtiendrons clairement des résultats différents imprimés par printf.

Et cette expression folle?

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 x++partie essaie d'ajouter 1 à x, de stocker la nouvelle valeur dans xet de renvoyer l'ancienne valeur de x; (2) la ++xpartie essaie d'ajouter 1 à x, de stocker la nouvelle valeur dans xet de renvoyer la nouvelle valeur de x; et (3) la x =partie essaie d'affecter la somme des deux autres à x. Laquelle de ces trois tentatives d'affectation «gagnera»? À laquelle des trois valeurs sera réellement attribué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 indiquent dans quel ordre les choses se passent, mais ce n'est pas le cas. Vous ne me croyez peut-être pas, mais croyez-moi sur 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, si dans une même expression il y a plusieurs différents endroits où nous essayons d'assigner une nouvelle valeur à quelque chose comme x, la priorité et associativité ne pas nous dire que ces tentatives se premier ou dernier, ou quoi que ce soit.


Donc, avec tout ce contexte et cette introduction à l'écart, 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 très 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 indé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 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 de choses à la fois, où vous ne pouvez pas être sûr dans quel ordre les choses se passent et où l'ordre compte:

  1. S'il y a une variable qui est modifiée (affectée) à deux ou plusieurs endroits différents, comment savoir 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 savoir si elle utilise l'ancienne ou la nouvelle valeur?

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

x = x++ + ++x;

il y a trois tentatives pour modifier `x.

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

y = x + x++;

nous utilisons tous les deux la valeur de xet la modifions.

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

5 alinsoar Oct 13 2017 at 20:58

Une bonne explication sur ce qui se passe dans ce type de calcul est fournie dans le document n1188 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 suivant, un objet doit voir sa valeur stockée 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.

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

Dans l'article 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 où i=i++il y a 2 points de séquence qui délimitent une expression complète. Une telle expression est syntaxiquement équivalente à une entrée de expression-statementsous 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 ces deux formes d'interprétation du 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. En changeant cet ordre, on peut parfois changer le résultat.

ÉDITER:

Une autre bonne source pour expliquer de telles ambiguïtés sont les entrées du site c-faq (également publié sous forme de livre ), à savoir ici et ici et ici .

3 MohamedEl-Nakib Jun 11 2017 at 05:56

La raison en est que le programme exécute un comportement indéfini. Le problème réside dans l'ordre d'évaluation, car il n'y a pas de points de séquence 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 désordonné.

  • Donc 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 dans un ordre 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 standards. (Oui, le logiciel équivalent en Java et C # a défini des comportements). Il évalue chaque sous-expression une par une dans l'instruction RHS dans un ordre de gauche à droite; pour chaque sous-expression: le ++ c (pré-incrémentation) est évalué en premier puis la valeur c est utilisée pour l'opération, puis le post-incrémentation 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 un comportement défini C ++ tel que le comprend GCC:

#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 passons à 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 fonctionne Visual Studio, il adopte une autre approche, il évalue toutes les expressions pré-incrémentées en première passe, puis utilise les valeurs de variables dans les opérations en deuxième passe, attribue de RHS à LHS en troisième passe, puis au dernier passage il évalue tous les expressions de post-incrémentation en un seul passage.

Donc, l'équivalent en comportement défini C ++ comme le comprend Visual C ++:

#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 l'indique la documentation de Visual Studio à la préséance et à l'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 les opérateurs Postfix.