一道阿里實習生筆試題的疑惑?
#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有了默認值,
推薦閱讀: