在同一句語句里調用自增/自減後的變數本來就是未定義行為。


Order of evaluation of the operands of almost all C++ operators (including the order of evaluation of function arguments in a function-call expression and the order of evaluation of the subexpressions within any expression) is unspecified. The compiler can evaluate operands in any order, and may choose another order when the same expression is evaluated again.

Order of evaluation?

en.cppreference.com

從C++17,這種寫法不再是undefined behavior,而是unspecified behavior


不同編譯器的實現不同,因為C語言標準沒有對這種寫法做出規範,就是俗稱的未定義行為。

不要糾結為什麼,沒有任何意義。


貌似GCC/G++ 7.3和VS 2017一樣。

我試著答一波吧,上學時不求甚解就會留下這種問題,雖然實際操作中很少碰到。

這個問題水很深,涉及到調用約定和指令重排,所以出現這種未定義行為也是正常的。

首先講調用約定。調用約定是函數調用時調用者和被調用者關於如何傳參如何返回值和調用棧維護相關的約定。由於參數傳遞時求值順序的影響,會造成這些自增自減運算符運算順序的不同,進而導致printf傳進去的參數值不同。(忽然想到求值順序和傳參順序可以不同,但沒有不同的必要,所以算是弱相關吧。)

然後是指令重排。這是編譯器做的一種代碼優化,在單線程不改變運算結果的前提下,編譯器會根據代碼附近的內存使用情況做內存讀寫代碼順序的調整。顯然這種調整僅僅保證是語句與語句之間結果的一致性,而不考慮這種僅僅涉及到一條語句之內的一致性。於是就會出現這種無論從左往右還是從右往左都無法解釋的結果——但這顯然都是從右往左計算的,因為最後一個參數的數字都是正確的。

不知道微軟的產品怎麼樣,GCC/C++ 7.3把"int j = 3"改為"volatile int j = 3"以後,就完全符合從右往左計算參數的結果,因為這個關鍵詞的存在導致編譯器不可以隨意修改這個變數讀寫之間的順序。


我的感覺應該和參數壓棧順序沒有關係。vc 6.0和vs2017的不同是因為編譯器實現過程中的歸約順序不同有關。要理解編譯原理中的歸約概念說來話長。先佔個位子回頭再展開吧。


我來更新了。

首先需要明白的是編譯器將源代碼生成目標代碼需要對源代碼進行詞法分析和語法分析。詞法分析的目的是將源代碼轉換成一個詞列表。語法分析尋找合適詞序列將其逐層合併成一個句子。合併的過程我們稱為規約。在規約的同時輸出目標代碼。這裡我用題主的例子說明一下。對於這句話

printf("%d,%d,%d",--j,++j,j++);

詞法分析後得到的此列表為

printf, (, "%d,%d,%d", ,, -, -, j, ,, +, +, j, ,, j, +, +, ), ;

然後規約順序是先從右往左規約四個參數,然後規約成參數列表,然後規約成函數調用。

  1. j++ ==&> exp
  2. ++j ==&> exp
  3. --j ==&> exp
  4. "%d,%d,%d" ==&>exp
  5. "%d,%d,%d",--j,++j,j++ ==&>argument list
  6. printf("%d,%d,%d",--j,++j,j++); ==&>function

規約1 會輸出偽代碼如下,並且返回變數t

t = j
j += 1

規約2 會輸出代碼如下,並且返回變數j

j += 1

規約3 會輸出代碼如下,並且返回變數j

j -= 1

規約4不輸出代碼,返回字元串地址

規約5將之前輸出的各個參數一一壓棧

push t as arg4
push j as arg3
push j as arg2
push "%d,%d,%d" as arg1

規約6調用子函數

call printf

vs2017按這個方法產生的代碼最後結果就是4,4,3

而vc6我試了一下發現他是這樣規約的

  1. j++ =&>arg list
  2. ++j, j++ ==&>arg list
  3. --j, ++j, j++ ==&> arg list
  4. "%d,%d,%d",--j,++j,j++ ==&>arg list
  5. printf("%d,%d,%d",--j,++j,j++); ==&>function

所以vc6生成的偽代碼如下

t = j
push t as arg4 // 不知道為什麼, vc6中 "j++" 中的自增並沒有當場產生指令,
// 而是會在最後參數列表規約的時候才產生指令
j+=1
push j arg3
j-=1
push j arg2
push "%d,%d,%d" as arg1
j+=1 // 參數列表規約時生成第四個參數"j++"中的自增操作
call printf

這樣執行的結果是3,4,3


誰教你寫這種代碼的


C/C++ 中發起函數調用時,在進入被調用函數體(該函數的第一條語句)之前,會保證完成對在調用者中傳入的實參進行求值 (evaluation of arguments)。實參可能以表達式、調用其它函數的形式出現。但是,不保證實參的求值順序 (order of evaluation)。因此,依賴實參求值順序來求得一些結果,是無定義行為(就是題目的現象)。

這個注意事項,在《C++ 編程規範》(下簡稱《規範》)的 Item 31 有敘述。基本上,C++ 是從 C 那裡繼承了這個規定,目的是為了給編譯器一定自由,視情況產生亂序地求值實參的目標代碼(以發揮某些處理器的優勢)。

與這個相關的注意事項,見《規範》Item 13,或《Effective C++》3Ed Item17:最好在獨立的語句中完成對象的資源初始化。例如,獨立地寫一行 shared_ptr& sp(new MyClass),成功之後再將對象作為實參調用函數,尤其是實參間有相互依賴關係時。

甜點:逗號表達式,看上去和參數列表一樣,都是用 , 分隔的表達式序列。但逗號表達式的求值順序是保證從左到右執行的,最後返回最右的值。所以,另一個規則(《規範》Item 30)是不要重載 operator,() 操作符,原因自明。你可以用逗號表達式重寫該程序,看看 VC6.0 和 VC2017 的運行結果。


參數壓棧順序不同。

簡單說就有的編譯器是從左往右來統計參數,而有的編譯器是從右往左來統計參數。當參數是表達式/函數時,就需要把表達式/函數執行一遍,然後將結果作為參數填在那裡。

正因為標準沒有規定參數的壓棧順序,程序表現出來的行為就依賴各家編譯器的實現方式。

這種情況叫"未定義行為",意思是這樣做雖然語法上沒有錯,但是不保證這段代碼在不同編譯器上都能跑出同樣的結果。

所以在編程中應該避免出現"未定義行為",應該換一種行為明確的方式來實現。


你用調試—&>窗口選項把反彙編打開,然後看一下彙編自然就懂了。

如果不想這樣,那就記住不要這樣寫,因為這是undefiend行為

這兩款軟體的底層的彙編語言不一樣,在某些程序運行時運算方式不同


推薦閱讀:
相关文章