Perché questi costrutti utilizzano il comportamento indefinito pre- e post-incremento?

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

C ha il concetto di comportamento non definito, cioè alcuni costrutti linguistici sono sintatticamente validi ma non è ansible prevedere il comportamento quando viene eseguito il codice.

Per quanto ne so, lo standard non dice esplicitamente perché esiste il concetto di comportamento non definito. Nella mia mente, è semplicemente perché i progettisti linguistici volevano che ci fosse un margine nella semantica, invece di richiedere che tutte le implementazioni gestissero l’overflow dei numeri interi nello stesso identico modo, che molto probabilmente imporrebbe dei notevoli costi di performance, hanno appena lasciato il comportamento indefinito in modo tale che se si scrive codice che causa l’overflow dei numeri interi, può succedere di tutto.

Quindi, con questo in mente, perché questi “problemi” sono? Il linguaggio dice chiaramente che certe cose portano a comportamenti indefiniti . Non c’è nessun problema, non c’è “dovrebbe” essere coinvolto. Se il comportamento indefinito cambia quando una delle variabili coinvolte è dichiarata volatile , ciò non prova o modifica nulla. Non è definito ; non puoi ragionare sul comportamento.

Il tuo esempio più interessante, quello con

 u = (u++); 

è un esempio di libro di testo di comportamento indefinito (vedi la voce di Wikipedia sui punti di sequenza ).

Basta compilare e smontare la tua linea di codice, se sei così incline a sapere esattamente come ottieni ciò che ottieni.

Questo è quello che ottengo sulla mia macchina, insieme a quello che penso stia succedendo:

 $ 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 = ii = 1 j = 1 0x00000014 < +20>: add %eax,%eax // j += ji = 1 j = 2 0x00000016 < +22>: add %eax,-0x4(%ebp) // i += ji = 3 0x00000019 < +25>: addl $0x1,-0x4(%ebp) // i++ i = 4 0x0000001d < +29>: leave 0x0000001e < +30>: ret End of assembler dump. 

(Io … suppongo che l’istruzione 0x00000014 fosse una specie di ottimizzazione del compilatore?)

Penso che le parti rilevanti dello standard C99 siano 6.5 Expressions, §2

Tra il punto di sequenza precedente e quello successivo, un object deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un’espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare.

e 6.5.16 Operatori di assegnazione, §4:

L’ordine di valutazione degli operandi non è specificato. Se si tenta di modificare il risultato di un operatore di assegnazione o di accedervi dopo il successivo punto di sequenza, il comportamento non è definito.

Il comportamento non può essere spiegato in quanto invoca un comportamento non specificato e un comportamento indefinito , quindi non possiamo fare previsioni generali su questo codice, sebbene se si leggano i lavori di Olve Maudal come Deep C e Unspecified e Undefined a volte è ansible fare del bene indovina in casi molto specifici con un compilatore specifico e l’ambiente, ma per favore non farlo ovunque vicino alla produzione.

Passando quindi a un comportamento non specificato , nella bozza di standard C99, paragrafo 6.5 paragrafo 3, si afferma ( sottolineatura mia ):

Il raggruppamento di operatori e operandi è indicato dalla syntax.74) Tranne come specificato in seguito (per gli operatori function-call (), &&, ||,?:, E virgola), l’ordine di valutazione delle sottoespressioni e l’ordine in quali effetti collaterali hanno luogo sono entrambi non specificati.

Quindi, quando abbiamo una linea come questa:

 i = i++ + ++i; 

non sappiamo se i++ o ++i verranno valutati per primi. Questo è principalmente per dare al compilatore migliori opzioni per l’ottimizzazione .

Anche qui abbiamo un comportamento indefinito poiché il programma sta modificando le variabili ( i , u , ecc.) Più di una volta tra i punti di sequenza . Dalla bozza standard sezione 6.5 paragrafo 2 ( sottolineatura mia ):

Tra il punto di sequenza precedente e quello successivo, un object deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un’espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare .

cita i seguenti esempi di codice come non definiti:

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

In tutti questi esempi il codice sta tentando di modificare un object più di una volta nello stesso punto di sequenza, che terminerà con il ; in ciascuno di questi casi:

 i = i++ + ++i; ^ ^ ^ i = (i++); ^ ^ u = u++ + ++u; ^ ^ ^ u = (u++); ^ ^ v = v++ + ++v; ^ ^ ^ 

Il comportamento non specificato è definito nella bozza dello standard C99 nella sezione 3.4.4 come:

