1. 程式人生 > 其它 >C++虛擬函式

C++虛擬函式

技術標籤:程式設計基礎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;

兩個物件的記憶體空間分配如下:
1.png

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;

其記憶體空間如下:

2.png

由圖可以看出:

Base1 *b_p = &d1;     // 指向子類物件的基類指標
b_p->base1_fun1();    // 呼叫子類虛擬函式