虛函數對於多態具有決定性的作用,有虛函數才能構成多態,這節我們來重點說一下虛函數的注意事項。
1) 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
2) 為了方便,你可以只將基類中的函數聲明為虛函數,這樣所有派生類中具有遮蔽(覆蓋)關系的同名函數都將自動成為虛函數。關於名字遮蔽已在《C++繼承時的名字遮蔽》一節中進行了講解。
3) 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那麼將使用基類的虛函數。
4) 只有派生類的虛函數遮蔽基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。例如基類虛函數的原型為
virtual void func();
,派生類虛函數的原型為
virtual void func(int);
,那麼當基類指針 p 指向派生類對象時,語句
p -> func(100);
將會出錯,而語句
p -> func();
將調用基類的函數。
5) 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明為虛函數沒有什麼意義。
6) 析構函數可以聲明為虛函數,而且有時候必須要聲明為虛函數,這點我們將在下節中講解。
構成多態的條件
站在“學院派”的角度講,封裝、繼承和多態是面向對象的三大特征,封裝、繼承分別在《C++類成員的訪問權限》《C++繼承與派生》中進行了講解,而多態是指通過基類的指針既可以訪問基類的成員,也可以訪問派生類的成員。
下面是構成多態的條件:
-
必須存在繼承關系;
-
繼承關系中必須有同名的虛函數,並且它們是遮蔽(覆蓋)關系。
-
存在基類的指針,通過該指針調用虛函數。
下面的例子對各種混亂情形進行了演示:
#include <iostream>
using namespace std;
//基類Base
class Base{
public:
virtual void func();
virtual void func(int);
};
void Base::func(){
cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
cout<<"void Base::func(int)"<<endl;
}
//派生類Derived
class Derived: public Base{
public:
void func();
void func(char *);
};
void Derived::func(){
cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
cout<<"void Derived::func(char *)"<<endl;
}
int main(){
Base *p = new Derived();
p -> func(); //輸出void Derived::func()
p -> func(10); //輸出void Base::func(int)
p -> func("http://c.biancheng.net"); //compile error
return 0;
}
在基類 Base 中我們將
void func()
聲明為虛函數,這樣派生類 Derived 中的
void func()
就會自動成為虛函數。p 是基類 Base 的指針,但是指向了派生類 Derived 的對象。
語句
p -> func();
調用的是派生類的虛函數,構成了多態。
語句
p -> func(10);
調用的是基類的虛函數,因為派生類中沒有函數遮蔽它。
語句
p -> func("http://c.biancheng.net");
出現編譯錯誤,因為通過基類的指針只能訪問從基類繼承過去的成員,不能訪問派生類新增的成員。
什麼時候聲明虛函數
首先看成員函數所在的類是否會作為基類。然後看成員函數在類的繼承後有無可能被更改功能,如果希望更改其功能的,一般應該將它聲明為虛函數。如果成員函數在類被繼承後功能不需修改,或派生類用不到該函數,則不要把它聲明為虛函數。