Perché questi costrutti utilizzano un comportamento non definito pre e post-incremento?

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

Risposte

573 unwind Jun 04 2009 at 16:20

C ha il concetto di comportamento indefinito, cioè alcuni costrutti del linguaggio sono sintatticamente validi ma non è possibile prevedere il comportamento quando il codice viene eseguito.

Per quanto ne so, lo standard non dice esplicitamente perché esiste il concetto di comportamento indefinito. Nella mia mente, è semplicemente perché i progettisti del linguaggio volevano che ci fosse un margine di manovra nella semantica, invece di richiedere cioè che tutte le implementazioni gestissero l'overflow intero nello stesso identico modo, il che molto probabilmente imporrebbe seri costi di prestazione, hanno semplicemente lasciato il comportamento undefined in modo che se scrivi codice che causa un intero overflow, può succedere di tutto.

Quindi, con questo in mente, perché sono questi "problemi"? La lingua dice chiaramente che certe cose portano a comportamenti indefiniti . Non ci sono problemi, non ci sono "dovrebbero" coinvolti. Se il comportamento indefinito cambia quando viene dichiarata una delle variabili coinvolte volatile, ciò non prova né cambia nulla. Non è definito ; non puoi ragionare sul comportamento.

Il tuo esempio più interessante, quello con

u = (u++);

è un esempio da manuale di comportamento indefinito (vedere la voce di Wikipedia sui punti della sequenza ).

76 badp May 24 2010 at 20:26

Compila e disassembla la tua riga di codice, se sei così incline a sapere come esattamente ottieni quello che stai ricevendo.

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