uso di un valore non specificato, o altro comportamento in cui questo standard internazionale offre due o più possibilità e non impone ulteriori requisiti su cui viene scelto in ogni caso

e il comportamento non definito è definito nella sezione 3.4.3 come:

comportamento, sull’uso di un costrutto di programma non portatile o errato o di dati errati, per i quali questo Standard internazionale non impone requisiti

e osserva che:

Il ansible comportamento indefinito va dall’ignorare completamente la situazione con risultati imprevedibili, a comportarsi durante la traduzione o l’esecuzione del programma in un modo documentato caratteristico dell’ambiente (con o senza emissione di un messaggio diagnostico), a terminare una traduzione o un’esecuzione (con l’emissione di un messaggio diagnostico).

La maggior parte delle risposte qui citate dallo standard C sottolinea che il comportamento di questi costrutti non è definito. Per capire perché il comportamento di questi costrutti non è definito , cerchiamo di capire prima questi termini alla luce dello standard C11:

Sequenziata: (5.1.2.3)

Date due valutazioni A e B , se A è sequenziato prima di B , allora l’esecuzione di A precederà l’esecuzione di B

non in sequenza:

Se A non è stato sequenziato prima o dopo B , allora A e B sono seguiti.

Le valutazioni possono essere una delle due cose:

  • calcoli del valore , che elaborano il risultato di un’espressione; e
  • effetti collaterali , che sono modifiche di oggetti.

Punto di sequenza:

La presenza di un punto di sequenza tra la valutazione delle espressioni A e B implica che ogni calcolo del valore ed effetto collaterale associato ad A è sequenziato prima di ogni calcolo del valore ed effetto collaterale associato a B

Ora venendo alla domanda, per le espressioni come

 int i = 1; i = i++; 

lo standard dice che:

6.5 Espressioni:

Se un effetto collaterale su un object scalare è ingiustificato rispetto a un effetto collaterale diverso sullo stesso object scalare o un calcolo del valore che utilizza il valore dello stesso object scalare, il comportamento non è definito . […]

Pertanto, l’espressione precedente invoca UB poiché due effetti collaterali sullo stesso object i sono in successione l’uno rispetto all’altro. Ciò significa che non viene sequenziato se l’effetto collaterale assegnato a i verrà eseguito prima o dopo l’effetto collaterale da ++ .
A seconda che l’assegnazione avvenga prima o dopo l’incremento, verranno prodotti risultati diversi e questo è il caso del comportamento non definito .

Permette di rinominare la i a sinistra di assignment e il diritto di assegnazione (nell’espressione i++ ) be ir , quindi l’espressione essere come

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

Un punto importante per quanto riguarda l’operatore Postfix ++ è che:

solo perché il ++ viene dopo che la variabile non significa che l’incremento avviene tardi . L’incremento può accadere fin da quando il compilatore apprezza fino a quando il compilatore assicura che venga utilizzato il valore originale .

Significa che l’espressione il = ir++ potrebbe essere valutata come

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

o

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

risultando in due diversi risultati 1 e 2 che dipendono dalla sequenza di effetti collaterali per assegnazione e ++ e quindi invoca UB.

Un altro modo di rispondere a questo, piuttosto che impantanarsi in dettagli arcani di punti di sequenza e comportamenti indefiniti, è semplicemente chiedere, cosa dovrebbero significare? Cosa stava cercando di fare il programmatore?

Il primo frammento chiesto, i = i++ + ++i , è abbastanza chiaramente folle nel mio libro. Nessuno lo scriverà mai in un programma reale, non è ovvio che cosa faccia, non ci sono algoritmi concepibili che qualcuno avrebbe potuto provare a codificare che avrebbe portato a questa particolare sequenza di operazioni forzate. E poiché non è ovvio per te e me cosa debba fare, va bene nel mio libro se il compilatore non riesce a capire cosa deve fare, neanche.

Il secondo frammento, i = i++ , è un po ‘più facile da capire. Qualcuno sta chiaramente cercando di incrementare i, e assegnare il risultato a i. Ma ci sono un paio di modi per farlo in C. Il modo più semplice per aggiungere 1 a i, e assegnare il risultato a I, è lo stesso in quasi tutti i linguaggi di programmazione:

 i = i + 1 

C, ovviamente, ha una scorciatoia pratica:

 i++ 

Ciò significa “aggiungi 1 a i e assegna il risultato a”. Quindi, se costruiamo un miscuglio dei due, scrivendo

 i = i++ 

