標籤:

一道阿里實習生筆試題的疑惑?

#include &
using namespace std;
class animal
{
protected:
int age;
public:
virtual void print_age(void) = 0;
};
class dog : public animal
{
public:
dog() {this -&> age = 2;}
~dog() { }
virtual void print_age(void) {cout&<&<"Wang, my age = "&<& age&<& age = 1;}
~cat() { }
virtual void print_age(void) {cout&<&<"Miao, my age = "&<& age&<& print_age();
return 0;
}

輸出是:

Wang, my age = 1

今天線上筆試遇到的一道題,很好奇,這幾句:

int * p = (int *)(kitty);
int * q = (int *)(jd);
p[0] = q[0];

這是為什麼呢?

--------------------------------------------------------------------------------

確實是:

p[0] = q[0];

我沒有記錯。

當時讀題時,看到 基類,派生類,虛函數,,我就猜到肯定是要考 多態,虛函數表這些知識,

當時就是對這句挺疑惑:

p[0] = q[0];

因為這句可能會修改虛函數表,但也不一定阿,因為時間比較緊,就賭了一把,認為虛函數表被修改了。

所以筆試完了,就挺困惑,宿舍熄燈後,怎麼也睡不著,就拿到知乎上,想問問各位。

說實話,真的很感謝 @藍色 大大,這麼晚還回答了

這個是昨天筆試完,晚上11點左右提的,不能算我筆試違規吧? T_T

好吧,我也不匿名了,感覺匿名不好,不懂就問,為啥要匿名。

我不是 陳浩大大,只是個普通院校 大三 計算機科班生。


這是一個取巧的改變虛表指針的辦法,它利用了C++的對象模型的特點。我們知道,一個類有了虛函數後,它會有一個虛表來維護虛函數和一個虛表指針__vptr來指向它,而這個程序利用的即是改變虛指針的指向。它首先kitty,並且轉換為int*,獲得cat類的虛表首地址,同樣jd獲得dog類的虛表地址,而p[0] = q[0]令指向cat的虛表首地址,一下就變成了指向dog類的虛表首地址,然後基類獲取到了這個指向dog類的kitty,調用虛方法則自然調用到了dog的print_age,然後這裡的age則依然保留的是cat的,因為你只是改變了虛指針指向的虛表地址,不影響member data.


首先 @藍色大大的答案已經把思路說得很清楚了。這是個hack,不是C++語言規範所保證的行為,而是某些C++編譯器採用的C++ ABI的行為。

放個傳送門:為什麼bs虛函數表的地址(int*)(bs)與虛函數地址(int*)*(int*)(bs) 不是同一個? - RednaxelaFX 的回答

其次,這代碼不但依賴某些C++編譯器的行為,還依賴平台的指針寬度是32位。

int * p = (int *)(kitty);
int * q = (int *)(jd);
p[0] = q[0];

這幾句不應該用int*,而應該用intptr_t*才對。這樣才能保證拷貝的是一個指針寬度的數據,而不是一個int寬度的數據。

  • 在32位平台上,int通常是32位,而指針是32位,所以正好匹配了,程序能正常運行;

