文章目录
虚函数指针与虚函数表
- 虚函数指针 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 是指向对象的指针,p->vptr 是虚函数指针,vtbl 中的第 n 个元素即为需要调用的虚函数地址。(* (p->vptr)[n])(p)
4. 虚函数机制与继承
-
继承与重写:
- 当子类继承父类时,子类对象包含了父类的部分。如果父类含有虚函数,子类会继承这些虚函数的调用权。
- 子类可以选择重写(override)父类的虚函数,此时其 vtbl 中相应的虚函数地址会指向子类的实现。
-
共享与独立:
- 同一类型的所有对象共享同一个 vtbl。
- 当存在继承关系时,如果子类没有重写父类的虚函数,则子类对象的 vptr 会指向父类的 vtbl;如果子类重写了某些虚函数,则编译器会为子类生成一个新的 vtbl,这个表中部分条目会替换为子类的实现地址。
5. 虚函数调用过程
-
对象创建:
当创建一个包含虚函数的对象时,编译器自动将 vptr 设置为指向该类对应的 vtbl。 -
调用虚函数:
当调用虚函数时,程序首先通过对象的 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. 代码说明
-
模板方法模式:
- 基类
CDocument
定义了一个算法骨架OnFileOpen
,其中包含了对纯虚函数Serialize
的调用。 Serialize
是一个虚函数,由派生类CMyDoc
实现具体逻辑。
- 基类
-
动态绑定:
- 在
main
函数中,调用myDoc.OnFileOpen()
时,OnFileOpen
内部的Serialize
会根据实际对象类型动态绑定到CMyDoc::Serialize
。
- 在
-
代码结构:
- 基类提供通用逻辑,派生类实现具体行为,体现了模板方法模式的核心思想。
动态绑定
1. 静态绑定分析(对象调用)
代码场景
B b;
A a = (A)b; // 将b强制向上转型为A类型对象
a.vfunc1(); // 通过对象直接调用虚函数
a.vfunc1()
这是通过对象来调用(将B转成A类对象),是静态的调用。可以看到右边的汇编代码,调用call
来执行(固定地址)。
关键机制
- 对象切片:
A a = (A)b
会触发对象切片,仅保留基类A的成员数据,丢失B的派生类信息。 - 静态绑定特征:
- 汇编指令表现为
call @ILT+420(A::vfunc1) (004011a9)
call
后接固定地址(如004011a9
),直接定位到基类A的虚函数实现
- 汇编指令表现为
- 本质原因:
- 通过对象(非指针/引用)调用虚函数时,编译器在编译期即可确定具体函数地址,无需运行时多态。
2. 动态绑定分析(指针调用)
代码场景
A* pa = new B; // 向上转型,pa指向B类型对象
pa->vfunc1(); // 通过基类指针调用虚函数
这里是使用动态绑定调用。首先向上转型,new B
的指针是A*
,下面用指针调用函数,是动态的。从右边的汇编中可以看到,调用函数的时候,call
的是dword
ptr[edx]
,即是vtbl
中对应虚函数的位置。
动态绑定条件
- 通过指针或引用调用
- 调用的是虚函数
- 存在向上转型关系(基类指针指向派生类对象)
底层实现过程
- 虚表指针访问:
mov eax, [pa] ; 获取对象头部的虚表指针(vptr) mov edx, [eax] ; 从虚表(vtable)中取第一个条目(即vfunc1地址) call edx ; 通过函数指针调用
- 虚表结构:
虚表偏移 对应函数 0 B::vfunc1() 4 其他虚函数… - this指针传递:调用时隐含传递
pa
作为this
指针,确保访问正确的对象数据。
3. 核心对比
特性 | 静态绑定 | 动态绑定 |
---|---|---|
调用方式 | 通过对象调用 | 通过指针/引用调用 |
绑定时机 | 编译期确定函数地址 | 运行期通过虚表查找函数地址 |
汇编特征 | call 固定地址 | call 寄存器/内存地址 |
对象完整性 | 可能发生对象切片 | 保持完整派生类对象信息 |
性能开销 | 无额外开销 | 需虚表查询,存在微小运行时开销 |
4. 虚函数机制的意义
通过虚表指针与虚表的分层结构,C++实现:
- 运行时多态:派生类可覆盖基类行为,相同接口不同实现
- 对象自描述:每个对象携带虚表指针,明确自身类型信息
- 扩展性:新增派生类无需修改基类调用逻辑