(Suppongo ... suppongo che l'istruzione 0x00000014 fosse una sorta di ottimizzazione del compilatore?)

66 Christoph Jun 04 2009 at 16:35

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

Tra il punto della sequenza precedente e quello successivo, un oggetto 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 della sequenza, il comportamento non è definito.

61 haccks Jun 27 2015 at 07:27

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 , comprendiamo prima questi termini alla luce dello standard C11:

Sequenziato: (5.1.2.3)

Date due valutazioni qualsiasi Ae B, se Aè sequenziato prima B, l'esecuzione di Adeve precedere l'esecuzione di B.

Non sequenziato:

Se Anon è sequenziato prima o dopo B, allora Ae non Bsono sequenziati.

Le valutazioni possono essere una delle due cose:

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

Punto sequenza:

La presenza di un punto di sequenza tra la valutazione delle espressioni Ae Bimplica che ogni calcolo del valore e l' effetto collaterale associato a esso Asia sequenziato prima di ogni calcolo del valore e effetto collaterale associato B.

Venendo ora alla domanda, per le espressioni come

int i = 1;
i = i++;

lo standard dice che:

6.5 Espressioni:

Se un effetto collaterale su un oggetto scalare è non in sequenza rispetto ad entrambi un diverso effetto collaterale sullo stesso oggetto scalare o un calcolo valore utilizzando il valore dello stesso oggetto scalare, è definito il comportamento . [...]

Pertanto, l'espressione precedente richiama UB perché due effetti collaterali sullo stesso oggetto non isono sequenziali l'uno rispetto all'altro. Ciò significa che non è in sequenza se l'effetto collaterale assegnato a iverrà 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 è quello del caso di comportamento indefinito .

Rinomina la parte ia sinistra dell'assegnazione ile quella a destra dell'assegnazione (nell'espressione i++) ir, quindi l'espressione è 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é ++viene dopo la variabile non significa che l'incremento avvenga in ritardo . L'incremento può avvenire non appena il compilatore desidera, purché il compilatore si assicuri che venga utilizzato il valore originale .

Significa che l'espressione il = ir++può 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  

risultante in due risultati diversi 1e 2che dipende dalla sequenza degli effetti collaterali per assegnazione ++e quindi richiama UB.

54 ShafikYaghmour Aug 16 2013 at 02:25

Il comportamento non può davvero essere spiegato perché invoca sia un comportamento non specificato e comportamento non definito , quindi non possiamo fare previsioni generali su questo codice, anche se leggete di Olve Maudal lavoro, come profonda C e non specificato e non definita a volte si può fare un buon ipotizza in casi molto specifici con un compilatore e un ambiente specifici, ma per favore non farlo da nessuna parte vicino alla produzione.

Quindi, passando al comportamento non specificato , nella bozza della sezione standard c99 il6.5 paragrafo 3 dice ( enfasi mia ):

Il raggruppamento di operatori e operandi è indicato dalla sintassi.74) Ad eccezione di quanto specificato in seguito (per gli operatori di chiamata di funzione (), &&, ||,?: E virgola), l'ordine di valutazione delle sottoespressioni e l'ordine in quali effetti collaterali si verificano non sono specificati.

Quindi, quando abbiamo una riga come questa:

i = i++ + ++i;

noi non sappiamo se i++o ++isaranno valutate prima. Questo è principalmente per fornire al compilatore migliori opzioni per l'ottimizzazione .

Abbiamo anche un comportamento indefinito anche qui in quanto il programma sta modificando le variabili ( i, u, ecc ..) più di una volta tra i punti di sequenza . Dalla bozza della sezione standard 6.5paragrafo 2 (il corsivo è mio ):

Tra il punto della sequenza precedente e quello successivo, un oggetto 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 oggetto più di una volta nello stesso punto della 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.4come:

uso di un valore non specificato o altro comportamento in cui la presente norma internazionale fornisce due o più possibilità e non impone ulteriori requisiti su cui è scelto in qualsiasi caso

e il comportamento indefinito è definito nella sezione 3.4.3come:

comportamento, in caso di utilizzo di un costrutto di programma non portabile o errato o di dati errati, per i quali la presente norma internazionale non impone requisiti

e osserva che:

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

38 SteveSummit Jun 18 2015 at 18:55

Un altro modo per rispondere a questa domanda, 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 di cui è stato chiesto i = i++ + ++i, è abbastanza chiaramente folle nel mio libro. Nessuno lo scriverebbe mai in un programma reale, non è ovvio cosa fa, non esiste un algoritmo concepibile che qualcuno avrebbe potuto provare a codificare che avrebbe portato a questa particolare sequenza di operazioni artificiose. E poiché non è ovvio per te e per me cosa dovrebbe fare, nel mio libro va bene se il compilatore non riesce a capire cosa dovrebbe fare.

Il secondo frammento,, i = i++è un po 'più facile da capire. Qualcuno sta chiaramente cercando di incrementare i e di 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 comoda scorciatoia:

i++

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

i = i++

quello che stiamo veramente dicendo è "aggiungi 1 a i, e assegna il risultato a i, e assegna il risultato a i". Siamo confusi, quindi non mi disturba troppo se anche il compilatore si confonde.

Realisticamente, l'unica volta che queste folli espressioni vengono scritte è quando le persone le usano come esempi artificiali di come dovrebbe funzionare ++. E ovviamente è importante capire come funziona ++. Ma una regola pratica per usare ++ è: "Se non è ovvio cosa significhi un'espressione che usa ++, non scriverla".

Trascorrevamo innumerevoli ore su comp.lang.c discutendo di espressioni come queste e del motivo per cui sono indefinite. 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 degli operatori non determina l'ordine di valutazione?

Vedi anche mettere in discussione 3.8 e il resto delle domande nella sezione 3 della lista C FAQ .

27 P.P Dec 31 2015 at 03:26

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

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

o

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

o varianti simili.

Sebbene questo sia anche un comportamento indefinito come già affermato, ci sono sottili differenze quando printf()è coinvolto 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 nonprintf() è specificato . Ciò significa che le espressioni i++e ++ipotrebbero essere valutate in qualsiasi ordine. Lo standard C11 ha alcune descrizioni rilevanti su questo:

Allegato J, comportamenti non specificati

L'ordine in cui il designatore di funzione, 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 la presente norma internazionale fornisce due o più possibilità e non impone ulteriori requisiti su cui è scelto in ogni caso.

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

Il comportamento non specificato in 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 ++xe y++non è specificato. Ma è un'affermazione perfettamente legale e valida. Non c'è alcun comportamento indefinito in questa dichiarazione. Perché le modifiche ( ++xe y++) vengono apportate a 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 oggetto isenza 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, il 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=%d j=%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++);, ++icon incrementi idi 6e i++rese vecchio valore di i( 6), che viene assegnato a j. Quindi idiventa 7dovuto al 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 un comportamento indefinito perché la virgola qui è un separatore .


Coloro che sono nuovi al comportamento indefinito trarrebbero beneficio dalla lettura di Ciò che ogni programmatore C dovrebbe sapere sul comportamento indefinito per comprendere il concetto e molte altre varianti di comportamento indefinito in C.

Questo post: Anche il comportamento non definito, non specificato e definito dall'implementazione è rilevante.

23 supercat Dec 06 2012 at 01:30

Sebbene sia improbabile che compilatori e processori lo facciano effettivamente, sarebbe legale, secondo 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

Sebbene non penso che nessun processore supporti l'hardware per consentire che una cosa del genere venga eseguita in modo efficiente, si possono facilmente immaginare situazioni in cui tale comportamento renderebbe più semplice il codice multi-thread (ad esempio, garantirebbe che se due thread provassero a eseguire quanto sopra sequenza simultanea, iverrebbe incrementata di due) e non è del tutto inconcepibile che qualche futuro processore possa fornire una funzionalità qualcosa del genere.

Se il compilatore dovesse scrivere i++come sopra indicato (legale secondo lo standard) e intervallasse le istruzioni di cui sopra durante la valutazione dell'espressione complessiva (anche legale), e se non si accorgesse che una delle altre istruzioni è avvenuta per accedere i, sarebbe possibile (e legale) per il compilatore generare una sequenza di istruzioni che andrebbe in deadlock. A dire il vero, un compilatore rileverebbe quasi certamente il problema nel caso in cui la stessa variabile ivenga utilizzata in entrambe le posizioni, ma se una routine accetta riferimenti a due puntatori pe q, e utilizza (*p)e (*q)nell'espressione sopra (invece di usare idue volte) il compilatore non sarebbe necessario riconoscere o evitare lo stallo che si verificherebbe se l'indirizzo dell'oggetto stesso fosse passato per entrambi pe q.

18 AnttiHaapala Mar 26 2017 at 21:58

