欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++之虛函數(shù)與多態(tài)的實現(xiàn)原理分析

 更新時間:2025年09月18日 08:50:14   作者:一枝小雨  
文章講解了C++多態(tài)原理,通過虛函數(shù)表(虛表)實現(xiàn)動態(tài)綁定,探討了單繼承、多繼承及菱形繼承中的虛表結(jié)構(gòu),并指出虛函數(shù)不能是內(nèi)聯(lián)或靜態(tài)函數(shù),因需函數(shù)地址存儲于虛表

1.多態(tài)的原理

1.1 虛函數(shù)表(簡稱虛表)

class Base
{
public:
        virtual void Func1()
        {
                cout << "Func1()" << endl;
        }
private:
        int _b = 1;
};

問:sizeof(Base) 是多少?

  • 32位系統(tǒng)下,他是 8。
int main()
{
        Base a;
        cout << sizeof(a) << endl;    // 8
        return 0;
}

觀察監(jiān)視窗口,我們發(fā)現(xiàn)除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關(guān)),對象中的這個指針我們叫做虛函數(shù)表指針(一般是__vftptr,即virtual function table ptr)。

一個含有虛函數(shù)的類中都至少都有一個虛函數(shù)表指針,因為虛函數(shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡稱虛表。

針對上面的代碼我們做出以下改造:

  • 我們增加一個派生類Derive去繼承Base
  • Derive中重寫Func1
  • Base再增加一個虛函數(shù)Func2和一個普通函數(shù)Func3
class Base
{
public:
        virtual void Func1(){cout << "Base::Func1()" << endl;}
        virtual void Func2(){cout << "Base::Func2()" << endl;}
        void Func3(){cout << "Base::Func3()" << endl;}

private:
        int _b = 1;
};
class Derive : public Base
{
public:
        virtual void Func1(){cout << "Derive::Func1()" << endl;}

private:
        int _d = 2;
};

int main()
{
        Base b;
        Derive d;
        return 0;
}

觀察監(jiān)控窗口的信息,我們可以發(fā)現(xiàn):

  • 派生類對象d中也有一個虛表指針,d對象由兩部分構(gòu)成,一部分是父類繼承下來的成員(Base 部分),一類是自己的成員(_d)。虛表指針就是圖中的__vfptr。
  • 基類b對象和派生類d對象虛表是不一樣的,Func1 在 d 類中完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,通過對比可以發(fā)現(xiàn),d對象中的Func1的虛表指針和b對象中的Func1虛表指針指向地址不一樣。所以虛函數(shù)的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數(shù)的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
  • Func2繼承下來后是虛函數(shù),所以放進(jìn)了虛表,但是因為Func2沒有重寫覆蓋,所以b對象中的Func2的虛表地址和d對象的Func2虛表地址一致。父類中的Func3也繼承下來了,但是不是虛函數(shù),所以不會放進(jìn)虛表。

1.2 虛函數(shù)表本質(zhì)

虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況這個數(shù)組最后面放了一個 nullptr 標(biāo)志數(shù)組結(jié)束。

派生類的虛表生成:

  • 先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中。
  • 如果派生類重寫了基類中某個虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)。
  • 派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。
  • 注意:對象中存的是虛表指針,不是虛表本身。虛表中存的是虛函數(shù)指針,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣的,都是存在代碼段的,vs下虛表本身是存在代碼段(常量區(qū))的。
  • 同類型的對象共用一個虛表。

1.3 多態(tài)的原理

多態(tài)是如何實現(xiàn)指向誰就調(diào)用誰的虛函數(shù)的?

在運(yùn)行時,多態(tài)會到指向?qū)ο蟮奶摫碇胁檎乙{(diào)用的虛函數(shù)的地址,父類對象的虛表中的虛函數(shù)指針指向的是父類虛函數(shù),而子類對象的虛表中的虛函數(shù)指針指向的是子類重寫后的虛函數(shù)。

class Person {
public:
        virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:
        virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
void Func(Person& p)
{
        p.BuyTicket();
}
int main()
{
        Person Mike;
        Func(Mike);
        Student Johnson;
        Func(Johnson);
        return 0;
}

  • 當(dāng) p 是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數(shù)是Person::BuyTicket。
  • 當(dāng) p 是指向johnson對象時,p->BuyTicket在johson的虛表中找到虛函數(shù)是Student::BuyTicket。

也就是說,調(diào)用函數(shù)時,他其實并不知道自己要調(diào)用的是子類還是父類的虛函數(shù),他只需要到這個對象的虛表里面找就行了,找到是哪個就是哪個。如果是父類對象,那就直接通過虛表指針到虛表里找。

如果是子類對象,我們知道:

派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。

子類對象中的父類部分會被切割賦給 p,p 還是按照父類對象找虛函數(shù)的方式去虛表里找。這樣就實現(xiàn)了不同對象去完成同一行為時,展現(xiàn)出不同的形態(tài)。

PS:滿足多態(tài)以后的函數(shù)調(diào)用,不是在編譯時確定的,是運(yùn)行起來以后到指向?qū)ο蟮奶摵瘮?shù)表中查找對應(yīng)的虛函數(shù)的地址。不滿足多態(tài)的函數(shù)調(diào)用是編譯時直接確定的,通過 p 的類型確定要調(diào)用函數(shù)的地址。

1.4 動態(tài)綁定與靜態(tài)綁定

  1. 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為也稱為靜態(tài)多態(tài),比如:函數(shù)重載
  2. 動態(tài)綁定又稱后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動態(tài)多態(tài)
int i = 0;
double d = 1.1;
// 靜態(tài)綁定 靜態(tài)的多態(tài)(靜態(tài):編譯時確定函數(shù))
f1(i);
f1(d);

// 動態(tài)綁定 動態(tài)的多態(tài)(一般的多態(tài)指的就是動態(tài)多態(tài))(動態(tài):運(yùn)行時去虛表找函數(shù))
Base* p = new Base;
p->Func1();
p = new Derive;
p->Func1();

2. 單繼承和多繼承關(guān)系的虛函數(shù)表

需要注意的是在單繼承和多繼承關(guān)系中,下面我們?nèi)リP(guān)注的是派生類對象的虛表模型,因為基類的虛表模型前面我們已經(jīng)看過了,沒什么需要特別研究的

2.1 單繼承中的虛函數(shù)表

class Base {
public:
        virtual void func1() { cout << "Base::func1" << endl; }
        virtual void func2() { cout << "Base::func2" << endl; }
private:
        int a;
};
class Derive :public Base {
public:
        virtual void func1() { cout << "Derive::func1" << endl; }
        virtual void func3() { cout << "Derive::func3" << endl; }
        virtual void func4() { cout << "Derive::func4" << endl; }
private:
        int b;
};

int main()
{
        Base b;
        Derive d;

        return 0;
}

從監(jiān)視窗口我們可以看到虛表的內(nèi)容,但是我們 d 對象的 func3 和 func4 呢?監(jiān)視窗口好像沒有,實際上不是虛表中沒有,而是監(jiān)視窗口沒有展示,它認(rèn)為不需要展示,就沒有顯示出來。

我們可以自己打印虛表,看看到底有沒有 func3 和 func4 :

typedef void(*VF_PTR)();   // 函數(shù)指針類型重定義
                           // 定義完成后可以使用 VF_PTR p;來創(chuàng)建一個函數(shù)指針對象

void PrintVFTable(VF_PTR* pTable)
{
        for (size_t i = 0; pTable[i] != 0; ++i)
        {
                printf("vfTable[%d]:%p->", i, pTable[i]);
                VF_PTR f = pTable[i];
                f();        // 調(diào)用函數(shù)指針指向的這個函數(shù)
        }
        cout << endl;
}

int main()
{
        Base b;
        Derive d;
        // 取對象中前四個字節(jié)存的虛表指針打印虛表
        PrintVFTable((VF_PTR*)(*(int*)&b));
        PrintVFTable((VF_PTR*)(*(int*)&d));

        return 0;
}

