標籤:

C語言中 *p++ = *p 是如何工作的?

我在測試一個運算符優先順序的代碼的時候,碰見了這個問題,我沒法用自己的方式理解,很抱歉打擾各位,希望你們在閑暇之餘,能解答一下我的困惑。

示例代碼:

#include &

#define LEN 5

int main ()

{

printf ("Hello World");

int a[LEN] = { 100, 200, 300, 400, 500 };

int i = 0;

int *p = a;

for (; i &< LEN; ++i)

{

*p++ = *p;

}

for(i = 0;i&

{

printf("%d ",a[i]);

}

return 0;

}


也許有語言律師會告訴你,C++17 之後這個表達式是 well-defined。Order of evaluation / Refining Expression Evaluation Order for Idiomatic C++

不過,作為普通程序員,我寧願繼續當它是 Undefined。誰寫出這樣的代碼就是找死。

陳碩:C++的求值順序問題?


我教你。

你不應該嘗試去理解這種問題。

你應該嘗試去換一個地方面試。

講真的我不查文檔我也不知道這玩意兒的優先順序是什麼。

下頭的人要是誰敢在公司的項目里寫這樣的代碼,被管事兒的拉出去批鬥三天三夜不為過。

做我們伺服器這一行的,講究的是你寫的代碼傻逼都能看懂。

這樣的話,萬一我出去休假了,有人可以幫我修bug。

好吧,實際上是,等我離職了,有人可以把鍋接過去。

代碼里都是這種奇技淫巧,你讓別人怎麼接?

我們都非常不鼓勵程序員通過在代碼里加這種代碼增加自己的不可替代性這種方式來要挾老闆給自己加工資,太沒品,程序員不應該追求這種東西的,low爆了。

因為去糾結這個問題,毫無意義。無論對於你的抽象思維能力,編碼能力還是邏輯能力,架構能力,都是毫無幫助的。類似於回的四種寫法,不要鑽進去,浪費人生。

當然了,如果你僅僅是好奇的話,去看輪子哥的答案。

他把過程講得很清楚了(包括語言標準里決定為「未定義」的順序,他也列舉了可能性了)


在c++里,這條語句屬於未定義行為。

前面的*p++和後面的*p,具體是什麼樣的求值順序,取決於編譯器,非人為能預見到的。

明明兩句話可以說清楚的事非要混在一起,無論是對於自己日後閱讀代碼或者別人閱讀代碼都是一個很頭疼的問題,所以請一條語句完成一件事情。


這行代碼是UB

你不能在一條表達式內多次修改同一個變數的值


*p++=*p

這一句話一共包含了四個操作:

  1. *p // 等號左邊那個
  2. p++
  3. *p
  4. =

這些操作的所有依賴關係如下:

  • 1(*p)在2(p++)之前執行
  • 4(=)在1(*p)和3(*p)之後執行

有了這個約束,那麼這四個操作的所有可能的順序就有(人肉枚舉有可能出錯你自己檢查):

  • 1234
  • 1324
  • 1342
  • 3124
  • 3142

分別的意思就是

  • p[0]=p[1]; p++;
  • p[0]=p[0]; p++;
  • p[0]=p[0]; p++;
  • p[0]=p[0]; p++;
  • p[0]=p[0]; p++;

這就是這類傻逼面試問題的正確解答思路。


一般是這樣工作的:

  1. 把寫這句代碼的人罵一頓。
  2. 然後讓他自己把代碼改成正常代碼。


應對面試:這不符合我的編碼規範,拒絕回答,你放心我在工作中不會寫出這樣的代碼。另外,對於任何我不能確定結果的代碼,我都會編寫單元測試用例確認正常。

I"m bigger than you think.

PS我關於自增自減運算符的看法: 除非我知道我在幹什麼,否則自增自減操作符不與其他計算在同一個語句內,且一條語句最多只能有一個這樣的運算符


看同事寫出這句話,拉出去斬了,就能繼續工作了

另外如果面試遇到,你換家公司,也可以工作了


總覺得看編譯器心情


左邊的*p++,都是先算++再算*。很多同學對p++的理解有誤,p++是先複製副本,然後自加1然後返回副本,然後這個問題其實就變成了先算賦值時左值還是先算右值,而這個未定義,所以不要這麼寫。

說句題外話,其實*pa++=*pb++這種寫法還是經常可以見到的。

另外一個題外話,++p比p++快。

================

現在有點空,找下規範,說得更詳細點。

首先是p++,postfix increment

在c99中定義為:

其中標色的一段話,說明了p++的行為,這個操作返回的是那個操作數,而這個值獲得以後,這個操作數已經被改變

在C11中是說得更明確了一點,這個操作返回的是那個操作數,而作為副作用,這個操作數已經被改變。為了達到規範里所描述的效果,後加加是先賦值臨時變數,然後自加,然後返回臨時變數。我們自己重載一個類的後加加函數的話,代碼類似如下:

class Test{

Test operator++(int)

{

Test tmp = *this;

++ this-&>data;

return tmp;

}

int data;

}

而例子中的*p++這個行為,是先算p++,然後對返回的臨時變數值再做取值操作,這個在operator優先順序上是定義得很明確的,完全沒有歧義,而不是很多同學說的,*p;p++。

有歧義的地方是a = b這個賦值行為,先算左值還是先算右值,對本例來說,可能導致不同的後果。

關於這點,C99明確寫著,順序是沒有定義的。

c11里換了個說法,是無序的,其實還是沒定義

如上,*p ++ = *p,屬於未定義行為,不要這麼寫。


我比較討厭自加、自減運算符和別的別的運算符或者判斷句同時出現,因為這樣根本搞不清參與運算的到底是自加減前的還是自加減後的,可讀性變差。自加減運算符置前的還情有可原,置後的那是絕不能忍。

包括但不限於以下幾種類型:

a = b++;

if (i++ == 10) ...

while (*s++) ...

*p++ = *q++;


本著好奇的心態,在linux下嘗試反彙編看看彙編語言是如何處理的:

上圖是運行到將array數組的地址傳入 ptr 為([ebp-0xc])

接下來為核心的操作 *ptr++ = *ptr

首先&<+51&>: 將 ptr ( [ebp-0xc] )的中的地址(為array的首地址)保存到 eax 中

然後&<+54&> -&> &<+ 57&>: 進行 ptr+1 的操作,並將結果地址通過 edx 最終保存到了 ptr ( [ebp-0xc] 此時保存的為array[1]的地址).

最後 &<+60&> -&> &<+65&>: 將array[1]的內容寫入到array[0]中。

最終總結為:

此結論只針對Linux下的gcc和g++環境

tmp = ptr;

ptr = ptr + 1;

*tmp = *ptr;


建議題主閱讀一下 c陷阱與缺陷 這本書,其中3.7節有這樣的話:

」c語言中只有四個運算符(&&、‖、?:和 ,)存在規定的求值順序」

……

」c語言中其他所有運算符對其操作數的求值順序是未定義的。特別的,賦值運算符並不能保證任何求值順序」

最後,題主要區分好求值順序和運算符優先順序還不是一回事。


這句代碼產生的result的多樣化並不是++與*的優先順序關係,而是那個等號。

C++的大多數運算符,包括賦值運算符,除了自定義的operator以外,都是沒有確定定義的求值順序。

如果編譯器首先對等號左側求值,則等號右側的對象的值就不是程序員所預期的值了。

《C陷阱與缺陷》:不要在一個表達式內對同一個對象多次求值


++在分號結束前都不參與本次計算。

*p=*p;

p++;


未定義行為。前面的 p++ 副作用生效和後一個 p 的求值順序不確定,導致UB。這和運算符優先順序無關。


這樣的代碼有什麼意義,想顯得自己聰明?


拒絕回答好了……

不要在一個語句中多次修改同一變數的值……

P大某雙學位期末考試出過請寫出f(a++,a++)的值,也是一臉懵逼,不過那門課現在已經消失了……


《C陷阱與缺陷》裡面有相似的代碼所以每個*p最後都等於自己 然後再移位,但是看輪子哥的說法好像有很多種情況。

不過還是不要這麼寫的好


似乎大家都很喜歡討論UB...


推薦閱讀:

C/C++中++符號的運算順序是怎樣的?
C 語言的內存管理如何比 C++ 的 RAII 靠譜?
輸出測試時有哪些有趣的字元串可以用來代替「hello, world"?
有些語言用{},有些語言用end,大家怎麼看?
為什麼unordered_map源碼中的need_rehash函數只是將桶容量擴到下一個最小的素數?

TAG:C編程語言 | CC |