【C++】面向对象高级开发 | 对象模型:虚函数指针与虚函数表、动态绑定

虚函数指针与虚函数表

  • 虚函数指针 vptr 是每个含有虚函数的对象的隐式成员,它指向存储虚函数地址的 vtbl
  • 虚函数表 vtbl 是一个类级别的表格,记录了所有虚函数的地址,实现了多态和动态绑定。
  • 继承关系中,子类会继承父类的虚函数调用权,并可以通过重写虚函数更新 vtbl 中的条目。
  • 无论类中虚函数数量多少,每个对象只增加一个 vptr 指针,节省了内存开销,同时支持高效的运行时动态函数调用。

这种机制使得 C++ 能够在运行时根据对象的实际类型调用合适的函数实现,是面向对象编程中实现多态的重要手段。
在这里插入图片描述


1. 虚函数的引入与目的

  • 虚函数:C++ 允许在基类中声明虚函数,以实现多态(polymorphism)。多态的核心在于运行时能够根据实际对象类型调用正确的函数实现,而非仅根据静态类型决定。
  • 动态绑定:虚函数机制实现了函数调用的动态绑定(runtime binding),即在运行时决定调用哪个函数,而不是在编译时静态绑定。

2. vptr(虚函数指针)

  • 概念
    当一个类中存在至少一个虚函数时,编译器会在该类的对象中隐式地加入一个指针成员,这个指针称为 vptr
  • 作用
    • vptr 指向当前对象对应的虚函数表(vtbl)。
    • 每个对象都有自己的 vptr,但所有同一类型的对象通常共享同一个 vtbl。
  • 内存占用
    • 无论该类中定义了多少个虚函数,对象中只会增加一个 vptr(在 32 位系统上一般为 4 字节,在 64 位系统上则可能为 8 字节)。

3. vtbl(虚函数表)

  • 概念
    vtbl 是一个存储虚函数地址的数组(或表格),它记录了类中所有虚函数的入口地址。
  • 作用
    • 当程序调用虚函数时,通过对象的 vptr 定位到对应的 vtbl,再从表中取出相应的函数地址,从而实现动态调用。
    • 例如,调用过程可以描述为:
      (* (p->vptr)[n])(p)
      
      其中,p 是指向对象的指针,p->vptr 是虚函数指针,vtbl 中的第 n 个元素即为需要调用的虚函数地址。

4. 虚函数机制与继承

  • 继承与重写

    • 当子类继承父类时,子类对象包含了父类的部分。如果父类含有虚函数,子类会继承这些虚函数的调用权。
    • 子类可以选择重写(override)父类的虚函数,此时其 vtbl 中相应的虚函数地址会指向子类的实现。
  • 共享与独立

    • 同一类型的所有对象共享同一个 vtbl。
    • 当存在继承关系时,如果子类没有重写父类的虚函数,则子类对象的 vptr 会指向父类的 vtbl;如果子类重写了某些虚函数,则编译器会为子类生成一个新的 vtbl,这个表中部分条目会替换为子类的实现地址。

5. 虚函数调用过程

  1. 对象创建
    当创建一个包含虚函数的对象时,编译器自动将 vptr 设置为指向该类对应的 vtbl。

  2. 调用虚函数
    当调用虚函数时,程序首先通过对象的 vptr 定位到虚函数表 vtble,然后根据虚函数在表中的位置(索引)取出实际函数地址,再通过这个地址进行函数调用。

    • 这种机制称为“动态绑定”或“晚绑定”,因为实际调用哪个函数要等到运行时才能确定。

this 指针

在这里插入图片描述

1. 基类 CDocument

class CDocument {
public:
    virtual void Serialize() = 0; // 纯虚函数,由派生类实现
    void OnFileOpen() {
        // OnFileOpen的通用逻辑部分
        // 调用Serialize方法,动态绑定到派生类的实现
        Serialize();
    }
};

父类中其他可以通用,读文件这个函数Serialize设置为虚函数,需要override

2. 派生类 CMyDoc

class CMyDoc : public CDocument {
public:
    virtual void Serialize() {
        // CMyDoc中具体的Serialize逻辑实现
        // 例如保存数据到文件等操作
        // ...
    }
};

我们定义一个读文档的类,那么serialize函数就要override成读文档的函数。

3. 主函数 main

int main() {
    CMyDoc myDoc; // 创建CMyDoc对象
    myDoc.OnFileOpen(); // 调用OnFileOpen方法,触发动态绑定的Serialize
    return 0;
}

调用serialize时,通过隐藏的this pointer来调用,因为myDoc.OnFileOpen,因此this就是myDoc(这里就是上面说的动态绑定,this指向的serialize,是重载过的虚函数),因此调用的是我们override之后的serialize函数。
这就是设计模式,模板方法

4. 代码说明

  1. 模板方法模式

    • 基类 CDocument 定义了一个算法骨架 OnFileOpen,其中包含了对纯虚函数 Serialize 的调用。
    • Serialize 是一个虚函数,由派生类 CMyDoc 实现具体逻辑。
  2. 动态绑定

    • main 函数中,调用 myDoc.OnFileOpen() 时,OnFileOpen 内部的 Serialize 会根据实际对象类型动态绑定到 CMyDoc::Serialize
  3. 代码结构

    • 基类提供通用逻辑,派生类实现具体行为,体现了模板方法模式的核心思想。

动态绑定

1. 静态绑定分析(对象调用)

在这里插入图片描述
代码场景

B b;
A a = (A)b;    // 将b强制向上转型为A类型对象
a.vfunc1();    // 通过对象直接调用虚函数

a.vfunc1()这是通过对象来调用(将B转成A类对象),是静态的调用。可以看到右边的汇编代码,调用call来执行(固定地址)。

关键机制

  1. 对象切片A a = (A)b 会触发对象切片,仅保留基类A的成员数据,丢失B的派生类信息。
  2. 静态绑定特征
    • 汇编指令表现为 call @ILT+420(A::vfunc1) (004011a9)
    • call后接固定地址(如004011a9),直接定位到基类A的虚函数实现
  3. 本质原因
    • 通过对象(非指针/引用)调用虚函数时,编译器在编译期即可确定具体函数地址,无需运行时多态。

2. 动态绑定分析(指针调用)

在这里插入图片描述
代码场景

A* pa = new B;  // 向上转型,pa指向B类型对象
pa->vfunc1();    // 通过基类指针调用虚函数

这里是使用动态绑定调用。首先向上转型,new B的指针是A*,下面用指针调用函数,是动态的。从右边的汇编中可以看到,调用函数的时候,call的是dword ptr[edx],即是vtbl中对应虚函数的位置。

动态绑定条件

  1. 通过指针或引用调用
  2. 调用的是虚函数
  3. 存在向上转型关系(基类指针指向派生类对象)

底层实现过程

  1. 虚表指针访问
    mov eax, [pa]          ; 获取对象头部的虚表指针(vptr)
    mov edx, [eax]         ; 从虚表(vtable)中取第一个条目(即vfunc1地址)
    call edx               ; 通过函数指针调用
    
  2. 虚表结构
    虚表偏移对应函数
    0B::vfunc1()
    4其他虚函数…
  3. this指针传递:调用时隐含传递pa作为this指针,确保访问正确的对象数据。

3. 核心对比

特性静态绑定动态绑定
调用方式通过对象调用通过指针/引用调用
绑定时机编译期确定函数地址运行期通过虚表查找函数地址
汇编特征call 固定地址call 寄存器/内存地址
对象完整性可能发生对象切片保持完整派生类对象信息
性能开销无额外开销需虚表查询,存在微小运行时开销

4. 虚函数机制的意义

通过虚表指针与虚表的分层结构,C++实现:

  1. 运行时多态:派生类可覆盖基类行为,相同接口不同实现
  2. 对象自描述:每个对象携带虚表指针,明确自身类型信息
  3. 扩展性:新增派生类无需修改基类调用逻辑
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清流君

感恩有您,共创未来,愿美好常伴

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值