最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

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

1.1 虛函數(shù)表(簡(jiǎn)稱虛表)

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

問(wèn):sizeof(Base) 是多少?

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

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

一個(gè)含有虛函數(shù)的類中都至少都有一個(gè)虛函數(shù)表指針,因?yàn)樘摵瘮?shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡(jiǎn)稱虛表。

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

  • 我們?cè)黾右粋€(gè)派生類Derive去繼承Base
  • Derive中重寫(xiě)Func1
  • Base再增加一個(gè)虛函數(shù)Func2和一個(gè)普通函數(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):

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

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

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

派生類的虛表生成:

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

1.3 多態(tài)的原理

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

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

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

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

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

如果是子類對(duì)象,我們知道:

派生類對(duì)象 可以賦值給 基類的對(duì)象 / 基類的指針 / 基類的引用。這里有個(gè)形象的說(shuō)法叫切片或者切割。寓意把派生類中父類那部分切來(lái)賦值過(guò)去。

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

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

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

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

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

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

需要注意的是在單繼承和多繼承關(guān)系中,下面我們?nèi)リP(guān)注的是派生類對(duì)象的虛表模型,因?yàn)榛惖奶摫砟P颓懊嫖覀円呀?jīng)看過(guò)了,沒(méi)什么需要特別研究的

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 對(duì)象的 func3 和 func4 呢?監(jiān)視窗口好像沒(méi)有,實(shí)際上不是虛表中沒(méi)有,而是監(jiān)視窗口沒(méi)有展示,它認(rèn)為不需要展示,就沒(méi)有顯示出來(lái)。

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

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

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ù)指針指向的這個(gè)函數(shù)
        }
        cout << endl;
}

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

        return 0;
}

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

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

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

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ù)指針指向的這個(gè)函數(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;
}

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

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

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

3. Q&A

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

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

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

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

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

  • 不依賴于任何類的實(shí)例(對(duì)象)
  • 可以直接通過(guò)類名調(diào)用(ClassName::StaticFunction()
  • 沒(méi)有 this 指針,無(wú)法訪問(wèn)對(duì)象的非靜態(tài)成員

虛函數(shù):

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

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

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

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

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

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

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

總結(jié)

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

相關(guān)文章

最新評(píng)論

乐业县| 安顺市| 准格尔旗| 水富县| 甘德县| 台山市| 齐齐哈尔市| 武邑县| 壤塘县| 古交市| 文登市| 黄陵县| 清河县| 新丰县| 甘谷县| 高碑店市| 鱼台县| 融水| 布尔津县| 颍上县| 巴南区| 昭觉县| 两当县| 德惠市| 崇文区| 黄骅市| 伊宁市| 那曲县| 若尔盖县| 含山县| 五华县| 晋州市| 永胜县| 临沭县| 公主岭市| 旬阳县| 嘉祥县| 开化县| 格尔木市| 阜宁县| 弥勒县|