Sebbene la sintassi delle espressioni come a = a++o a++ + a++sia legale, il comportamento di questi costrutti è indefinito perché un must nello standard C non è obbedito. C99 6.5p2 :

  1. Tra il punto della sequenza precedente e quello successivo, un oggetto 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 la nota 73 che lo chiarisce ulteriormente

  1. Questo paragrafo rende espressioni di istruzioni indefinite come

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

    pur 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 della sequenza descritti in 5.1.2.3:

    • Tra le valutazioni del designatore di funzione 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); virgola, (6.5.17).
    • Tra le valutazioni del primo operando del condizionale? : operatore e il secondo e il terzo operando valutato (6.5.15).
    • La fine di un dichiaratore completo: dichiaratori (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 una dichiarazione di espressione (6.8.3); l'espressione di controllo di un'istruzione di selezione (if o switch) (6.8.4); l'espressione di controllo di un'istruzione while o do (6.8.5); ciascuna delle espressioni (facoltative) di un'istruzione for (6.8.5.3); l'espressione (facoltativa) in un'istruzione return (6.8.6.4).
    • Immediatamente prima che una funzione di libreria ritorni (7.1.4).
    • Dopo le azioni associate a ogni specificatore 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 oggetto scalare non viene sequenziato rispetto a un effetto collaterale diverso sullo stesso oggetto scalare o un calcolo di valore utilizzando il valore dello stesso oggetto scalare, il comportamento non è definito. Se ci sono più ordinamenti consentiti delle sottoespressioni di un'espressione, il comportamento è indefinito se tale effetto collaterale non sequenziale si verifica in uno qualsiasi degli ordinamenti.84)

È possibile rilevare tali errori in un programma, ad esempio utilizzando una versione recente di GCC con -Walle -Werror, quindi GCC si rifiuterà completamente di compilare il programma. Il seguente è 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 cos'è un punto sequenza - e cos'è un punto sequenza e cosa no . Ad esempio, l' operatore virgola è un punto della sequenza, quindi

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

è ben definito e aumenterà idi uno, restituendo il vecchio valore, scartando quel valore; quindi all'operatore virgola, regolare gli effetti collaterali; e poi incrementa idi uno, e il valore risultante diventa il valore dell'espressione - cioè questo è solo un modo artificioso di scrivere j = (i += 2)che è ancora una volta un modo "intelligente" di scrivere

i += 2;
j = i;

Tuttavia, gli ,elenchi di argomenti della funzione in non sono un operatore virgola e non vi è alcun punto di sequenza tra le valutazioni di argomenti distinti; invece le loro valutazioni non sono influenzate l'una dall'altra; quindi la chiamata di funzione

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

ha un comportamento indefinito perché non c'è un punto di sequenza tra le valutazioni di i++e ++inegli argomenti della funzione , e il valore di iviene quindi modificato due volte, da entrambi i++e ++i, tra il punto di sequenza precedente e quello successivo.

14 NikhilVidhani Sep 11 2014 at 19:36

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

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

e così via violano quella regola. Lo standard dice anche che il comportamento non è definito e non è specificato. Alcuni compilatori li rilevano e producono qualche risultato, ma questo non è per standard.

Tuttavia, è possibile incrementare due diverse variabili tra due punti della sequenza.

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

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

11 TomOnTime Apr 08 2015 at 10:20

Nel 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 ++iincrementi non sono garantiti per tutti completi prima che il resto dei calcoli. In effetti, compilatori diversi qui otterranno risultati diversi. Nell'esempio che hai fornito, il primo 2 ++ieseguita, quindi i valori di k[]sono stati letti, poi l'ultimo ++i, allora k[].

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

I compilatori moderni lo ottimizzeranno molto bene. In effetti, forse migliore del codice che hai scritto originariamente (supponendo che abbia funzionato come speravi).

6 SteveSummit Aug 16 2018 at 18:54

La tua domanda probabilmente non era: "Perché questi costrutti hanno un comportamento indefinito in C?". La tua domanda era probabilmente: "Perché questo codice (utilizzando ++) non mi ha dato il valore che mi aspettavo?", E qualcuno ha contrassegnato la tua domanda come duplicato e ti ha inviato qui.

Questa risposta cerca di rispondere a questa domanda: perché il tuo codice non ti ha dato la risposta che ti aspettavi e come puoi imparare a riconoscere (ed evitare) espressioni che non funzioneranno come previsto.

Presumo che tu abbia già sentito la definizione di base di C ++e --operatori e come la forma del prefisso ++xdifferisce dalla forma del suffisso x++. Ma questi operatori sono difficili da pensare, quindi per essere sicuro di aver capito, forse hai scritto un minuscolo programma di test che coinvolge qualcosa di simile

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

Ma, con tua sorpresa, questo programma non ti ha aiutato a capire: ha stampato un output strano, inaspettato, inspiegabile, suggerendo che forse ++fa qualcosa di completamente diverso, per niente quello che pensavi che facesse.

O forse stai guardando un'espressione difficile da capire come

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

Forse qualcuno ti ha dato quel codice come un puzzle. Anche questo codice non ha senso, specialmente se lo esegui e se lo compili ed esegui con due compilatori diversi, probabilmente otterrai due risposte diverse! Cosa succede con quello? Quale risposta è corretta? (E la risposta è che entrambi lo sono o nessuno dei due lo è.)

Come hai sentito ormai, tutte queste espressioni sono indefinite , il che significa che il linguaggio C non garantisce ciò che faranno. Questo è un risultato strano e sorprendente, perché probabilmente pensavi che qualsiasi programma che potresti scrivere, purché compilato ed eseguito, genererebbe un output unico e ben definito. Ma nel caso di un comportamento indefinito, non è così.

Cosa rende un'espressione indefinita? Le espressioni sono coinvolgenti ++e --sempre indefinite? Certo che no: si tratta di operatori utili e, se usati correttamente, sono perfettamente definiti.

Per le espressioni di cui parliamo, ciò che le rende indefinite è quando c'è troppo da fare in una volta, quando non siamo sicuri in quale ordine succederanno le cose, ma quando l'ordine è importante per il risultato che otteniamo.

Torniamo ai due esempi che ho usato in questa risposta. Quando ho scritto

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

la domanda è, prima di chiamare printf, il compilatore calcola il valore di xfirst, o x++, o forse ++x? Ma si scopre che non lo sappiamo . Non esiste una regola in C che dica che gli argomenti di una funzione vengono valutati da sinistra a destra, da destra a sinistra o in un altro ordine. Quindi non possiamo dire se il compilatore farà x, poi ++x, quindi x++, o x++allora ++xallora x, o qualche altro ordine. Ma l'ordine è chiaramente importante, perché a seconda dell'ordine utilizzato dal compilatore, otterremo chiaramente risultati diversi stampati printf.

Che mi dici di questa espressione folle?

x = x++ + ++x;

Il problema con questa espressione è che contiene tre diversi tentativi di modificare il valore di x: (1) la x++parte cerca di aggiungere 1 a x, memorizzare il nuovo valore in xe restituire il vecchio valore di x; (2) la ++xparte cerca di aggiungere 1 ax, memorizzare il nuovo valore in xe restituire il nuovo valore di x; e (3) la x =parte cerca di riassegnare la somma delle altre due a x. Quale di questi tre tentativi di assegnazione "vincerà"? A quale dei tre valori verrà effettivamente assegnato x? Ancora una volta, e forse sorprendentemente, non ci sono regole in C da dirci.

Potresti immaginare che la precedenza o l'associatività o la valutazione da sinistra a destra ti dica in quale ordine avvengono le cose, ma non lo fanno. Potresti non credermi, ma ti prego di credermi sulla parola e lo ripeto: la precedenza e l'associatività non determinano ogni aspetto dell'ordine di valutazione di un'espressione in C. In particolare, se all'interno di un'espressione ci sono più punti diversi in cui proviamo ad assegnare un nuovo valore a qualcosa come x, precedenza e associatività non ci dicono quale di quei tentativi avviene per primo, o per ultimo, o qualcos'altro.


Quindi, con tutto quel background e l'introduzione fuori mano, se vuoi assicurarti che tutti i tuoi programmi siano ben definiti, quali espressioni puoi scrivere e quali non puoi scrivere?

Queste espressioni vanno bene:

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

Queste espressioni sono tutte indefinite:

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

E l'ultima domanda è: come puoi sapere quali espressioni sono ben definite e quali sono indefinite?

Come ho detto prima, le espressioni indefinite sono quelle in cui c'è troppo da fare in una volta, in cui non puoi essere sicuro in quale ordine avvengono le cose e dove l'ordine è importante:

  1. Se c'è una variabile che viene modificata (assegnata a) in due o più posizioni diverse, come fai a sapere quale modifica avviene per prima?
  2. Se c'è una variabile che viene modificata in un punto e il suo valore viene utilizzato in un altro, come fai a sapere se utilizza il vecchio valore o il nuovo valore?

Come esempio di # 1, nell'espressione

x = x++ + ++x;

ci sono tre tentativi per modificare `x.

Come esempio di # 2, nell'espressione

y = x + x++;

entrambi usiamo il valore di xe lo modifichiamo.

Quindi questa è la risposta: assicurati che in qualsiasi espressione che scrivi, ogni variabile venga modificata al massimo una volta, e se una variabile viene modificata, non provi anche a usare il valore di quella variabile da qualche altra parte.

5 alinsoar Oct 13 2017 at 20:58

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 che si applica in questa situazione è 6.5p2.

Tra il punto della sequenza precedente e quello successivo, un oggetto 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 i=e dopo i++.

Nel documento che ho citato sopra è spiegato che puoi immaginare il programma come formato da piccole scatole, ciascuna scatola contenente le istruzioni tra 2 punti di sequenza consecutivi. I punti di sequenza sono definiti nell'allegato C della norma, nel caso in cui i=i++ci siano 2 punti di sequenza che delimitano un'espressione completa. Tale espressione è sintatticamente equivalente a una voce di expression-statementnella forma 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

perché entrambe tutte queste forme per interpretare il codice i=i++sono valide e poiché entrambe generano risposte diverse, il comportamento è indefinito.

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

MODIFICARE:

Un'altra buona fonte per spiegare tali ambiguità sono le voci dal sito c-faq (pubblicato anche come libro ), ovvero qui e qui e qui .

3 MohamedEl-Nakib Jun 11 2017 at 05:56

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

Tuttavia, se ti attieni a un compilatore, troverai il comportamento persistente, a condizione che non aggiungi chiamate di funzione o puntatori, il che renderebbe il comportamento più disordinato.

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

    #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
    

    }

Come funziona GCC? valuta le sottoespressioni in un ordine da sinistra a destra per il lato destro (RHS), quindi assegna il valore al lato sinistro (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 ogni sottoespressione una per una nella dichiarazione RHS in un ordine da sinistra a destra; per ogni sottoespressione: prima viene valutato il ++ c (pre-incremento), quindi viene utilizzato il valore c per l'operazione, quindi il post incremento c ++).

secondo GCC C ++: Operators

In GCC C ++, la precedenza degli operatori controlla l'ordine in cui vengono valutati i singoli operatori

il codice equivalente nel comportamento definito C ++ come interpreta 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
}

Quindi andiamo a Visual Studio . Visual Studio 2015, ottieni:

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

Come funziona lo studio visivo, richiede un altro approccio, valuta tutte le espressioni pre-incrementi nel primo passaggio, quindi utilizza i valori delle variabili nelle operazioni nel secondo passaggio, assegna da RHS a LHS nel terzo passaggio, quindi alla fine valuta tutti i espressioni post-incremento in un solo passaggio.

Quindi l'equivalente nel comportamento definito C ++ come comprende 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 
}

come afferma la documentazione di Visual Studio in Precedence and Order of Evaluation :

Quando più operatori compaiono insieme, hanno uguale precedenza e vengono valutati in base alla loro associatività. Gli operatori nella tabella sono descritti nelle sezioni che iniziano con Operatori Postfix.