title: 理解C++虛函數(shù)
date: 2018-11-11 15:31:26
1. 簡單介紹
C++虛函數(shù)是定義在基類中的函數(shù),子類必須對其進(jìn)行覆蓋。在類中聲明(無函數(shù)體的形式叫做聲明)虛函數(shù)的格式如下:
virtual void display();
2. 虛函數(shù)的作用
虛函數(shù)有兩大作用:
(1)定義子類對象,并調(diào)用對象中未被子類覆蓋的基類函數(shù)A。同時在該函數(shù)A中,又調(diào)用了已被子類覆蓋的基類函數(shù)B。那此時將會調(diào)用基類中的函數(shù)B,可我們本應(yīng)該調(diào)用的是子類中的覆蓋函數(shù)B。虛函數(shù)即能解決這個問題。
以下是沒有使用虛函數(shù)的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
void display() {
cout<<"Father::display()\n";
}
// 在函數(shù)中調(diào)用了,子類覆蓋基類的函數(shù)display()
void fatherShowDisplay() {
display();
}
};
// 子類Son
class Son:public Father {
public:
// 重寫基類中的display()函數(shù)
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Son son; // 子類對象
son.fatherShowDisplay(); // 通過基類中未被覆蓋的函數(shù),想調(diào)用子類中覆蓋的display函數(shù)
}
該例子的運行結(jié)果是: Father::display()
以下是使用虛函數(shù)的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
virtual void display() {
cout<<"Father::display()\n";
}
// 在函數(shù)中調(diào)用了,子類覆蓋基類的函數(shù)display()
void fatherShowDisplay() {
display();
}
};
// 子類Son
class Son:public Father {
public:
// 重寫基類中的display()函數(shù)
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Son son; // 子類對象
son.fatherShowDisplay(); // 通過基類中未被覆蓋的函數(shù),想調(diào)用子類中覆蓋的display函數(shù)
}
該例子的運行結(jié)果是: Son::display()
(2)在使用指向子類對象的基類指針,并調(diào)用子類中的覆蓋函數(shù)時,如果該函數(shù)不是虛函數(shù),那么將調(diào)用基類中的該函數(shù);如果該函數(shù)是虛函數(shù),則會調(diào)用子類中的該函數(shù)。
以下是沒有使用虛函數(shù)的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
void display() {
cout<<"Father::display()\n";
}
};
// 子類Son
class Son:public Father {
public:
// 覆蓋基類中的display函數(shù)
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Father *fp; // 定義基類指針
Son son; // 子類對象
fp=&son; // 使基類指針指向子類對象
fp->display(); // 通過基類指針想調(diào)用子類中覆蓋的display函數(shù)
}
該例子的運行結(jié)果是: Father::display()
結(jié)果說明,通過指向子類對象的基類指針調(diào)用子類中的覆蓋函數(shù)是不能實現(xiàn)的,因此虛函數(shù)應(yīng)運而生。
以下是使用虛函數(shù)的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
// 定義了虛函數(shù)
void virtual display() {
cout<<"Father::display()\n";
}
};
// 子類Son
class Son:public Father {
public:
// 覆蓋基類中的display函數(shù)
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Father *fp; // 定義基類指針
Son son; // 子類對象
fp=&son; // 使基類指針指向子類對象
fp->display(); // 通過基類指針想調(diào)用子類中覆蓋的display函數(shù)
}
該例子的運行結(jié)果是: Son::display()
3. 虛函數(shù)的實際意義
或許,很多小伙伴都會有這樣一個疑問:如果想調(diào)用子類中的覆蓋函數(shù),直接通過子類對象,或者指向子類對象的子類指針來調(diào)用,不就沒這個煩惱了嗎?要虛函數(shù)還有什么用呢?
其實不然,虛函數(shù)的實際意義非常之大。比如在實際開發(fā)過程中,會用到別人封裝好的框架和類庫,我們可以通過繼承其中的類,并覆蓋基類中的函數(shù),來實現(xiàn)自定義的功能。
但是,有些函數(shù)是需要框架來調(diào)用,并且API需要傳入基類指針類型的參數(shù)。而使用虛函數(shù)就可以,將指向子類對象的基類指針來作為參數(shù)傳入API,讓API能夠通過基類指針,來調(diào)用我們自定義的子類函數(shù)。這就是多態(tài)性的真正體現(xiàn)。
4. 淺談虛函數(shù)的原理
參考:C++中的虛函數(shù)(表)實現(xiàn)機制以及用C語言對其進(jìn)行的模擬實現(xiàn)
虛函數(shù)的本質(zhì)是一個簡單的虛函數(shù)表。
當(dāng)一個類存在虛函數(shù)時,通過該類創(chuàng)建的對象實例,會在內(nèi)存空間的前4字節(jié)保存一個指向虛函數(shù)表的指針__vfptr
。
__vfptr
指向的虛函數(shù)表,是類獨有的,而且被該類的所有對象共享。虛函數(shù)表的實質(zhì),是一個虛函數(shù)地址的數(shù)組,它包含了類中每個虛函數(shù)的地址,既有當(dāng)前類定義的虛函數(shù),也有覆蓋父類的虛函數(shù),也有繼承而來的虛函數(shù)。
當(dāng)子類覆蓋了父類的虛函數(shù)時,子類虛函數(shù)表將包含子類虛函數(shù)的地址,而不會有父類虛函數(shù)的地址。
同時,當(dāng)用基類指針指向子類對象時,基類指針指向的內(nèi)存空間中的__vfptr
依舊指向了子類的虛函數(shù)表。所以,基類指針依舊會調(diào)用子類的虛函數(shù)。
見如下示例:
4.1. 自己定義了虛函數(shù)的類
class Base1 {
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
定義兩個對象:
Base1 b1;
Base1 b2;
兩個對象的內(nèi)存空間分配如下:
4.2. 既包含覆蓋虛函數(shù),又包含繼承虛函數(shù)的類
class Base1 {
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Derive1 : public Base1 {
public:
int derive1_1;
int derive1_2;
// 覆蓋基類函數(shù)
virtual void base1_fun1() {}
};
定義一個子類對象:
Derive1 d1;
其內(nèi)存空間如下:
由圖可以看出:
Base1 *b_p = &d1; // 指向子類對象的基類指針
b_p->base1_fun1(); // 調(diào)用子類虛函數(shù)