2008-09-19 105 views
15

可能重複:
Pre & post increment operator behavior in C, C++, Java, & C#後增量運算符的行爲

下面是測試情況:


void foo(int i, int j) 
{ 
    printf("%d %d", i, j); 
} 
... 
test = 0; 
foo(test++, test); 

我希望得到一個 「0 1」 輸出,但我得到「0 0」 什麼給?

+0

也許你的描述/例子應該完全包含標題問題? – 2008-09-19 00:18:52

+0

標題和代碼示例不匹配 – itj 2008-09-19 08:02:24

+0

問題在標題和代碼示例之間混淆。 title has ++ n例如test ++ – itj 2008-09-19 08:05:16

回答

47

這是未指定行爲的示例。標準確定而不是說明應評估哪些順序參數。這是一個編譯器實現決策。編譯器可以按任意順序自由評估函數的參數。

在這種情況下,它看起來像實際上從右到左處理參數,而不是預期的從左到右。

一般來說,在參數中做副作用是不好的編程習慣。

代替FOO(試驗++,測試);你應該寫foo(test,test + 1);試驗++;

這將是語義上等同於你正在試圖完成什麼。

編輯: 正如Anthony正確指出的那樣,讀取和修改單個變量時沒有介入序列點是未定義的。所以在這種情況下,行爲確實是undefined。所以編譯器可以自由生成任何想要的代碼。

+0

作爲額外的重點,爲了避免這些問題,我總是將增量作爲單獨的語句。 – 2008-09-19 01:17:18

1

編譯器可能不會按照您期望的順序評估參數。

14

我剛纔說的一切都是錯的!計算副作用的時間點未指定爲。如果test是一個局部變量,Visual C++會在調用foo()之後執行增量,但如果test被聲明爲靜態或全局,它將在調用foo()之前增加併產生不同的結果,儘管最終值測試將是正確的。

在調用foo()之後,增量應該在單獨的語句中完成。即使行爲是在C/C++標準中指定的,也會令人困惑。你會認爲C++編譯器會將其標記爲潛在的錯誤。

Here是序列點,不確定的行爲一個很好的說明。

< ----錯錯錯START ---->

的 「++」 「++試驗」 的位在呼叫之後被執行到foo。所以,你在(0,0)傳遞給富,不是(1,0)

下面是從Visual Studio 2002的彙編輸出:

mov ecx, DWORD PTR _i$[ebp] 
push ecx 
mov edx, DWORD PTR tv66[ebp] 
push edx 
call _foo 
add esp, 8 
mov eax, DWORD PTR _i$[ebp] 
add eax, 1 
mov DWORD PTR _i$[ebp], eax 

增量完成後調用給foo() 。儘管這種行爲是有目的的,但對於偶然的讀者來說,這當然是令人困惑的,應該避免。增量真的應該在一個單獨的語句來完成通話後給foo()

< ----錯錯就錯---->

1

評價爲參數的順序給函數的一端未定義。在這種情況下,它似乎是從右到左做的。

(修改序列點之間的變量基本上允許編譯器做任何事情就是了。)

2

C沒有保證的參數計算順序函數調用,所以這個你可能會得到的結果「 0 1「或」0 0「。順序可以從編譯器更改爲編譯器,同一編譯器可以根據優化參數選擇不同的順序。

寫foo(test,test + 1)然後在下一行做++測試會更安全。無論如何,編譯器應儘可能優化它。

6

這是「不確定的行爲」,但在與C調用棧指定它幾乎總是這樣的做法保證你會看到它爲0,0,永不1,0

正如有人指出的那樣,彙編程序通過VC輸出將堆棧中最右側的參數先推入。這是C函數調用在彙編器中的實現方式。這是爲了適應C的「無盡參數列表」功能。通過按從右到左的順序推送參數,第一個參數保證是堆棧中的頂層項目。

採取的printf的簽名:

int printf(const char *format, ...); 

這些省略號表示未知數量的參數。如果參數是從左到右推動的,那麼格式將位於我們不知道大小的堆棧的底部。

知道在C(和C++)中,參數是從左到右處理的,我們可以確定解析和解釋函數調用的最簡單方法。到達參數列表的末尾,並開始推送,隨時評估任何複雜的語句。

但是,即使這樣也無法爲您節省,因爲大多數C編譯器都可以選擇分析函數「Pascal風格」。而這一切意味着函數參數以從左到右的方式壓入堆棧。例如,如果printf是用Pascal選項編譯的,那麼輸出很可能是1,0(但是,因爲printf使用橢圓,我不認爲它可以編譯爲Pascal樣式)。

29

這不只是未指定行爲,它實際上是未定義的行爲

是,參數計算順序是未指定的,但它是未定義到讀取和修改的單個可變而沒有插入序列中的點,除非讀出僅是爲了計算新的值的目的。在函數參數的評估之間沒有順序點,所以f(test,test++)未定義的行爲test正在讀取一個參數併爲另一個參數進行了修改。如果您將改造成一個功能,那麼你的罰款:

int preincrement(int* p) 
{ 
    return ++(*p); 
} 

int test; 
printf("%d %d\n",preincrement(&test),test); 

這是因爲,在入口和出口preincrement序列點,所以必須調用之前或簡單的讀取後進行評估。現在訂單只是未指定

還要注意的是逗號操作提供了一個序列點,所以

int dummy; 
dummy=test++,test; 

是好的---讀之前的增量情況發生,所以dummy被設置爲新值。

1

嗯,現在的OP已經被編輯過的一致性,它不與答案不同步。關於評估順序的基本答案是正確的。但是,foo(++ test,test)的具體可能值是不同的;案件。

++ test 在傳遞之前遞增,因此第一個參數將始終爲1.第二個參數將爲0或1,具體取決於評估順序。

1

根據C標準,在單個序列點中有多個對一個變量的引用是不確定的行爲(這裏您可以將其視爲一個語句或函數的參數),其中一個或多個這些參考文獻包括前/後修改。 So: foo(f ++,f)< - 未定義f增量。 同樣(我在用戶代碼中始終看到這一點): * p = p ++ + p;

通常,編譯器將不會改變其行爲的這種類型的事情(除了重大修改)。

通過打開警告並關注它們來避免它。

1

要重複別人怎麼說,這是不明確的行爲,而是不確定的。這個程序可以合法地輸出任何東西或任何東西,將n留在任何值,或發送侮辱性的電子郵件給你的老闆。

作爲一個實踐問題,編譯器編寫者通常只會做他們最容易編寫的東西,這通常意味着程序會讀取一次或兩次,調用函數並在某個時間遞增。根據標準,這與其他任何可想像的行爲一樣好。沒有理由期望編譯器,版本或不同編譯器選項之間的行爲相同。沒有理由爲什麼同一個程序中的兩個不同但相似的例子必須一致編譯,儘管這是我敢打賭的方式。

總之,不要這樣做。如果你很好奇,在不同情況下測試它,但不要假裝有一個正確或甚至可預測的結果。