事實證明 func3 和 func4 確實存在于虛表中,我們成功打印并且調(diào)用了。

2.2 多繼承中的虛函數(shù)表

typedef void(*VF_PTR)();        // 函數(shù)指針類型重定義
                                                        // 定義完成后可以使用 VF_PTR p;來創(chuàng)建一個函數(shù)指針對象

void PrintVFTable(VF_PTR pTable[])
{
        for (size_t i = 0; pTable[i] != 0; ++i)
        {
                printf("vfTable[%d]:%p->", i, pTable[i]);
                VF_PTR f = pTable[i];
                f();        // 調(diào)用函數(shù)指針指向的這個函數(shù)
        }
        cout << endl;
}

class Base1 {
public:
        virtual void func1() { cout << "Base1::func1" << endl; }
        virtual void func2() { cout << "Base1::func2" << endl; }
private:
        int b1;
};
class Base2 {
public:
        virtual void func1() { cout << "Base2::func1" << endl; }
        virtual void func2() { cout << "Base2::func2" << endl; }
private:
        int b2;
};
class Derive : public Base1, public Base2 {
public:
        virtual void func1() { cout << "Derive::func1" << endl; }
        virtual void func3() { cout << "Derive::func3" << endl; }
private:
        int d1;
};

int main()
{
        // base1虛表4 + int4 + base2虛表4 + int4 + int4 = 20字節(jié)
        cout << sizeof(Derive) << endl;    // 20
        
        Derive d;

        // base1 的虛表
        PrintVFTable((VF_PTR*)(*(int*)&d));
        // base2 的虛表
        PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));

        return 0;
}

  • 這說明子類 d 對象的虛函數(shù) func3 是往第一個繼承的父類 base1 的虛表里放的。
  • 另外也說明了先繼承的父類,它的虛表放在前面(即低地址處)。

2.3 菱形繼承、菱形虛擬繼承

實際中不建議設(shè)計出菱形繼承及菱形虛擬繼承,一方面太復(fù)雜容易出問題,另一方面這樣的模型,訪問基類成員有一定得性能損耗。

3. Q&A

3.1 內(nèi)聯(lián)函數(shù)為什么不能是虛函數(shù)?

內(nèi)聯(lián)函數(shù)會在調(diào)用位置直接展開,所以內(nèi)聯(lián)函數(shù)沒有地址,也不需要地址,沒有函數(shù)地址就無法放入虛表,所以內(nèi)聯(lián)函數(shù)不能是虛函數(shù)。

3.2 靜態(tài)函數(shù)為什么不能是虛函數(shù)?

1. 調(diào)用方式與對象綁定的根本差異

