先上概念,C++的多態性:系統在運行時根據對象類型,來確定調用哪個重載的成員函數的能力。
多態性是通過虛函數實現的。成員函數之前加了virtual,即成為虛函數。
有虛成員函數的類,編譯器在其每個對象的開始處自動加一個指針,稱為虛表指針,因為它指向一個表,稱為虛函數表,表的元素是函數指針,指向該類的虛成員函數代碼塊。
該類的所有對象共享一張表。關於虛表指針和虛函數表的具體信息,可以參考皓叔的 虛函數表解析 。
1.如果虛函數在基類與派生類中出現,僅僅是名字相同,而形式參數或者返回類型不同,那麼即使加上了virtual關鍵字,也不會實現多態的。
【此時基類/派生類對象只能直接訪問各自定義的函數,雖然派生類對象的虛表裡有基類的虛函數指針,但是派生類對象不能直接調用】
2.只有類的成員函數才能說明為虛函數,因為虛函數僅適合用與有繼承關系的類對象,所以普通函數不能說明為虛函數。
3.靜態成員函數不能是虛函數,因為靜態成員函數是屬於類的,不屬於任意對象,只作用在類的靜態變量上。
【訪問虛函數需要通過對象的虛表指針訪問虛表,來獲得虛函數入口】
【靜態成員函數也不能是const成員函數,因為編譯器會在對象的const函數中自動插入一個const T *this參數,而靜態成員函數不屬於對象】
4.內聯(inline)函數不能是虛函數。即使虛函數在類的內部定義定義,編譯的時候系統仍然將它看做是非內聯的。
【內聯函數在編譯時可能會展開代碼,這樣內存的代碼區就沒有該函數的代碼了,已經不是一個函數的概念了,自然虛表裡面也沒法保存函數指針了】
5.構造函數不能是虛函數,因為構造的時候,對象還是一片位定型的空間,只有構造完成後,對象才是具體類的實例。
6.析構函數可以是虛函數,而且通常聲名為虛函數。
【有派生類的基類,其析構函數必須為虛函數,這樣析構的時候會先析構派生類對象,再析構基類對象,否則派生類的部分就沒被析構】
Overload(重載):將語義、功能相似的幾個函數用同一個名字表示,但<參數>或<參數與返回值都>不同(參數個數、類型或順序不同),即函數重載。 (1)相同的范圍(在同一個類中或同一個文件內的普通函數); (2)函數名字相同; (3)參數不同; (4)virtual 關鍵字可有可無。 Override(覆蓋):是指派生類函數覆蓋基類函數,只能是虛函數,特征是: (1)不同的范圍(分別位於派生類與基類); (2)函數名字相同; (3)返回值和參數相同; (4)基類函數必須有virtual 關鍵字。 Overwrite(重寫):是指派生類的函數屏蔽了與其同名的基類函數,規則如下: (1)如果派生類的函數與基類的函數同名,但是參數不同。不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。 【規則1】 (2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。
1 class A 2 { 3 public: 4 A() //自定義默認構造函數 5 { 6 Print(); 7 } 8 virtual void Print() 9 { 10 cout << "A is constructed." << endl; 11 } 12 }; 13 14 class B: public A 15 { 16 public: 17 B() 18 { 19 Print(); 20 } 21 22 virtual void Print() //“覆蓋”,但實際可能不是這樣 23 { 24 cout << "B is constructed." << endl; 25 } 26 }; 27 28 int main(int argc, char *argv[]) 29 { 30 A *pa = new B; //加括號或者括號有參數的,調用相應的自定義的構造函數,沒有括號,則調用默認構造函數或者唯一的構造函數 31 delete pa; 32 33 return 0; 34 }
以上代碼輸出
1 A is constructed. 2 B is constructed.
用B的構造函數時,會先調用B的基類即A的構造函數。
A的構造函數裡調用了Print,由於此時對象的類型B的部分還沒有構造好,本質上它只是A的一個對象,其虛表指針指向的是類型A的虛函數表。
接著調用類型B的構造函數,並調用Print。此時已經開始構造B,因此此時調用的Print是B::Print。
因此虛函數在構造函數中調用時,已經失去了虛函數的動態綁定特性。
class A { public: void print() { doPrint(); //調用虛函數 } virtual void doPrint() { cout << "A::doPrint" << endl; } }; class B: public A { public: virtual void doPrint() //虛函數覆蓋 { cout << "B::doPrint" << endl; } }; int main(int argc, char *argv[]) { A a; a.print(); B b; b.print(); return 0; }
以上代碼輸出
1 A::doPrint 2 B::doPrint
在print中調用doPrint時,doPrint()的寫法和this->doPrint()是等價的,因此將根據實際的類型調用對應的doPrint。
1 class A 2 { 3 public: 4 virtual void fun(char c = 'A') //缺省參數 5 { 6 cout << "A::fun " << c << endl; 7 } 8 }; 9 10 class B: public A 11 { 12 public: 13 virtual void fun(char c = 'B') //缺省參數,虛函數覆蓋 14 { 15 cout << "B::fun " << c << endl; 16 } 17 }; 18 19 int main(int argc, char *argv[]) 20 { 21 B b; 22 A &a = b; 23 24 a.fun(); //基類引用派生類對象,動態綁定 25 26 return 0; 27 }
以上代碼輸出
1 B::fun A
由於基類的a是一個指向B對象的引用,因此在運行的時候會調用B::Fun。動態綁定的過程。
但缺省參數是在編譯期決定的。
編譯時,編譯器只知道a是一個類型A的引用,具體指向什麼類型在編譯期是不能確定的,因此會按照A::fun的聲明把缺省參數c設為'a'。
虛函數的作用是可以通過基類的指針或者引用調到派生類的這個函數。
你上面的代碼是演示虛函數的作用,不用去簡便他。
你可以把這個程序中的virtual全部刪除掉,然後再運行程序,觀察一下兩次結果的不一樣,估計你就能理解虛函數的作用了。
B b=new B(); //這句是錯的,不能用括號。
你的問題可以這用解釋:
class A{virtual void fun();}
class B :A{
virtual void fun();
}
int main(){
B b=new B;
b.fun();//調用B的fun();
A *p=&b;
p->fun();//調用B的fun();
return 0;
}
參考資料:www.vckbase.com/document/viewdoc/?id=950