重載:在同一個範圍下面同名不同參的兩個函數互為重載函數;
覆蓋:父類的虛函數在子類中重寫了,同名同參的父類虛函數被重寫;
隱藏:父類中的某個函數名為fun(int,double)函數,子類中同名函數fun(xxx),無論參數是否相同,都將父類中的函數給覆蓋了,雖然你會覺得很噁心,但是cxx大師本傑明在設計cxx的時候處於安全考慮:某些c++程式員可能根部不知道父類中有這麼個名字的函數,在子類中寫了個同名函數,在傳參的時候傳的不是那麼準確,這個時候其實給父類的函數給他執行是一件很恐怖的事情,所以會出現後來cxx的隱藏。
先說下,編譯時間期的函數匹配的問題,比如一個對象Derived d;這麼個對象,在編譯的時候d.test();編譯器會到d的類中給它找到同名test的函數,然後實參匹配形參,匹配度最高的為他的入口地址,如果找到兩個匹配度相同的函數,則編譯器報錯;
eg:
class Base {
public:
virtual int test() {cout << "base" << endl;}
};
class Derived: public Base {
public:
int test(int i) {cout << "derived:int"<< endl;}
int test(double i) {cout << "derived:double" << endl;}
};
int main(){
Derived d;
d.test();
}
d.test();會使得編譯器在子類中尋找名字為test的,結果找到兩個test(int i)和test(double),注意:這裡是找不到Base中的test()函數的,理由上面解釋的很清楚,本傑明大師在設計cxx的時候就明確不允許去基類中找同名函數使得某些程式員誤以為程式是對的;所以這一句會引起cxx編譯器的報錯;
如果我們把Derived的定義改寫為如下形式:
class Derived :public Base {
public:
int test(int i) {cout <<"derived:int" <<endl;}
int test(double i) {cout <<"derived:double" <<endl;}
using Base::test;
};
此時這種情況下,由於程式員告訴了編譯器,我他媽的就是要到基類中找到匹配的函數,這個時候編譯器會讓你去,因為不存在誤解;這個時候d.test();會找到三個匹配的函數,最後匹配度最高的當然是父類中的那個了,參數匹配;ok,編譯通過,當然如果這裡將父類中的test函數設定為private,編譯器仍然會報錯,因為你用using Base::test實際上是人以為父類中的test是pulic的,編譯器這裡報錯是很合理的,他告訴你你的想法錯了;
隱藏大概就上面這些,接下來講解下萬眾矚目的覆蓋了;
覆蓋,其實在每個地方都定義不一樣,在這裡的意思指的是子類中重定義父類中的虛函數;
首先,我們要弄明白為什麼父類會設計虛函數,其實父類的設計者知道這個函數會被重定義;被重定義後,Base *b = new Derived();b的記憶體模型很簡單,
其實,虛函數表是在編譯期間產生的,為什嗎?因為每個類的虛函數表其實是一樣的,在編譯期間產生好了之後,到時候直接放到記憶體中就行了,每個類只需要一個虛函數表就夠了,運行期間的每個對象只需要一個4位元組的指標就可以指向虛函數表了,多省空間啊。也就是說,虛函數表示編譯期間就畫好了,換句話說,函數的覆蓋其實也是在編譯期間完成的,所謂的運行時類型識別,用下面的例子來解釋吧:
Base *pb = new Derived();此時pb是一個Base指標,但是他指向的是一個derived對象,剛才不是說了麼derived對象會有一個虛函數表指標,該指標指向類Derived的虛函數表,就是說在啟動並執行時候pb會先找到derived對象,然後通過該對象找到相應的虛函數表,再找到虛函數表中響應函數的入口,最後調用了Derived類的函數;
對於下面這種情況,Base* pb = new Base();此時pb指向的是一個Base對象,最終找到的是Base類的虛函數表,然後Base的函數。這就達到了運行時類型識別;
所以虛函數表在編譯期間產生的好處就是省空間啊;第一個是虛函數指標,指向一個虛函數表,虛函數表的第一個函數是test(),假設在Derived中test()函數被重定義了,這個時候編譯器會讓Derived的test()去覆蓋Base的test(),然後假設Base中有第二個虛函數test2(),就會在虛函數表的Derived::test()之後的一個表格內,依次類推;
注意:父類的虛函數表會被子類先繼承,然後覆蓋;