爲何這些構造使用先後遞增的未定義行爲?

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

#1樓

儘管不太可能有任何編譯器和處理器實際執行此操做,可是在C標準下,對於編譯器而言,使用如下序列實現「 i ++」是合法的: c++

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

雖然我不認爲任何處理器都支持硬件來有效地完成這樣的事情,但人們能夠輕鬆想象這種行爲會使多線程代碼更容易的狀況(例如,若是兩個線程嘗試執行上述操做,則能夠保證這種狀況)序列同時, i將增長兩個),而且未來的處理器可能會提供相似的功能並非徹底不可想象的。 sass

若是編譯器按照上述指示編寫i++ (根據標準合法)並在整個表達式求值過程當中散佈以上指令(也是合法的),而且沒有注意到其餘指令之一碰巧訪問了i ,編譯器可能會(而且合法)生成一系列死鎖的指令。 能夠確定的是,在兩個地方都使用相同變量i的狀況下,可是若是例程接受對兩個指針pq引用,並使用(*p)(*q) ,則編譯器幾乎能夠檢測到問題。在上面的表達式中(而不是使用i兩次),不須要編譯器識別或避免若是爲pq傳遞了相同對象的地址時將發生死鎖。 多線程


#2樓

該行爲沒法真正解釋,由於它同時調用了未指定的行爲未定義的行爲 ,所以咱們沒法對此代碼作出任何通常性的預測,儘管若是您閱讀Olve Maudal的著做(例如Deep CUnspecified and Undefined),有時能夠在特定狀況下使用特定的編譯器和環境進行猜想,可是請不要在生產環境附近這樣作。 函數

所以,繼續進行未指定的行爲 ,在c99標準草案6.5節第3段中說( 強調個人 ): 優化

語法表示操做符和操做數的分組。74)除非稍後指定(對於函數調用(),&&,||,?:和逗號運算符), 子表達式的求值順序和哪一種反作用都未指定。 this

所以,當咱們有這樣的一行時: 編碼

i = i++ + ++i;

咱們不知道會先評估i++仍是++i 。 這主要是爲編譯器提供更好的優化選項spa

因爲程序在序列點之間屢次修改變量( iu等),所以咱們在這裏也具備未定義的行爲 。 從標準草案第6.5節第2段( 重點是個人 ): 線程

在上一個序列點與下一個序列點之間,對象的存儲值最多隻能經過對錶達式的求值來修改 。 此外, 在先值僅應被讀取以肯定要存儲的值翻譯

它引用瞭如下未定義的代碼示例:

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

在全部這些示例中,代碼都試圖在同一序列點中屢次修改對象,這些對象將以;結尾; 在如下每種狀況下:

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

未指定的行爲c99標準草案的 3.4.4節中定義爲:

使用未指定的值,或本國際標準提供兩種或兩種以上可能性且在任何狀況下均不對所選內容施加任何其餘要求的其餘行爲

未定義的行爲3.4.3節中定義爲:

在使用非便攜式或錯誤程序構造或錯誤數據時的行爲,對此國際標準不施加任何要求

並指出:

可能的不肯定行爲範圍從徹底忽略具備沒法預測結果的狀況到在翻譯或程序執行過程當中以環境特徵記錄的方式表現(帶有或不帶有診斷消息)到終止翻譯或執行(帶有發佈)診斷消息)。


#3樓

C標準說,一個變量最多隻能在兩個序列點之間分配一次。 例如,分號是一個序列點。
所以,每一個形式的語句:

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

以此類推。 該標準還規定行爲是不肯定的,不是不肯定的。 一些編譯器確實會檢測到這些併產生一些結果,但這不是每一個標準的結果。

可是,能夠在兩個序列點之間增長兩個不一樣的變量。

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

上面是複製/分析字符串時的常見編碼實踐。


#4樓

若是您很想知道代碼的確切程度,那麼只需編譯和反彙編您的代碼便可。

這是我在機器上獲得的,以及我認爲正在發生的事情:

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

(我...假設0x00000014指令是某種編譯器優化?)


#5樓

https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c中,有人問到如下語句:

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

它將打印7 ... OP但願它打印6。

不能保證++i增量在其他計算以前所有完成。 實際上,不一樣的編譯器在這裏會獲得不一樣的結果。 在您提供的示例中,首先執行2個++i ,而後讀取k[]的值,而後讀取最後一個++i ,而後讀取k[]

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

現代編譯器將對此進行很好的優化。 實際上,它可能比您最初編寫的代碼更好(假設它按照您但願的方式工做)。

相關文章
相關標籤/搜索