  • 在64位平台上,如果是流行的LP64模型,int是32位而指針是64位,這裡實際上只拷貝了指針的一半,程序能否正常運行就看運氣了。

如果是在一個64位且小端(little endian)的平台上,那這代碼拷貝的是指針的低32位。很可能會運氣好能正常運行,因為dog類與cat類的vtable可能正好在內存里處於很近的位置,它們的地址的高32位可能正好相同,地址不同的地方都在低32位,這樣這個程序就運氣好能正常運行。

如果是在一個64位且大端(big endian)的平台上,那這段代碼拷貝的是指針的高32位,那就完全達不到效果了。

不知道誰出的這種題?

或者題主把題目的細節記錯了。

後面有回答說原本的筆試題不是p[0] = q[0],而是p[1] = q[1]。如果是這樣的話那仍然只能在32位平台上能行,在64位平台上就紗布了。

再次,這種題還有很多玩法。例如說一種簡單的玩法是像這樣:

#include &
using namespace std;

class animal
{
protected:
int age_;
animal(int age): age_(age) { }

public:
virtual void print_age(void) = 0;
virtual void print_kind() = 0;
virtual void print_status() = 0;
};

class dog : public animal
{
public:
dog(): animal(2) { }
~dog() { }

virtual void print_age(void) {
cout &<&< "Woof, my age = " &<&< age_ &<&< endl; } virtual void print_kind() { cout &<&< "Im a dog" &<&< endl; } virtual void print_status() { cout &<&< "Im barking" &<&< endl; } }; class cat : public animal { public: cat(): animal(1) { } ~cat() { } virtual void print_age(void) { cout &<&< "Meow, my age = " &<&< age_ &<&< endl; } virtual void print_kind() { cout &<&< "Im a cat" &<&< endl; } virtual void print_status() { cout &<&< "Im sleeping" &<&< endl; } }; void print_random_message(void* something) { cout &<&< "Im crazy" &<&< endl; } int main(void) { cat kitty; dog puppy; animal* pa = kitty; intptr_t* cat_vptr = *((intptr_t**)(kitty)); intptr_t* dog_vptr = *((intptr_t**)(puppy)); intptr_t fake_vtable[] = { dog_vptr[0], // for dog::print_age cat_vptr[1], // for cat::print_kind (intptr_t) print_random_message }; *((intptr_t**) pa) = fake_vtable; pa-&>print_age(); // Woof, my age = 1
pa-&>print_kind(); // Im a cat
pa-&>print_status(); // Im crazy

return 0;
}

直接整個vtable偽造出來然後想往裡面填啥就填啥。

至於有沒有實際應用使用了題主原本寫的那種代碼,還真有。(但這麼用的都該拖出去打pp?

例如說Oracle/Sun JDK / OpenJDK里的HotSpot VM,在PermGen Removal之前,有一類叫klassOopDesc的對象是由GC管理的,但裡面還嵌套包含一個Klass的子類對象,而Klass類有vptr。為了能正確管理klassOopDesc里嵌套的Klass的vptr,就有了這麼個奇葩的東西:

class Klass_vtbl

好同學們請不要學這種例子?這個奇葩的結構在PermGen Removal後就移除了嗯。


對啊我就在想,C++的對象存儲順序不是應該和聲明順序一樣嗎?那應該首先用4位元組存age,然後用4位元組存那個void*才對

然後像上邊幾位大神說的一樣,這個寫法依賴於系統是32位,否則會出錯。再者實際工作中寫程序的話應該盡量尊重類型本身,C++雖然可以用改變類型的方式做hack但這不是什麼好事。生產環境下這種代碼很容易出問題,而且極難定位。

最後就是,現在這個時代了還考hack,阿里也太low了…………


我能說 dog jd 亮了么


修改對象實例在起始地址位置的那個虛函數表指針,把它替換成 dog 類的虛函數表地址了,所以虛函數多態時就會令對象表現成 dog 的虛函數。

當然,這裡和 cpp object model 有關,也就是說,這裡默認為對象起始位置是虛函數表指針。

所謂 p[0] =q [0].

就是把位於實例起始位置的虛函數表指針的值改了,讓它指向另一個類型的虛函數表。當然,兩個類型的虛函數表必須後者兼容前者。也就是說, dog cat 的虛函數表形式一樣。


這種代碼不適合筆試吧,vptr實現又不一定在開頭,只要回答了這個問題,答案就是錯的。不能回答。這叫測不準法則,薛定諤的代碼,不回答是對的,回答了就是錯的。


出這種題做面試題不太好吧?這能考量出什麼?我記得inside the c++ object model 裡面講了,vptr又不一定放在開始。。。。 看編譯器吧。 最不喜歡這種hack語言的題目


作為實習生的筆試題,這題有點過了,最多算附加題。


那兩句應該是取到了kitty和jd兩個類的虛函數表指針的指針,然後將jd的虛函數表指針賦予kitty,這時kitty向上類型轉換後調用的printage是其實綁定的是jd的printage。

另外將jd作為dog類的變數名真是讓人浮想聯翩。


這種只能在特定平台上工作的,甚至連LP64都可能出問題的代碼做為面試題只能說是太不合理了


你把題目看錯了,明明是p[1]=q[1],他就沒想考虛表指針的事,簡單的對象模型的題,而且題目說了是32位機,你不把這個寫出來大神們會覺得這題有問題(PS:題主不是故意來騙回復的吧,~逃)


我的隊友要是寫出這樣的代碼直接拖出去打死


這就是個為了考試而考試的題目,真的能有什麼實際意義么?


我記得阿里筆試有要求不許泄露題目的,這樣直接問出來不太好吧。如果實在想知道答案,求知慾強烈,可以私下問人啊。

ps:昨天做到一道差不多的題


道理我都懂,可明明我考的是java研發,為什麼給我出這題,心好累。


這題我蒙的


我感覺這個問題不是虛表的問題 這個應該考的是對象在內存中如何存儲的 其中直接操作了成員變數age了


出這個題目的面試官應該被批評才對。

這種類型的題目應該出問答題而不是具體實現。


相信你問這道題,你已經了解虛函數的機制,以及虛函數表vptable、虛函數指針vptr是什麼

一個虛類共用一個vptable,而每一個對象實例,擁有一個vptr

這道題成功的前提是vptr的地址就是對象的首地址,這是編譯器來實現的,所以如果你使用的編譯器不是這麼做的,那就沒這個trick的結果了。

然後實現的邏輯就是 @藍色 所說的了

給你耗子叔多年前寫的文章參考下

C++ 虛函數表解析


為啥看著看著我就想到了京東


同學,你價值觀有問題。

你已經被我司HR部門鎖定,終身不錄用,謝謝。


有本書上寫的,dog和cat的age有了默認值,


推薦閱讀:

學數據結構用C還是C++?
如何提升 C++ Trie 樹的存取效率?

TAG:實習生 | C |