靜態(tài)函數(shù):

  • 不依賴于任何類的實例(對象)
  • 可以直接通過類名調(diào)用(ClassName::StaticFunction()
  • 沒有 this 指針,無法訪問對象的非靜態(tài)成員

虛函數(shù):

  • 完全依賴于類的實例(對象)
  • 必須通過對象或?qū)ο笾羔樥{(diào)用
  • this 指針,可以訪問對象的非靜態(tài)成員
  • 通過虛函數(shù)表(vTable)實現(xiàn)動態(tài)綁定,而vTable是每個對象實例的一部分

2. 虛函數(shù)機(jī)制依賴于對象實例

虛函數(shù)的實現(xiàn)依賴于:

  • 每個對象內(nèi)部的虛函數(shù)表指針(vPtr)
  • 通過vPtr在運(yùn)行時查找正確的函數(shù)實現(xiàn)

靜態(tài)函數(shù)沒有this指針,因此無法訪問對象的vPtr,也就無法實現(xiàn)動態(tài)綁定。如果靜態(tài)函數(shù)是虛的,編譯器無法知道應(yīng)該使用哪個類的虛函數(shù)表。

3.3 虛表?虛基表?虛基類?

  • 虛表是虛函數(shù)表,存儲的是虛函數(shù)指針,是一個函數(shù)指針數(shù)組。
  • 虛基表存儲的是偏移量,是解決菱形繼承的數(shù)據(jù)冗余和二義性問題的。
  • 虛基類是在繼承中給父類前面加 virtual 關(guān)鍵字,是為了解決菱形繼承的數(shù)據(jù)冗余和二義性問題而存在的。

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • C語言中變量與其內(nèi)存地址對應(yīng)的入門知識簡單講解

    C語言中變量與其內(nèi)存地址對應(yīng)的入門知識簡單講解

    這篇文章主要介紹了C語言中變量與其內(nèi)存地址對應(yīng)的入門知識簡單講解,同時這也是掌握指針部分知識的基礎(chǔ),需要的朋友可以參考下
    2015-12-12
  • C++11如何引入的尾置返回類型

    C++11如何引入的尾置返回類型

    C++11 標(biāo)準(zhǔn)引入的尾置返回類型,可以讓返回復(fù)雜類型的函數(shù)聲明更加清晰易讀,在無法使用C++14 標(biāo)準(zhǔn)的情況下,通過尾置返回類型的語法來推導(dǎo)函數(shù)模板的返回類型無疑是最簡便的方法,這篇文章主要介紹了C++11引入的尾置返回類型,需要的朋友可以參考下
    2023-01-01
  • C/C++編程語言中的指針(pointer)你了解嗎

    C/C++編程語言中的指針(pointer)你了解嗎

    這篇文章主要為大家詳細(xì)介紹了C/C++編程語言中的指針,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-02-02
  • 淺談C++11新引入的lambda表達(dá)式

    淺談C++11新引入的lambda表達(dá)式

    Lambda表達(dá)式(又稱Lambda函數(shù),英文原文是Lambda Expression),是C++11的新特性中非常實用的一個。
    2017-07-07
  • Visual Studio 2019下配置 CUDA 10.1 + TensorFlow-GPU 1.14.0

    Visual Studio 2019下配置 CUDA 10.1 + TensorFlow-GPU 1.14.0

    這篇文章主要介紹了Visual Studio 2019下配置 CUDA 10.1 + TensorFlow-GPU 1.14.0,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • C語言實現(xiàn)文本編輯器系統(tǒng)

    C語言實現(xiàn)文本編輯器系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了C語言實現(xiàn)文本編輯器系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-02-02
  • C語言實現(xiàn)BMP格式圖片轉(zhuǎn)化為灰度

    C語言實現(xiàn)BMP格式圖片轉(zhuǎn)化為灰度

    這篇文章主要為大家詳細(xì)介紹了C語言實現(xiàn)BMP格式圖片轉(zhuǎn)化為灰度,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-10-10
  • C++中volatile限定符的實現(xiàn)示例

    C++中volatile限定符的實現(xiàn)示例

    volatile關(guān)鍵字在C和C++中用于確保編譯器不優(yōu)化特定變量的訪問,主要用于多線程和硬件交互場景,本文就來介紹C++中volatile限定符的實現(xiàn)示例,感興趣的可以了解一下
    2024-11-11
  • C語言數(shù)據(jù)結(jié)構(gòu)之單向鏈表詳解

    C語言數(shù)據(jù)結(jié)構(gòu)之單向鏈表詳解

    單向鏈表(單鏈表)是鏈表的一種,其特點是鏈表的鏈接方向是單向的,對鏈表的訪問要通過順序讀取從頭部開始。本文將為大家詳細(xì)講講單向鏈表的實現(xiàn)與使用,需要的可以參考一下
    2022-08-08
  • C++設(shè)置超時時間的簡單實現(xiàn)方法

    C++設(shè)置超時時間的簡單實現(xiàn)方法

    這篇文章主要介紹了C++設(shè)置超時時間的簡單實現(xiàn)方法,涉及系統(tǒng)函數(shù)setsockopt對套接口的操作,具有一定的實用價值,需要的朋友可以參考下
    2014-10-10

最新評論