quello che stiamo realmente dicendo è “aggiungi 1 a i, assegna il risultato a I e assegna il risultato a I”. Siamo confusi, quindi non mi preoccupa troppo se anche il compilatore si confonde.

Realisticamente, l’unica volta in cui queste espressioni pazze vengono scritte è quando le persone le usano come esempi artificiali di come si suppone che il ++ funzioni. E ovviamente è importante capire come funziona ++. Ma una regola pratica per usare ++ è, “Se non è ovvio che un’espressione che usa ++ significa, non scriverlo.”

Passavamo innumerevoli ore su comp.lang.c discutendo espressioni come queste e perché non sono definite. Due delle mie risposte più lunghe, che cercano di spiegare davvero perché, sono archiviate sul web:

  • Perché lo standard non definisce cosa fanno?
  • La precedenza dell’operatore non determina l’ordine di valutazione?

Mentre è improbabile che qualsiasi compilatore e processore lo farebbe in realtà, sarebbe legale, sotto lo standard C, che il compilatore implementasse “i ++” con la sequenza:

 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 

Anche se non penso che nessun processore supporti l’hardware per consentire una tale azione in modo efficiente, si può facilmente immaginare situazioni in cui tale comportamento renderebbe più semplice il codice multi-thread (ad esempio, garantirebbe che se due thread tentassero di eseguire il suddetto sequenza simultaneamente, i piacerebbe essere incrementata di due) e non è del tutto inconcepibile che qualche futuro processore possa fornire una caratteristica del genere.

Se il compilatore dovesse scrivere i++ come indicato sopra (legale secondo lo standard) e dovessero interspacciare le istruzioni di cui sopra per tutta la valutazione dell’espressione generale (anche legale), e se non è accaduto di notare che una delle altre istruzioni è successo per accedere a, sarebbe ansible (e legale) per il compilatore di generare una sequenza di istruzioni che sarebbe deadlock. Per essere sicuri, un compilatore potrebbe quasi certamente rilevare il problema nel caso in cui la stessa variabile i sia utilizzata in entrambi i posti, ma se una routine accetta riferimenti a due puntatori q , e utilizza (*p) e (*q) nell’espressione sopra (piuttosto che usare i due volte) il compilatore non sarebbe tenuto a riconoscere o evitare il deadlock che si verificherebbe se lo stesso indirizzo dell’object venisse passato per entrambi p e q .

Spesso questa domanda è collegata come un duplicato di domande relative al codice

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

o

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

o varianti simili.

Anche se questo è un comportamento indefinito come già detto, ci sono sottili differenze quando è coinvolto printf() quando si confronta con un’affermazione come:

  x = i++ + i++; 

Nella seguente dichiarazione:

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

l’ ordine di valutazione degli argomenti in printf() non è specificato . Ciò significa che le espressioni i++ e ++i potrebbero essere valutate in qualsiasi ordine. Lo standard C11 ha alcune descrizioni rilevanti su questo:

Allegato J, comportamenti non specificati

L’ordine in cui la funzione designatore, gli argomenti e le sottoespressioni all’interno degli argomenti vengono valutati in una chiamata di funzione (6.5.2.2).

3.4.4, comportamento non specificato

Uso di un valore non specificato o altro comportamento in cui questo Standard internazionale offre due o più possibilità e non impone ulteriori requisiti su cui viene scelto in qualsiasi caso.

ESEMPIO Un esempio di comportamento non specificato è l’ordine in cui vengono valutati gli argomenti di una funzione.

Il comportamento non specificato di per sé NON è un problema. Considera questo esempio:

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

Anche questo ha un comportamento non specificato perché l’ordine di valutazione di ++x e y++ non è specificato. Ma è una dichiarazione perfettamente legale e valida. Non c’è un comportamento indefinito in questa affermazione. Perché le modifiche ( ++x e y++ ) sono fatte per oggetti distinti .

Cosa rende la seguente dichiarazione

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

come comportamento indefinito è il fatto che queste due espressioni modificano lo stesso object i senza un punto di sequenza intermedio .


Un altro dettaglio è che la virgola coinvolta nella chiamata printf () è un separatore , non l’ operatore virgola .

Questa è una distinzione importante perché l’ operatore virgola introduce un punto di sequenza tra la valutazione dei loro operandi, che rende legale quanto segue:

 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=%dj=%d\n",i, j); // prints: i=7 j=6 

