首先,面向對象程序設計(object-oriented programming)的核心思想是數據抽象、繼承、動態綁定。通過數據抽象,可以使類的接口與實現分離,使用繼承,可以更容易地定義與其他類相似但不完全相同的新類,使用動態綁定,可以在一定程度上忽略相似類的區別,而以統一的方式使用它們的對象。
虛函數的作用是實現多態性(Polymorphism),多態性是將接口與實現進行分離,采用共同的方法,但因個體差異而采用不同的策略。純虛函數則是一種特殊的虛函數。虛函數聯系到多態,多態聯系到繼承。
一、虛函數
1 . 定義
C++的虛函數主要作用是“運行時多態”,父類中提供虛函數的實現,為子類提供默認的函數實現。
子類可以重寫父類的虛函數實現子類的特殊化。
如下就是一個父類中的虛函數:
class A { public: virtual void out2(string s) { cout<<"A(out2):"<<s<<endl; } };
當我們在派生類中覆蓋某個函數時,可以在函數前加virtual關鍵字。然而這不是必須的,因為一旦某個函數被聲明成虛函數,則所有派生類中它都是虛函數。任何構造函數之外的非靜態函數都可以是虛函數。派生類經常(但不總是)覆蓋它繼承的虛函數,如果派生類沒有覆蓋其基類中某個虛函數,則該虛函數的行為類似於其他的普通成員,派生類會直接繼承其在基類中的版本。
2 . 動態綁定
當我們使用基類的引用(或指針)調用一個虛函數時將發生動態綁定(dynamic binding)。因為我們直到運行時才能知道到底調用了哪個版本的虛函數,可能是基類中的版本也可能是派生類中的版本,判斷的依據是引用(或指針)所綁定的對象的真實類型。與非虛函數在編譯時綁定不同,虛函數是在運行時選擇函數的版本,所以動態綁定也叫運行時綁定(run-time binding)。
3 . 靜態類型與動態類型
靜態類型指的是變量聲明時的類型或表達式生成的類型,它在編譯時總是已知的;動態類型指的是變量或表達式表示的內存中的對象的類型,它直到運行時才可知。當且僅當通過基類的指針或引用調用虛函數時,才會在運行時解析該調用,也只有在這種情況下對象的動態類型才有可能與靜態類型不同。如果表達式既不是引用也不是指針,則它的動態類型永遠與靜態類型一致。
二、純虛函數
1 . 定義
C++中包含純虛函數的類,被稱為是“抽象類”。抽象類不能使用new出對象,只有實現了這個純虛函數的子類才能new出對象。
C++中的純虛函數更像是“只提供申明,沒有實現”,是對子類的約束,是“接口繼承”。
C++中的純虛函數也是一種“運行時多態”。
如下面的類包含純虛函數,就是“抽象類”:
class A { public: virtual void out1(string s)=0; virtual void out2(string s) { cout<<"A(out2):"<<s<<endl; } };
請注意,純虛函數應該只有聲明,沒有具體的定義,即使給出了純虛函數的定義也會被編譯器忽略。
2.引入原因:
1) 為了方便使用多態特性,我們常常需要在基類中定義虛擬函數。
2) 在很多情況下,基類本身生成對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。
為了解決上述問題,引入了純虛函數的概念,將函數定義為純虛函數(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重載以實現多態性。同時含有純虛擬函數的類稱為抽象類,它不能生成對象。這樣就很好地解決了上述兩個問題。
三、普通函數(no-virtual)
普通函數是靜態編譯的,沒有運行時多態,只會根據指針或引用的“字面值”類對象,調用自己的普通函數。
普通函數是父類為子類提供的“強制實現”。
因此,在繼承關系中,子類不應該重寫父類的普通函數,因為函數的調用至於類對象的字面值有關。
四、重載、重寫、重定義
重定義 (redefining)也叫做隱藏:
子類重新定義父類中有相同名稱的非虛函數 ( 參數列表可以不同 ) 。
五、 虛析構函數
虛析構函數: 在析構函數前面加上關鍵字virtual進行說明,稱該析構函數為虛析構函數。雖然構造函數不能被聲明為虛函數,但析構函數可以被聲明為虛函數。
一般來說,如果一個類中定義了虛函數, 析構函數也應該定義為虛析構函數。
六、 抽象基類
含有(或者未經覆蓋直接繼承)純虛函數的類叫抽象基類(abstract base class)。抽象基類負責定義接口,而后續的其他類可以覆蓋該接口。如果派生類中沒有重新定義純虛函數,而只是繼承基類的純虛函數,則這個派生類仍然還是一個抽象基類。因為抽象基類含有純虛函數(沒有定義),所以我們不能創建一個抽象基類的對象,但可以聲明指向抽象基類的指針或引用。
#include <iostream> using namespace std; class A { public: virtual void out1()=0; ///由子類實現 virtual ~A(){}; virtual void out2() ///默認實現 { cout<<"A(out2)"<<endl; } void out3() ///強制實現 { cout<<"A(out3)"<<endl; } }; class B:public A { public: virtual ~B(){}; void out1() { cout<<"B(out1)"<<endl; } void out2() { cout<<"B(out2)"<<endl; } void out3() { cout<<"B(out3)"<<endl; } }; int main() { A *ab=new B; ab->out1(); ab->out2(); ab->out3(); cout<<"************************"<<endl; B *bb=new B; bb->out1(); bb->out2(); bb->out3(); bb->A::out3();
delete ab; delete bb; return 0; }

out3()是一個實函數的重定義.
調用ab->out3();會去調A類中的out3(),它是在我們寫好代碼的時候就會定好的。因為out3()不是虛函數,不會動態綁定,也就是根據它是由A類定義的,這樣就調用這個類的函數。
out2()是虛函數。調用ab->out2();會調用bb中保存的對象中對應的這個函數。這是由於new的B對象。
out1()與out2()一樣,只是在基類中不需要寫函數實現。
同時,還可以通過作用域運算符來實現在子類中調用父類的虛函數。
總結:
①.虛函數必須實現,不實現編譯器會報錯。
②.父類和子類都有各自的虛函數版本。由多態方式在運行時動態綁定。
③.通過作用域運算符可以強行調用指定的虛函數版本。
④.純虛函數聲明如下:virtual void funtion()=0; 純虛函數無需定義。包含純虛函數的類是抽象基類,抽象基類不能創建對象,但可以聲明指向抽象基類的指針或引用。
⑤.派生類實現了純虛函數以后,該純虛函數在派生類中就變成了虛函數,其子類可以再對該函數進行覆蓋。
⑥.析構函數通常應該是虛函數,這樣就能確保在析構時調用正確的析構函數版本。
c++虛函數表: