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

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放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數(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ù),所以放進了虛表,但是因為Func2沒有重寫覆蓋,所以b對象中的Func2的虛表地址和d對象的Func2虛表地址一致。父類中的Func3也繼承下來了,但是不是虛函數(shù),所以不會放進虛表。

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

虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況這個數(shù)組最后面放了一個 nullptr 標志數(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ù)的?

在運行時,多態(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;
}

  • 當 p 是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數(shù)是Person::BuyTicket。
  • 當 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)用,不是在編譯時確定的,是運行起來以后到指向?qū)ο蟮奶摵瘮?shù)表中查找對應的虛函數(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)綁定又稱后期綁定(晚綁定),是在程序運行期間,根據(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):運行時去虛表找函數(shù))
Base* p = new Base;
p->Func1();
p = new Derive;
p->Func1();

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

需要注意的是在單繼承和多繼承關系中,下面我們?nèi)リP注的是派生類對象的虛表模型,因為基類的虛表模型前面我們已經(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)視窗口沒有展示,它認為不需要展示,就沒有顯示出來。

我們可以自己打印虛表,看看到底有沒有 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 菱形繼承、菱形虛擬繼承

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

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ù)機制依賴于對象實例

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

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

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

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

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

總結(jié)

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

相關文章

最新評論

安新县| 武城县| 墨脱县| 库伦旗| 弥勒县| 辽阳县| 财经| 嵩明县| 碌曲县| 太保市| 城市| 五河县| 钦州市| 宜城市| 永康市| 吴桥县| 都兰县| 西乌珠穆沁旗| 新晃| 辉南县| 闸北区| 宜章县| 武川县| 缙云县| 仙桃市| 康马县| 且末县| 周至县| 师宗县| 乐都县| 阳东县| 鸡泽县| 庆安县| 柏乡县| 乌兰察布市| 淮南市| 宁陕县| 阿拉善左旗| 内丘县| 庆云县| 罗田县|