如何看待阿里2016校招研發工程師筆試題題目?
據說『C語言之父』譚浩強先生經常被黑,原因之一就是他糾纏於一些奇奇怪怪的貌似高深然而是 UB 的語法,比如 i+++++i。
但萬萬沒想到在憧憬的阿里的筆試題裡邊見到了這樣的題目,更沒想到的是竟然看到有同學有模有樣地分析起了求值順序、入棧順序,反駁題目是未定義行為的說法。。。
在阿里巴巴這種體量的重視基礎的大公司的校招筆試題里,出現了這樣的題目,應該怎麼來看待呢。
從非技術的角度說,如何看待這道校招題呢?
首先說明出題的人在 C++ 方面的知識有問題,他在一些方面還不如題主。
其次說明在阿里筆試也確實不受重視,題目或許根本不是認真出的,沒什麼大不了的。
下面從技術角度來看。
熟悉 C++ 的話會知道以下的事實:
- C++ 不規定求值順序;
- 因為同一個函數調用的幾個參數之間沒有序列點,不規定副作用(變數值變化)發生時機;(按 C++11 以後用 sequencing 的概念代替了 sequence point,不過結果一樣)
- ++i 與 i++ 在同一段無序列點的代碼中,對同一個變數修改了兩次,是危險的未定義行為;
- 現實中 ABI 雖然有一些函數的調用約定,但通常所說的入棧次序,其實只是規定每個參數的在棧中的位置,並不規定誰先計算。更何況一些常見情況下如 x64,參數不多時傳參用寄存器,就更不存在壓棧次序的問題了。
由於有未定義行為,這題不僅沒有唯一確定的答案(題出錯了),而且題中的程序本身也是錯的。理論上來說,編譯器可以拒絕編譯,編譯出來的結果也可以完全無厘頭。當然現實中的編譯器也就是在幾個指令順序上會有所區別,總會給出一個能運行結果來。
另外需要指出一點的是,這個題還有一個隱秘之處,就是兩個參數都是 const int 這個常量引用類型。i++ 的求值結果是一個右值,v2 這個常量引用得到的是這個求值結果的臨時值的引用,而 ++i 在 C++ 是個左值,參數 v1 確實拿到了 i 的引用。因此現實中編譯器不故意抽風的話,序列點間的未定義行為通常仍會令 i 的值增加 2,然後參數 v1 一定得到的是 i 最後的值 2。
不過總有一種辯解,是說要看實際的情況如何。下面就看看實際的情況。
寫一個 t.cpp:
#include &
void func(const int v1, const int v2)
{
std::cout &<&< v1 &<&< " ";
std::cout &<&< v2 &<&< " ";
}
int main(int argc, char* argv[])
{
int i = 0;
func(++i, i++);
return 0;
}
我們首先用 g++ 編譯(MingW GCC 5.2):
$ g++ -Wall t.cpp
t.cpp: In function "int main(int, char**)":
t.cpp:12:16: warning: operation on "i" may be undefined [-Wsequence-point]
func(++i, i++);
^
然後運行:
$ ./a
2 0
我們再用 clang++ 編譯(Clang 3.6.2):
$ clang++ t.cpp
t.cpp:12:7: warning: multiple unsequenced modifications to "i" [-Wunsequenced]
func(++i, i++);
^ ~~
1 warning generated.
然後運行:
$ ./a
2 1
再用 VC++ 2013 編譯一遍:
&>cl /W4 t.cpp
用於 x64 的 Microsoft (R) C/C++ 優化編譯器 18.00.21005.1 版
版權所有(C) Microsoft Corporation。 保留所有權利。
t.cpp
t.cpp(8) : warning C4100: 「argv」: 未引用的形參
t.cpp(8) : warning C4100: 「argc」: 未引用的形參
Microsoft (R) Incremental Linker Version 12.00.21005.1
Copyright (C) Microsoft Corporation. All rights reserved.
/out:t.exe
t.obj
運行結果是:
2 0
以上結果說明:
- 一些現代編譯器,如這裡的 GCC 和 Clang 都足以準確發現並辨別出代碼中的這種未定義行為,並給出了警告。
- 編譯器警告這個東西大家取捨不同,並不是所有編譯器都能發現這裡的未定義行為(如 VC)。
- 顯然三個編譯器編譯出了不同的代碼,當然,都是不違反 C++ 標準的。
我知道有人還會疑惑 Clang 為什麼會得到 2 1 的結果,這裡只要閱讀 Clang 在未優化時生成的彙編代碼就能明白:
movl $0, -20(%rbp) # 這裡 -20(%rbp) 就是變數 i,初始值為 0
movl $1, -20(%rbp) # 計算左邊的 ++i,對 i 發生副使用
movl $2, -20(%rbp) # 計算右邊的 i++,對 i 發生副使用
movl $1, -24(%rbp) # 按照首次發生副作用之後 i 的值,對 i++ 求值為 1 存入臨時變數 -24(%rbp)
leaq -20(%rbp), %rcx # 按 x64 ABI 使用 %rcx 傳第一個參數,即 ++i 的引用,即 i 的引用,即 i 的指針
leaq -24(%rbp), %rdx # 使用 %rdx 傳第二個參數,即 i++ 的結果臨時變數的引用(指針)
callq _Z4funcRKiS0_ # 調用 func
這裡對 ++i、i++ 語句的求值和副作用就完全是分開的,先完成了計算和副作用,後完成的求值。所以會有這樣的結果。上面的代碼中如果分不清兩次自增各自對應哪條語句,可以把左邊的 ++i 改成 i+=5 這樣的語句(C++ 中 i+=5 也是左值),這樣會更清楚一些:
movl $0, -20(%rbp)
movl $5, -20(%rbp)
movl $6, -20(%rbp)
movl $5, -24(%rbp)
leaq -20(%rbp), %rcx
leaq -24(%rbp), %rdx
callq _Z4funcRKiS0_
結論:別想當然。
函數參數的傳遞方式(包括入棧順序)由ABI規定,函數實參的求值順序(以及side effects生效時間)由編譯器自由決定,兩個順序可能不同(eg. Intel C Compiler),所以這是一道錯題。
如果沒有其他背景資料,題目是錯的,其他人也解釋得很清楚了。
我試答「在阿里巴巴這種體量的重視基礎的大公司的校招筆試題里,出現了這樣的題目,應該怎麼來看待呢。」
不太相信一定規模的公司沒有合適審題的人。事實上我認識到這家公司的朋友,也是非常牛的。我覺得可能是出題流程出現問題,沒有經過足夠的審題過程。
程序會有bug,試卷有bug也不出奇。其實出題有問題並不是一件很嚴重的事,只要通知應試者,並取消該題分數就可以了。阿里果然最擅長的是JAVA
鼓勵大家去阿里寫出這樣奇奇怪怪的代碼~
辣雞。求值順序就不說了,cont 是什麼?
意思很明確了,這就是阿里對應屆生的態度
這種題目可以罵,因為確實做對了也不體現什麼編程功底。
但也不好罵,因為你要明白這類題目的初衷是什麼,筆試題其實就是個篩選。通過這個篩選過濾掉2/3的面試者。
07-12年,我都去過校招,好幾年的筆試題目還是我出的。但最後的篩選分數線其實就40分以上。連60分都沒有。所以大家放心。這卷子總有你能蒙對的題目的。看見有個把怪怪的題目,你就當被噁心了吧,然後趕快看下一題。
誰能告訴我這種未定義行為的題到底該怎麼分析
出題的嚴謹程度代表公司本身的嚴謹程度
我覺得阿里一定是沒碰到過下面這個bug,不然怎麼會堅持出這種題目呢?
話說,這個知識點價值肯定很高的。
你碰到過的最難調的 Bug 是什麼樣的? - 溫酒的回答
不知道出這種題的意義,估計是從網上拷來的題目,看都沒看。
技術分析已經有不少答案做過了。(作為學習者,受益匪淺,感謝那些回答的人。)
至於阿里為什麼會出這種題,我認為是公司達到一定規模之後的通病吧。
沒在阿里工作過,也沒遇到過阿里的員工,所以,以下回答僅為根據自己身邊發生的事情進行的推測。
阿里做到今天,他們的技術人員肯定不會是沒有能力,搞出這種漏洞百出問題的水平。同時,我也不認為阿里的技術人員是以玩弄這種晦澀難懂的語法為樂的。他們中的大多數應該跟我們中的大多數一樣,看到這種題會無言以對吧。
那麼,為什麼擁有這樣技術人員的公司的校園招聘筆試會有這樣的題目出現呢?這先得搞清楚招聘是哪個部門負責。如果沒有什麼特別的意外的話,我相信在阿里,招聘也是人事部門負責的。
那麼人事部門裡都是什麼人呢?都是學心理學、學管理學之類跟技術基本沒有半點關係的員工。所有的招聘流程,筆試、面試安排等等都是他們做的。
然後,大公司里為了保證有秩序的管理,部門之間也是要親兄弟明算賬的。所以,如果人事部門要做一些有技術含量的事情,要麼拿錢到外面找人做(比如請廣告公司做招聘廣告),要麼拿錢內部找其他部門的人做(比如找技術部門的人來出、審筆試題)。
雖然是內部算賬,但是部門領導也心疼啊。這錢出去了,就不是自己的了,想拿來慰勞手下員工的錢就少了,怎麼能省一點呢?
合理的節約是好的,但摳門到一定程度,這就得出事兒了。(然而,公司的財政壓力一般都會把領導們逼成葛朗台。)
可能出現的是以下幾種情況:
- 到外面找個公司,給不幾個錢,換套筆試題。那幾個錢,對方要麼實力不行,要麼就是網上搜搜臨時拼湊一套題交差。反正最後看的也都是不懂技術的人事部門,好糊弄。
- 問問手下的姑娘們,誰跟公司里技術部門的小夥子相熟。如果有親密關係更好,直接私下讓幫出一套題。結果,相熟的剛好是個經驗不多的毛頭小子,為博紅顏一笑,上網上搜了一堆大概組合了一下,交了出去。還是那句話,反正後面沒人審,好糊弄。(有經驗的大叔都有家有室了,不敢造次,所以一般輪不到他們。)
- 問問自己部門裡,有沒有多少有點技術背景的人。一旦發現人才,立即任命下去。人事部門裡的技術高手,通常能搞出這樣的題來,算是很了不起的了。
- 出錢拜託內部技術部門幫著搞,然而技術部門領導把這份錢當外快,把活作為完成本職工作之外的額外任務加給了員工。接活的員工本來就加班加到兩眼冒金星了,這又來個八竿子打不著的活。隨便把自己當年被害過的題目掏出來應付一下算了。
以上任何一種情況,都很容易出現現在大家看到的結果。可能問題就出在那麼一兩個人的身上,並不一定能說明阿里如何如何。阿里內部的技術人員看到這個問題說不定還會蹦出來叫:這哪個龜孫子出的題?這種活,倒是來找我啊?!
不過,這題既然有了,那麼人事部門一定會按照考試成績的好壞來進行人員篩選的。個人認為, @Milo Yip 說的筆試不被重視之類的說法不成立。不過,有了這個問題和大家的回答,阿里的人事招聘的人如果看到了,在算分的時候取消此題成績倒是有可能。(如果還來得及的話。)
我覺得,其實這不算什麼。人生那麼長、世界那麼大,這種被冤死的事情哪個人一生不得經歷那麼幾次?世界是不會保持絕對精確的公平的,它只能儘可能維持一個統計學上的公平,跟量子理論有點像。所以跟討論單個粒子的精確運動狀態和位置類似,糾結單個事件是會累死人的。
同時,這也不能說明阿里如何如何。它不過是曝光了一個幾乎所有大公司都會有的弊病之一罷了。這也正從側面說明,它的規模真的很大了。
我覺得,這裡有一個點需要重視。函數的參數,是「引用」(const 修飾在這個上下文下沒什麼意義,我們可以視而不見),也就是實際上是一個int的地址;
所以在調用
i=0;
foo(++i, i++) 的時候,
例如:(按照一種常規處理方式):
i2 = i;
i++; //這句話雖然常規是放在這裡,但是沒有人強制規定它一定在這個位置
++i;
foo(i, i2) ; 這樣,打出來第一個數字就是2了;第二個數字是0;
我只能說,微軟的編譯器會比較可能出現:2,0 這樣的結果。大家可以自己去試驗,然後反彙編看看編譯器怎麼做的。
雖然看起來兩個參數都是 i,但是第二個 i 可不一定是 i 本身!這取決於編譯器怎麼處理。
如果我把上面的 i++放在最後,那第一個數字就是1。
但是如果是這樣處理呢,可能不太現實一點,但是這樣的處理也是語法規則上「合法」的:
++i;
foo(i, i);
i++;
這樣打出來的就是兩個1;
如果一定要我們給一個共識性的結論,我們只能根據語言規則,給出這樣一個「模糊」的結論:
(1)輸出的第一個數字 是 1 或者 2;
(2)第二個數字是 0 或者 1;
(3)兩個數字可能相同,也可能不同。(簡直廢話)
推論:如果兩個數字相同,就是 1,1。
從上面的結論看選項,有: C,D,F 滿足上面的結論。(當然這只是成為答案的必要條件,不代表 C,D,F 已經是答案)。
如果不是這樣,那麼我就會感到很奇怪,請解釋,為什麼你的結果違反我的認知,如果你能解釋的通,那我也算你對。。。
PS:用 vs2005 測試了下,上面的題目的輸出是「2, 0」。
PS:這個也不能叫做錯題,和有bug,而是這種題目因為(人,以及處理方法)會產生分歧性,所以不應該被選為面試,考試的題目!毫無疑問,這是出這道題的人有問題。為什麼那兩個引用的都是一個變數,卻得到不同的結果?求解答。
難道都是臨時變數?
譚老爺子被黑才不是因為他糾結這樣的問題,而是因為他老說錯誤的東西…
C/C++語言的++運算符的順序在語言說明裡一直都有。Denis的書里照樣解釋了這個問題,這可是基礎啊……
至於傳值問題,這裡傳的都是引用,然而這樣是不會傳遞中間結果的。llvm會送你個warning……這道題當然可以說首先分析參數的入棧順序是什麼樣,而且如果我記得沒錯的話,這道題的原型是在某程序員面試寶典上,只是那道題是printf,這道題是func。
然後再是考察i++,和++i的區別。
但是,雖然你可以說這是考試,但是我還是覺得這道題不是一道好的題目,我覺得大公司還是應該避免出這樣的題目。寫Java的不懂C++
我昨晚剛好看了這套題,順便說一句,這套題還有一道二分法的題目也是有問題的...
我覺得出題人沒有深刻理解二分法,在邊界開閉不同情況下代碼處理都是不一樣的都沒有考慮到。
講真,這種然並卵的東西當初在學校考試都不會考
推薦閱讀: