注意:本文中代碼均使用 Qt 開發編譯環境
虛函數是動態聯編的基礎。虛函數必須是基類的非靜態成員函數,其訪問權限可以是protected或public,在基類的類定義中定義虛函數的一般形式:
virtual returnType functionName (param...) {
// some codes
}
我們知道,根據類型兼容規則,可以使用派生類的對象代替基類對象。如果用基類類型的指針指向派生類對象,就可以通過這個指針來訪問該對象,問題是訪問到的只是從基類繼承來的同名成員。解決這一問題的辦法是:如果需要通過基類的指針指向派生類的對象,并訪問某個與基類同名的成員,那么首先在基類中將這個同名函數說明為虛函數。這樣,通過基類類型的指針,就可以使屬于不同派生類的不同對象產生不同的行為,從而實現了運行過程的多態。
動態聯編規定,只能通過指向基類的指針或基類對象的引用來調用虛函數,其格式為:
ptrToBaseClass->virtualFunctionName(param...);
或
quoteToBaseClass.virtualFunctionName(param...);
虛函數是C++多態的一種表現
例如:子類繼承了父類的一個函數(方法),而我們把父類的指針指向子類,則必須把父類的該函數(方法)設為virtual(虛函數)。
使用虛函數,我們可以靈活的進行動態聯編,當然是以一定的開銷為代價。
如果父類的函數(方法)根本沒有必要或者無法實現,完全要依賴子類去實現的話,可以把此函數(方法)設為:
virtual returnType functionName(params) = 0;
我們把這樣的函數(方法)稱為純虛函數。而如果一個類包含了純虛函數,我們稱此類為抽象類。
虛函數示例:
#include <QCoreApplication>
#include <QDebug>
class B{
public:
virtual void display(){ qDebug() << "B::display()"; }
};
class C: public B{
public:
void display(){ qDebug() << "C::display()"; }
};
class D: public C{
public:
void display(){ qDebug() << "D::display()"; }
};
void fun(B *ptr){
ptr->display();
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
B b,*p;
C c;
D d;
p = &b; fun(p);
p = &c; fun(p);
p = &d; fun(p);
return a.exec();
}
運行輸出:
B::display()
C::display()
D::display()
在本例子中,派生類并沒有顯式給出虛函數聲明,這時系統就會遵循以下規則來判斷派生類的一個函數成員是不是虛函數:
(1)該函數是否與基類的虛函數有相同的名稱;
(2)該函數是否與基類的虛函數有相同的參數個數及相同的對應參數類型;
(3)該函數是否與基類的虛函數有相同的返回值或者滿足類型兼容規則的指針、引用型的返回值。
如果從名稱、參數以及返回值三個方面檢查之后,派生類的函數滿足了上述條件,就會自動確定為虛函數。這時,派生類的虛函數就會覆蓋基類的虛函數。不僅如此,派生類中的虛函數還會隱藏基類中同名函數的所有其他重載形式。
當基類的構造函數調用虛函數時,不會調用派生類的虛函數。這是因為基類被構造時,對象還不是一個派生類對象。同樣,當基類被析構時,對象已經不再是一個派生類對象了。所以基類此時調用的是基類的虛函數。
只有虛函數是動態聯編的,如果派生類需要修改基類的行為(即重寫與基類函數同名的函數),就應該在基類中將相應的函數聲明為虛函數。而基類中聲明的非虛函數,通常代表那些不希望被派生類改變的功能,也是不能實現多態的。因此一般不要重寫繼承來的非虛函數(雖然語法上并沒有強行限制)。
在重寫繼承來的虛函數時,如果函數有默認形參值,千萬不要重新定義不同的值。原因是:雖然虛函數是動態聯編的,但默認形參值是靜態綁定的。也就是說通過一個指向派生類對象的基類指針,可以訪問到派生類的虛函數,但默認形參值卻只能來自基類的定義。
實現動態聯編需要三個條件:
1、 必須把動態聯編的行為定義為類的虛函數。
2、 類之間存在子類型關系,一般表現為一個類從另一個類公有派生而來。
3、 必須先使用基類指針指向子類型的對象,然后直接或者間接使用基類指針調用虛函數。
定義虛函數的限制:
(1)非類的成員函數不能定義為虛函數,類的成員函數中靜態成員函數和構造函數也不能定義為虛函數,但可以將析構函數定義為虛函數。實際上,優秀的程序員常常把基類的析構函數定義為虛函數。因為,將基類的析構函數定義為虛函數后,當利用delete刪除一個指向派生類定義的對象指針時,系統會調用相應的類的析構函數。而不將析構函數定義為虛函數時,只調用基類的析構函數。
(2)只需要在聲明函數的類體中使用關鍵字“virtual”將函數聲明為虛函數,而定義函數時不需要使用關鍵字“virtual”。
(3)當將基類中的某一成員函數聲明為虛函數后,派生類中的同名函數自動成為虛函數。
(4)如果聲明了某個成員函數為虛函數,則在該類中不能出現和這個成員函數同名并且返回值、參數個數、類型都相同的非虛函數。在以該類為基類的派生類中,也不能出現這種同名函數。
虛函數聯系到多態,多態聯系到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什么都沒得談。