L’operatore virgola valuta i suoi operandi da sinistra a destra e restituisce solo il valore dell’ultimo operando. Quindi in j = (++i, i++); , ++i incrementa da i a 6 e i++ restituisce il vecchio valore di i ( 6 ) che è assegnato a j . Poi divento 7 causa di post-incremento.

Quindi se la virgola nella chiamata di funzione dovesse essere un operatore virgola allora

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

non sarà un problema Ma richiama il comportamento non definito perché la virgola qui è un separatore .


Per coloro che sono nuovi a un comportamento indefinito trarrebbe vantaggio dalla lettura di What Every C Programmer dovrebbe sapere sul comportamento indefinito per comprendere il concetto e molte altre varianti del comportamento non definito in C.

Questo post: anche il comportamento non definito, non specificato e definito dall’implementazione è rilevante.

Lo standard C dice che una variabile dovrebbe essere assegnata al massimo una volta tra due punti di sequenza. Ad esempio, un punto e virgola è un punto di sequenza.
Quindi ogni affermazione del modulo:

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

e così via viola questa regola. Lo standard dice anche che il comportamento non è definito e non è specificato. Alcuni compilatori li rilevano e producono alcuni risultati, ma questo non è per standard.

Tuttavia, due diverse variabili possono essere incrementate tra due punti di sequenza.

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

Quanto sopra è una pratica di codifica comune durante la copia / analisi delle stringhe.

Mentre la syntax delle espressioni come a = a++ o a++ + a++ è legale, il comportamento di questi costrutti è indefinito perché non si obbedisce ad uno standard in C. C99 6.5p2 :

  1. Tra il punto di sequenza precedente e quello successivo, un object deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un’espressione. [72] Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare [73]

Con nota in calce 73 chiarendo ulteriormente ciò

  1. Questo paragrafo rende espressioni di istruzioni non definite come

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

    mentre permettendo

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

I vari punti di sequenza sono elencati nell’allegato C di C11 (e C99 ):

  1. I seguenti sono i punti di sequenza descritti in 5.1.2.3:

    • Tra le valutazioni del designatore di funzioni e gli argomenti effettivi in ​​una chiamata di funzione e la chiamata effettiva. (6.5.2.2).
    • Tra le valutazioni del primo e del secondo operando dei seguenti operatori: AND logico && (6.5.13); OR logico || (6.5.14); comma, (6.5.17).
    • Tra le valutazioni del primo operando del condizionale? : viene valutato l’operatore e il secondo e il terzo operando (6.5.15).
    • La fine di un dichiarante completo: dichiarators (6.7.6);
    • Tra la valutazione di un’espressione completa e la successiva espressione completa da valutare. Le seguenti sono espressioni complete: un inizializzatore che non fa parte di un letterale composto (6.7.9); l’espressione in un’espressione (6.8.3); l’espressione di controllo di una dichiarazione di selezione (if o switch) (6.8.4); l’espressione di controllo di un while o do statement (6.8.5); ciascuna delle espressioni (facoltative) di una dichiarazione for (6.8.5.3); l’espressione (facoltativa) in una dichiarazione di ritorno (6.8.6.4).
    • Immediatamente prima che una funzione di libreria ritorni (7.1.4).
    • Dopo le azioni associate a ciascun identificatore di conversione della funzione di input / output formattato (7.21.6, 7.29.2).
    • Immediatamente prima e immediatamente dopo ogni chiamata a una funzione di confronto, e anche tra qualsiasi chiamata a una funzione di confronto e qualsiasi movimento degli oggetti passati come argomenti a quella chiamata (7.22.5).

La formulazione dello stesso paragrafo in C11 è:

  1. Se un effetto collaterale su un object scalare è ingiustificato rispetto a un effetto collaterale diverso sullo stesso object scalare o un calcolo del valore che utilizza il valore dello stesso object scalare, il comportamento non è definito. Se esistono più ordinamenti consentiti delle sottoespressioni di un’espressione, il comportamento non è definito se si verifica un tale effetto collaterale in qualsiasi ordine.84)

È ansible rilevare tali errori in un programma utilizzando, ad esempio, una versione recente di GCC con -Wall e -Werror , quindi GCC rifiuta categoricamente di compilare il programma. Quanto segue è l’output di 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 parte importante è sapere che cos’è un punto di sequenza – e qual è un punto di sequenza e cosa no . Ad esempio l’ operatore virgola è un punto di sequenza, quindi

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

è ben definito, e aumenterà di una unità il vecchio valore, scartando quel valore; poi all’operatore virgola, sistemare gli effetti collaterali; e poi incrementa di uno per uno, e il valore risultante diventa il valore dell’espressione – cioè questo è solo un modo forzato per scrivere j = (i += 2) che è ancora un modo “intelligente” per scrivere

 i += 2; j = i; 

Tuttavia, gli elenchi di argomenti in funzione non sono un operatore virgola e non vi è alcun punto di sequenza tra le valutazioni di argomenti distinti; invece sono indifferenti l’uno nei confronti dell’altro; quindi la funzione chiama

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

ha un comportamento indefinito perché non esiste un punto di sequenza tra le valutazioni di i++ e ++i negli argomenti di funzione , e il valore di i è quindi modificato due volte, sia da i++ che da ++i , tra il punto di sequenza precedente e quello successivo.

In https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c qualcuno ha chiesto una dichiarazione come:

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

che stampa 7 … l’OP si aspettava che stampasse 6.

Gli incrementi ++i non sono garantiti per tutti completi prima del resto dei calcoli. In effetti, diversi compilatori otterranno risultati diversi qui. Nell’esempio che hai fornito, i primi 2 ++i eseguiti, quindi sono stati letti i valori di k[] , quindi l’ultimo ++i quindi k[] .

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

I compilatori moderni lo ottimizzeranno molto bene. In effetti, forse meglio del codice che hai scritto in origine (supponendo che avesse funzionato nel modo in cui avevi sperato).

Una buona spiegazione di ciò che accade in questo tipo di calcolo è fornita nel documento n1188 dal sito ISO W14 .

Spiego le idee.

La regola principale dello standard ISO 9899 applicabile in questa situazione è 6.5p2.

Tra il punto di sequenza precedente e quello successivo, un object deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un’espressione. Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare.

I punti di sequenza in un’espressione come i=i++ sono prima di i= e dopo i++ .

Nel documento che ho citato sopra è spiegato che è ansible capire il programma come formato da piccole scatole, ciascuna scatola contenente le istruzioni tra 2 punti sequenza consecutivi. I punti di sequenza sono definiti nell’annesso C dello standard, nel caso di i=i++ ci sono 2 punti di sequenza che delimitano un’espressione completa. Tale espressione è sintatticamente equivalente a una voce di expression-statement nella forma di Backus-Naur della grammatica (una grammatica è fornita nell’allegato A dello standard).

Quindi l’ordine delle istruzioni all’interno di una scatola non ha un ordine chiaro.

 i=i++ 

può essere interpretato come

 tmp = i i=i+1 i = tmp 

o come

 tmp = i i = tmp i=i+1 

poiché entrambe queste forms interpretano il codice i=i++ sono valide e poiché entrambe generano risposte diverse, il comportamento non è definito.

Quindi un punto di sequenza può essere visto dall’inizio e dalla fine di ogni casella che compone il programma [le caselle sono unità atomiche in C] e all’interno di una scatola l’ordine delle istruzioni non è definito in tutti i casi. Cambiando quell’ordine si può a volte cambiare il risultato.

MODIFICARE:

Altre buone fonti per spiegare tali ambiguità sono le voci dal sito c-faq (anche pubblicate come un libro ), vale a dire qui, qui e qui .

Il motivo è che il programma sta eseguendo un comportamento indefinito. Il problema sta nell’ordine di valutazione, perché non sono richiesti punti di sequenza secondo lo standard C ++ 98 (nessuna operazione è sequenziata prima o dopo un’altra secondo la terminologia C ++ 11).

Tuttavia, se ci si attiene a un compilatore, si troverà il comportamento persistente, a condizione che non si aggiungano le chiamate di funzione oi puntatori, il che renderebbe il comportamento più disordinato.

  • Quindi prima il GCC: Usando Nuwen MinGW 15 GCC 7.1 otterrai:

     #include 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 

    }

Come funziona GCC? valuta le sotto espressioni nell’ordine da sinistra a destra per il lato destro (RHS), quindi assegna il valore a sinistra (LHS). Questo è esattamente il modo in cui Java e C # si comportano e definiscono i loro standard. (Sì, il software equivalente in Java e C # ha comportamenti definiti). Valuta ciascuna sotto espressione una per una nell’istruzione RHS in un ordine da sinistra a destra; for each sub expression: the ++c (pre-increment) is evaluated first then the value c is used for the operation, then the post increment c++).

according to GCC C++: Operators

In GCC C++, the precedence of the operators controls the order in which the individual operators are evaluated

the equivalent code in defined behavior C++ as GCC understands:

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

Then we go to Visual Studio . Visual Studio 2015, you get:

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

