先佔個位置,稍微寫點,這兩天忙完了寫代碼版:
關於虛函數與多態,一般是用微軟的解釋:基類指針或引用指向派生類對象時,如果調用虛函數,則調用派生類虛函數。
我看倒不如這樣來解釋:另寫一個函數,如 func (A&a) {} 其中一個參數是基類指針或引用,那麼我在主函數中調用它時,傳入參數是基類對象則是a是基類引用,傳入派生類對象則a是派生類引用,那麼就可以用a調用基類或者派生類虛函數,即在基類指針或引用和虛函數的支持下可以實現動態綁定。
借用c++primer上的一段話:當我們使用基類的引用或指針調用基類中定義的一個函數時,我們並不知道該函數真正作用的對象是什麼類型,因爲它可能是一個基類的對象也可能是一個派生類對象。如果該函數是虛函數,則直到運行時纔會決定到底執行哪個版本,判斷的依據是引用或指針所綁定的對象的真實類型。即對非虛函數的調用在編譯時進行綁定。
舉例如下:
#include <iostream>
using namespace std;
class father
{
public:
virtual void fA()
{
cout << "father fA" << endl;
}
virtual void fB()
{
cout << "father fB" << endl;
}
};
class son : public father
{
public:
virtual void fA()
{
cout << "son fA" << endl;
}
virtual void fC()
{
cout << "son fC" << endl;
}
};
void func(father *obj)
{
obj->fA();
}
int main()
{
father *f = new father;
son *s = new son;
func(f);
func(s);
return 0;
}
輸出如下:
上面就是多態的一個使用樣例。
那麼c++語言具體如何實現多態的呢?需要知道兩個東西!
第一:虛函數表,什麼意思?
就是說一個類一旦擁有虛函數,則會擁有一張虛函數表,這個表獨立於對象,是屬於類本身的,且這張表會被派生類繼承,其中子類重寫的虛函數則會使用子類的虛函數地址替代,比如上面的連個類,它們的虛函數表大致如下:
父類:
father::fA()
father::fB()
子類:
father::fA()
son::fB()
son::fC()
格式:首先是父類虛函數,然後是子類虛函數,父類被重寫部分會被子類的虛函數替代
第二:位於對象頭部的虛表地址,什麼意思?
當一個類被實例化的時候,這個實例化對象的首地址其實是一個指向該類虛表的指針。
因此當程序運行到這裏時,會通過頭部的地址查找虛表中的函數來進行調用。
那麼剛剛說的兩點:虛表?+ 在頭部?真的是這樣嗎,不如來驗證一下,畢竟實踐出真知:
計劃:既然虛函數是一個函數,那麼假設剛剛的兩點是真的,就一定能按照上述的方法使用函數指針來調用到虛函數
代碼如下:
int main()
{
father *f = new father;
void( *p )( );
//首先取到對象的地址,也就是: f
//然後地址的第一個位置存儲的便是虛函數表的地址:*(int*)f
//然後通過虛表就可以訪問函數,比如第一個函數的地址就是:*(int*)*(int*)f
p = ( void(*)( ) )*( ( int * )*( (int*) f ) ); //第一個函數
p();
p = ( void(*)( ) )*( ( int * )*( (int*) f ) + 1 ); //第二個函數
p();
return 0;
}
結果如下:
可以發現,都順利訪問到了,因此上面說的那兩點是對的!
這裏解釋一下,爲什麼取地址的時候需要轉換爲int*,因爲取地址的時候面對的是一個數值串,取地址的時候轉換成int*就是告訴計算機要從這個地方取4個字節的數值,將它當作地址,況且不這麼做怎麼知道+1的時候移動多少呢。
那麼動態綁定實現原理這裏就解釋完了。
最後,再囉嗦一句,可能有人被基類指針這個地方迷糊住了,說爲啥必須要用基類指針,而不能派生類指針呢,其實大可不必迷惑,因爲這是語法上不合邏輯的,因爲派生類本來就比基類的東西多,萬一訪問到基類不存在的呢,這不是出了岔子?其實簡而言之,如果用派生類訪問基類對象,編譯這一關都過不了嘻嘻。