大家常聽到一句話:面試造火箭 進來擰螺絲釘;這的確是真實存在的,很多面試官是有虛榮心的,透過展示他們的博學來襯托你的渺小,但不管讓你造啥,你也得想辦法造,造不出來你 就進不去這家公司;
今天,就談一談虛擬函式表,這個東西在面試中真是一道最常出現的菜,用來襯托面試官的博學在合適不過了,所以,你要注意了好好學了:
結合前一講,我們畫一下父類和子類物件關係圖:
上邊這個圖,要詳細解釋,來總結總結吧:
(1)包含虛擬函式的類才會有虛擬函式表, 同屬於一個類的物件共享虛擬函式表, 但是有各自的vptr(虛擬函式表指標),當然指標所指向的地址(虛擬函式表首地址)相同;
可以演示,增加一行:
Derive *d2 = new Derive(); //d2和d雖然都有不同的地址,但是他們裡邊的內容相同,這代表著所指向的地址(虛擬函式表首地址)相同。
(2)父類中有虛擬函式就等於子類中有虛擬函式,換句話說,父類中有虛擬函式表,則子類中肯定也會有虛擬函式表。
因為你是繼承父類的:所以子類中即便註釋掉如下,也依舊會是sizeof==4
virtual void g() { cout << “Derive::g()” << endl; }
也可能有人認為,如果子類中把父類的虛擬函式的virtual去掉,這些函式就不再是虛函數了,比如如下:
class Derive :public Base
{
void f() { cout << “Derive::f()” << endl; }
void g() { cout << “Derive::g()” << endl; }
void h() { cout << “Derive::h()” << endl; }
};
其實大家錯了,只要在父類中是虛擬函式,子類中不用寫virtual,也依舊是虛擬函式;
——-
但是不管你父類還是子類,都只會有一個虛擬函式表,不能認為子類中有一個虛擬函式表+父類中有一個虛擬函式表,得到子類中有兩個虛擬函式表的結論,那子類中是否可能有多個虛擬函式表,後續我們會逐步講到。
(3)如果子類中完全沒有新的虛擬函式,則我們可以認為子類的虛擬函式表和父類的虛擬函式表內容相同,但僅僅是內容相同,這兩個表在記憶體中是處於不同位置的【可以觀察程式中的vptr和vptrpar】,換句話說,這是內容相同的兩張表。虛擬函式表中每一項,儲存著一個虛擬函式的地址,但如果子類的虛擬函式表某項和父類中的虛擬函式表某項代表同一個函式(這表示子類沒有覆蓋父類的虛擬函式),則該表項所指向的該函式的地址應該相同。
(4)超出虛擬函式表部分內容不可知,比如有的顯示0x00000000; 有的是我們目前不理解的值。比如上邊的l,l2,lpar1,lpar2;
Main中程式碼註釋掉,我們研究另外一個話題:我還是寫一段程式碼,和上節課的類似,但有一些改變,大家看:
typedef void(*Func)(void);
Derive derive;
long* pvptrderive = (long*)(&derive);
long* vptrderive = (long*)(*pvptrderive); //0x00bc9b5c
Func f1 = (Func)vptrderive[0]; //0x00ff1569 {project4。exe!Base::f(void)}
Func f2 = (Func)vptrderive[1]; //0x00ff1564 {project4。exe!Derive::g(void)}
Func f3 = (Func)vptrderive[2]; //0x00ba156e {project4。exe!Base::h(void)}
Func f4 = (Func)vptrderive[3]; //0x00000000
現在,我們觀察一下vptrderive的值,並記錄下來,這就是它所指向的類Derive的虛擬函式表的地址,然後我們再生成個子類物件;
Derive derive2 = derive; //呼叫複製建構函式
long* pvptrderive2 = (long*)(&derive2);
long* vptrderive2 = (long*)(*pvptrderive2); //0x00bc9b5c ,所指向的地址和vptrderive一樣,說明兩個物件指向的是同一個虛擬函式表(子類Derive的虛擬函式表);
那我繼續寫如下程式碼:
Base base = derive; /
/直接用子類物件給父類物件值
long* pvptrbase = (long*)(&base);
long* vptrbase = (long*)(*pvptrbase); //0x00bc9db4
Func fb1 = (Func)vptrbase[0]; //0x00bc1569 {project4。exe!Base::f(void)}
滑鼠放fb1上觀察
Func fb2 = (Func)vptrbase[1]; //0x00bc1573 {project4。exe!Base::g(void)}
Func fb3 = (Func)vptrbase[2]; //0x00bc156e {project4。exe!Base::h(void)}
Func fb4 = (Func)vptrbase[3]; //0x00000000
現在呢base的資料是從derive複製過來的。那為什麼base中vptrbase和derive中的vptrderive不一樣呢?按理說如果複製過來資料則
虛擬函式表指標也應該相同
啊,系統內部做了什麼處理呢?
當用一個子類物件初始化一個父類物件時,比如Base base = derive;因為子類物件記憶體一般來講都有更多的成員,所以子類物件記憶體佔用一般都比父類物件記憶體佔用大,最差子類物件記憶體佔用量也會等於父類物件記憶體佔用,肯定不會小於父類記憶體佔用量;
所以子類物件(記憶體)會被切割出一部分(父類部分)放入到父類物件(記憶體)中去,所以:
這句:Base base = derive;
其實有兩個事情,第一個事情是生成一個base物件,第二個事情是用derive來初始化base物件的值,所以編譯器給咱們做了一個選擇,顯然derive初始化base物件時,dervice的虛擬函式表指標值並沒有覆蓋base物件的虛擬函式表指標值,編譯器幫助我們做到這點的;
————————-
OO(面向物件)和OB(基於物件)概念:
大家都有c++基礎,所以都知道c++透過類的
指標
和
引用
來支援多型,這種程式設計風格,就是我們常說的面向物件,OO(object-oriented model【面向物件】);
還有一種程式設計風格叫基於物件OB(object-based),這種風格以前叫ADT(abstract datatype model 【抽象資料模型】),這種模型不支援多型,所以這種模型下的物件的執行速度更快,因為函式呼叫的解析不需要執行時決定(沒有多型),而是編譯期間就解析完成,記憶體空間緊湊程度上更緊湊,因為沒有虛擬函式指標和虛擬函式表這個概念了。
但是顯然OB的設計靈活性就差;
C++它既支援面向物件程式設計,也支援基於物件程式設計,繼承機制和多型性機制就擺在那裡,用不用取決於你,你不用,那就是基於物件的程式設計,你用了那就是面向物件的程式設計。彈性和效率方面,怎麼選擇,取決於你;
大家聽懂了嗎?