引出:寫個類A,聲明類A指針指向NULL,調用類A的方法會有什么后果,編譯通過嗎,運行會通過嗎?
#include<stdio.h> #include<iostream> using namespace std; class base{ int a; public: void fun(){ printf("base fun\n"); } }; int main(){ base *b=NULL; b->fun(); }
看到這個的時候,一定以為運行會報錯吧。
但是奇跡般的,編譯器輸出了:base fun
#include<stdio.h> #include<iostream> using namespace std; class base{ int a; public: virtual void fun(){ printf("base fun\n"); } }; int main(){ base *b=NULL; b->fun(); }
在看這個代碼,還以為會輸出base fun么,又錯了,運行報錯!
為什么會是這個結果?
#include<stdio.h> #include<iostream> using namespace std; class base{ int a; public: virtual void fun(){ printf("base fun\n"); } void fun2(){ printf("base fun\n"); } }; int main(){ base *b=NULL; b->fun(); b->fun2(); }
可以發現,一個是虛函數,一個普通函數
在觀察下內存中得情況:
發現果然虛函數還沒在內存中,而fun2已經在內存中了
在看看匯編:
明顯發現虛函數的調用比普通函數多了好幾個步驟,
ecx 中放的this 指針,所以this=0(NULL),但是普通函數fun2放在全局內存區,所以可以訪問
而虛函數是根據虛函數表尋找的,這時沒有虛函數表,自然就沒法查到虛函數的地址了。
因為非虛函數的地址對編譯期來說“靜態”的,也就是函數地址在編譯期就已經確定了,實例地址對於非虛函數只是那個 this 指針參數。所以只要不訪問類的實例數據就沒什么問題。而虛函數的地址,是先到實例的地址前面去查找它的虛函數表所在的地址。然后從虛函數表里取出該函數所對應的元素(虛函數表是一個函數指針數組)來call的。(當然一個已知的類的虛函數表的內容也是編譯期靜態的,但不同類的虛函數表內容不同,即運行時多態的基礎)所以實例如果為NULL,是個有特殊意義的值,是會觸發運行時錯誤的。
總結:類中的虛函數是動態生成的,由虛函數表的指向進行訪問,不為類的對象分配內存,就沒有虛函數表就無法訪問。
類中的普通函數靜態生成,不為類的對象分配內存也可訪問。