C++虛擬函式
title: 理解C++虛擬函式
date: 2018-11-11 15:31:26
文章目錄
1. 簡單介紹
C++虛擬函式是定義在基類中的函式,子類必須對其進行覆蓋。在類中宣告(無函式體的形式叫做宣告)虛擬函式的格式如下:
virtual void display();
2. 虛擬函式的作用
虛擬函式有兩大作用:
(1)定義子類物件,並呼叫物件中未被子類覆蓋的基類函式A。同時在該函式A中,又呼叫了已被子類覆蓋的基類函式B。那此時將會呼叫基類中的函式B,可我們本應該呼叫的是子類中的覆蓋函式B。虛擬函式即能解決這個問題。
以下是沒有使用虛擬函式的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
void display() {
cout<<"Father::display()\n";
}
// 在函式中呼叫了,子類覆蓋基類的函式display()
void fatherShowDisplay() {
display();
}
};
// 子類Son
class Son:public Father {
public:
// 重寫基類中的display()函式
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Son son; // 子類物件
son.fatherShowDisplay(); // 通過基類中未被覆蓋的函式,想呼叫子類中覆蓋的display函式
}
該例子的執行結果是: Father::display()
以下是使用虛擬函式的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
virtual void display() {
cout<<"Father::display()\n";
}
// 在函式中呼叫了,子類覆蓋基類的函式display()
void fatherShowDisplay() {
display();
}
};
// 子類Son
class Son:public Father {
public:
// 重寫基類中的display()函式
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Son son; // 子類物件
son.fatherShowDisplay(); // 通過基類中未被覆蓋的函式,想呼叫子類中覆蓋的display函式
}
該例子的執行結果是: Son::display()
(2)在使用指向子類物件的基類指標,並呼叫子類中的覆蓋函式時,如果該函式不是虛擬函式,那麼將呼叫基類中的該函式;如果該函式是虛擬函式,則會呼叫子類中的該函式。
以下是沒有使用虛擬函式的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
void display() {
cout<<"Father::display()\n";
}
};
// 子類Son
class Son:public Father {
public:
// 覆蓋基類中的display函式
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Father *fp; // 定義基類指標
Son son; // 子類物件
fp=&son; // 使基類指標指向子類物件
fp->display(); // 通過基類指標想呼叫子類中覆蓋的display函式
}
該例子的執行結果是: Father::display()
結果說明,通過指向子類物件的基類指標呼叫子類中的覆蓋函式是不能實現的,因此虛擬函式應運而生。
以下是使用虛擬函式的例子:
#include<iostream>
using namespace std;
// 基類 Father
class Father {
public:
// 定義了虛擬函式
void virtual display() {
cout<<"Father::display()\n";
}
};
// 子類Son
class Son:public Father {
public:
// 覆蓋基類中的display函式
void display() {
cout<<"Son::display()\n";
}
};
int main() {
Father *fp; // 定義基類指標
Son son; // 子類物件
fp=&son; // 使基類指標指向子類物件
fp->display(); // 通過基類指標想呼叫子類中覆蓋的display函式
}
該例子的執行結果是: Son::display()
3. 虛擬函式的實際意義
或許,很多小夥伴都會有這樣一個疑問:如果想呼叫子類中的覆蓋函式,直接通過子類物件,或者指向子類物件的子類指標來呼叫,不就沒這個煩惱了嗎?要虛擬函式還有什麼用呢?
其實不然,虛擬函式的實際意義非常之大。比如在實際開發過程中,會用到別人封裝好的框架和類庫,我們可以通過繼承其中的類,並覆蓋基類中的函式,來實現自定義的功能。
但是,有些函式是需要框架來呼叫,並且API需要傳入基類指標型別的引數。而使用虛擬函式就可以,將指向子類物件的基類指標來作為引數傳入API,讓API能夠通過基類指標,來呼叫我們自定義的子類函式。這就是多型性的真正體現。
4. 淺談虛擬函式的原理
參考:C++中的虛擬函式(表)實現機制以及用C語言對其進行的模擬實現
虛擬函式的本質是一個簡單的虛擬函式表。
當一個類存在虛擬函式時,通過該類建立的物件例項,會在記憶體空間的前4位元組儲存一個指向虛擬函式表的指標__vfptr
。
__vfptr
指向的虛擬函式表,是類獨有的,而且被該類的所有物件共享。虛擬函式表的實質,是一個虛擬函式地址的陣列,它包含了類中每個虛擬函式的地址,既有當前類定義的虛擬函式,也有覆蓋父類的虛擬函式,也有繼承而來的虛擬函式。
當子類覆蓋了父類的虛擬函式時,子類虛擬函式表將包含子類虛擬函式的地址,而不會有父類虛擬函式的地址。
同時,當用基類指標指向子類物件時,基類指標指向的記憶體空間中的__vfptr
依舊指向了子類的虛擬函式表。所以,基類指標依舊會呼叫子類的虛擬函式。
見如下示例:
4.1. 自己定義了虛擬函式的類
class Base1 {
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
定義兩個物件:
Base1 b1;
Base1 b2;
兩個物件的記憶體空間分配如下:
4.2. 既包含覆蓋虛擬函式,又包含繼承虛擬函式的類
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;
// 覆蓋基類函式
virtual void base1_fun1() {}
};
定義一個子類物件:
Derive1 d1;
其記憶體空間如下:
由圖可以看出:
Base1 *b_p = &d1; // 指向子類物件的基類指標
b_p->base1_fun1(); // 呼叫子類虛擬函式