How does visual studio work, it takes another approach, it evaluates all pre-increments expressions in first pass, then uses variables values in the operations in second pass, assign from RHS to LHS in third pass, then at last pass it evaluates all the post-increment expressions in one pass.

So the equivalent in defined behavior C++ as Visual C++ understands:

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

as Visual Studio documentation states at Precedence and Order of Evaluation :

Where several operators appear together, they have equal precedence and are evaluated according to their associativity. The operators in the table are described in the sections beginning with Postfix Operators.

Your question was probably not, “Why are these constructs undefined behavior in C?”. Your question was probably, “Why did this code (using ++ ) not give me the value I expected?”, and someone marked your question as a duplicate, and sent you here.

This answer tries to answer that question: why did your code not give you the answer you expected, and how can you learn to recognize (and avoid) expressions that will not work as expected.

I assume you’ve heard the basic definition of C’s ++ and -- operators by now, and how the prefix form ++x differs from the postfix form x++ . But these operators are hard to think about, so to make sure you understood, perhaps you wrote a tiny little test program involving something like

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

But, to your surprise, this program did not help you understand — it printed some strange, unexpected, inexplicable output, suggesting that maybe ++ does something completely different, not at all what you thought it did.

Or, perhaps you’re looking at a hard-to-understand expression like

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

Perhaps someone gave you that code as a puzzle. This code also makes no sense, especially if you run it — and if you compile and run it under two different compilers, you’re likely to get two different answers! What’s up with that? Which answer is correct? (And the answer is that both of them are, or neither of them are.)

As you’ve heard by now, all of these expressions are undefined , which means that the C language makes no guarantee about what they’ll do. This is a strange and surprising result, because you probably thought that any program you could write, as long as it compiled and ran, would generate a unique, well-defined output. But in the case of undefined behavior, that’s not so.

What makes an expression undefined? Are expressions involving ++ and -- always undefined? Of course not: these are useful operators, and if you use them properly, they’re perfectly well-defined.

For the expressions we’re talking about what makes them undefined is when there’s too much going on at once, when we’re not sure what order things will happen in, but when the order matters to the result we get.

Let’s go back to the two examples I’ve used in this answer. When I wrote

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

the question is, before calling printf , does the compiler compute the value of x first, or x++ , or maybe ++x ? But it turns out we don’t know . There’s no rule in C which says that the arguments to a function get evaluated left-to-right, or right-to-left, or in some other order. So we can’t say whether the compiler will do x first, then ++x , then x++ , or x++ then ++x then x , or some other order. But the order clearly matters, because depending on which order the compiler uses, we’ll clearly get different results printed by printf .

What about this crazy expression?

 x = x++ + ++x; 

The problem with this expression is that it contains three different attempts to modify the value of x: (1) the x++ part tries to add 1 to x, store the new value in x , and return the old value of x ; (2) the ++x part tries to add 1 to x, store the new value in x , and return the new value of x ; and (3) the x = part tries to assign the sum of the other two back to x. Which of those three attempted assignments will “win”? Which of the three values will actually get assigned to x ? Again, and perhaps surprisingly, there’s no rule in C to tell us.

You might imagine that precedence or associativity or left-to-right evaluation tells you what order things happen in, but they do not. You may not believe me, but please take my word for it, and I’ll say it again: precedence and associativity do not determine every aspect of the evaluation order of an expression in C. In particular, if within one expression there are multiple different spots where we try to assign a new value to something like x , precedence and associativity do not tell us which of those attempts happens first, or last, or anything.


So with all that background and introduction out of the way, if you want to make sure that all your programs are well-defined, which expressions can you write, and which ones can you not write?

These expressions are all fine:

 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++; 

These expressions are all undefined:

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

And the last question is, how can you tell which expressions are well-defined, and which expressions are undefined?

As I said earlier, the undefined expressions are the ones where there’s too much going at once, where you can’t be sure what order things happen in, and where the order matters:

  1. If there’s one variable that’s getting modified (assigned to) in two or more different places, how do you know which modification happens first?
  2. If there’s a variable that’s getting modified in one place, and having its value used in another place, how do you know whether it uses the old value or the new value?

As an example of #1, in the expression

 x = x++ + ++x; 

there are three attempts to modify `x.

As an example of #2, in the expression

 y = x + x++; 

we both use the value of x , and modify it.

So that’s the answer: make sure that in any expression you write, each variable is modified at most once, and if a variable is modified, you don’t also attempt to use the value of that variable somewhere else.