先看一個例子:
#include<iostream>
using namespace std;
class Base1{
public:
virtual void display() const; //虛函數
};
void Base1::display() const
{
cout << "Base1::display()" << endl;
}
class Base2 :public Base1
{
public:
virtual void display() const; //虛函數
};
void Base2::display() const
{
cout << "Base2::display()" << endl;
}
class Derived :public Base2
{
virtual void display() const; //虛函數
};
void Derived::display() const
{
cout << "Derived::display()" << endl;
}
void fun(Base1 *ptr)
{
ptr->display();
}
int main()
{
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
system("pause");
return 0;
}
上面的程序中,每一個類中都有一個display函數,我們將其聲明爲虛函數,則當程序編譯時,並不會編譯該函數,即不會指定display屬於哪一個對象,而當程序運行時纔會編譯。
1. 初識虛函數
- 用virtual關鍵字說明的函數
- 虛函數是實現運行時多態性的基礎
- C++中的虛函數是動態綁定的函數
- 虛函數必須是非靜態的成員函數,虛函數經過派生之後,就可以實現運行過程中的多態。
什麼函數可以是虛函數:
- 一般成員函數可以是虛函數
- 構造函數不能是虛函數
- 析構函數可以是虛函數
一般虛函數成員:
- 虛函數的聲明:virtual 函數類型 函數名 (形參表)
- 虛函數聲明只能出現在類定義中的函數原型聲明中,而不能在成員函數實現的時候
- 在派生類中可以對基類中的成員函數進行覆蓋
- 虛函數一般不聲明爲內聯函數,因爲對虛函數的調用需要動態綁定,而對內聯函數的處理是靜態的。
virtual 關鍵字:
- 派生類可以不顯示地用virtual聲明虛函數,這時系統就會用以下規則來判斷派生類的一個函數成員是不是虛函數:(1)該函數是否與基類的虛函數有相同的名稱、參數個數及對應的參數類型;(2)該函數是否與基類的虛函數有相同的返回值或者滿足類型兼容規則的指針、引用型的返回值。
- 如果從名稱、參數及返回值三個方面檢查之後,派生類的函數滿足以上條件,就會自動確定爲虛函數。這時,派生類的虛函數便會覆蓋基類的虛函數。
- 派生類的虛函數還會隱藏基類中同名的所有其它重載形式
- 一般習慣於在派生類的函數中也是用virtual關鍵字,以增加程序的可讀性。
2. 虛析構函數
什麼時候會用到虛析構函數呢?如果打算允許其他人通過基類指針調用對象的析構函數,就需要讓基類的析構函數稱爲虛函數,否則執行delete的結果是不確定的。
通過一個例子說明:
#include<iostream>
using namespace std;
class Base{
public:
virtual ~Base();
};
Base::~Base(){
cout << "Base destructor" << endl;
}
class Derived :public Base{
public:
Derived();
virtual ~Derived();
private:
int *p;
};
Derived::Derived(){
p = new int(0);
}
Derived::~Derived(){
cout << "Derived destructor" << endl;
delete p;
}
void fun(Base *b){
delete b;
}
int main()
{
Base *b = new Derived();
fun(b);
system("pause");
return 0;
}
3. 虛表與動態綁定
虛表:
- 每個多態類有一個虛表(virtual table)
- 虛表中有當前類的各個函數的入口地址
- 每個對象有一個指向當前類的虛表的指針(虛指針vptr)
動態綁定的實現:
- 構造函數中爲對象的虛指針賦值
- 通過多態類型的指針或引用調用成員函數時,通過虛指針找到虛表,進而找到所調用的虛函數的入口地址
- 通過該入口地址調用虛函數
虛表示意圖: