Perché questi costrutti utilizzano un comportamento non definito pre e post-incremento?
#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
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 ).
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?)
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.
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
A
eB
, seA
è sequenziato primaB
, l'esecuzione diA
deve precedere l'esecuzione diB
.
Non sequenziato:
Se
A
non è sequenziato prima o dopoB
, alloraA
e nonB
sono 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
A
eB
implica che ogni calcolo del valore e l' effetto collaterale associato a essoA
sia sequenziato prima di ogni calcolo del valore e effetto collaterale associatoB
.
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 i
sono sequenziali l'uno rispetto all'altro. Ciò significa che non è in sequenza 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 è quello del caso di comportamento indefinito .
Rinomina la parte i
a sinistra dell'assegnazione il
e 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 1
e 2
che dipende dalla sequenza degli effetti collaterali per assegnazione ++
e quindi richiama UB.
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 ++i
saranno 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.5
paragrafo 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.4
come:
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.3
come:
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).
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 .
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 ++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 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 ++x
e y++
non è specificato. Ma è un'affermazione perfettamente legale e valida. Non c'è alcun comportamento indefinito in questa dichiarazione. Perché le modifiche ( ++x
e 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 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, 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++);
, ++i
con incrementi i
di 6
e i++
rese vecchio valore di i
( 6
), che viene assegnato a j
. Quindi i
diventa 7
dovuto 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.
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, i
verrebbe 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 i
venga utilizzata in entrambe le posizioni, ma se una routine accetta riferimenti a due puntatori p
e q
, e utilizza (*p)
e (*q)
nell'espressione sopra (invece di usare i
due volte) il compilatore non sarebbe necessario riconoscere o evitare lo stallo che si verificherebbe se l'indirizzo dell'oggetto stesso fosse passato per entrambi p
e q
.
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 :
- 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
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 ):
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 è:
- 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 -Wall
e -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à i
di uno, restituendo il vecchio valore, scartando quel valore; quindi all'operatore virgola, regolare gli effetti collaterali; e poi incrementa i
di 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 ++i
negli argomenti della funzione , e il valore di i
viene quindi modificato due volte, da entrambi i++
e ++i
, tra il punto di sequenza precedente e quello successivo.
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.
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 ++i
incrementi 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 ++i
eseguita, 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).
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 ++x
differisce 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 x
first, 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 ++x
allora 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 x
e restituire il vecchio valore di x
; (2) la ++x
parte cerca di aggiungere 1 ax, memorizzare il nuovo valore in x
e 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:
- Se c'è una variabile che viene modificata (assegnata a) in due o più posizioni diverse, come fai a sapere quale modifica avviene per prima?
- 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 x
e 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.
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-statement
nella 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 .
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.