1. 深入探索C++虚函数:从编译器视角看多态的"幕后魔法"
在C++编程中,虚函数是实现运行时多态的核心机制。很多开发者虽然知道如何使用虚函数,但对底层实现原理却知之甚少。今天,我们就来彻底剖析虚函数的工作原理,理解编译器是如何实现这一重要特性的。
1.1 一个奇怪的现象:类的大小变了?
让我们从一个简单的实验开始。考虑以下两个类:
cpp复制class A {
public:
void func() { cout << "A::func" << endl; }
};
class B {
public:
virtual void func() { cout << "B::func" << endl; }
};
在64位系统上,sizeof(A)的结果是1,而sizeof(B)的结果是8。这个差异揭示了虚函数的一个重要特性:当类中包含虚函数时,编译器会自动在对象中添加一个隐藏的指针成员——虚表指针(vptr)。
注意:类A的大小为1是因为C++规定空类的大小不能为0,需要至少1字节来确保每个对象都有唯一的地址。而类B的大小为8(64位系统)是因为添加了一个指针成员。
1.2 揭秘核心机制:虚函数表(vtable)与虚表指针(vptr)
1.2.1 虚函数表(vtable)的结构与作用
虚函数表是编译器为每个包含虚函数的类生成的一个静态数组,其中存储了该类所有虚函数的地址。这个表有以下特点:
- 每个类只有一个虚函数表,该类的所有对象共享同一个表
- 表的内容在编译时确定
- 通常存储在程序的只读数据段(.rodata)中
例如,对于以下类:
cpp复制class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
其虚函数表可能如下所示:
code复制Base的vtable:
+----------------+
| &Base::func1 |
+----------------+
| &Base::func2 |
+----------------+
1.2.2 虚表指针(vptr)的作用机制
每个包含虚函数的对象在内存布局的最前面(具体位置取决于编译器实现)都有一个隐藏的指针成员,指向该类的虚函数表。这个指针就是vptr。
对象的内存布局示意:
code复制+----------------+
| vptr | ---> 指向类的vtable
+----------------+
| 其他数据成员 |
+----------------+
当通过基类指针调用虚函数时,编译器会生成代码通过vptr查找并调用正确的函数实现。
1.3 动态绑定的全过程
考虑以下代码:
cpp复制Base* ptr = new Derived();
ptr->func();
这个调用实际上经历了以下步骤:
- 通过ptr获取对象的首地址
- 从对象首地址读取vptr
- 通过vptr找到Derived类的vtable
- 从vtable中获取func()的地址
- 跳转到该地址执行函数
这个过程就是所谓的"动态绑定"或"晚期绑定",它使得程序能够在运行时确定要调用的具体函数。
1.4 继承中的虚表变换
当派生类重写基类的虚函数时,虚函数表会发生有趣的变化。考虑以下继承关系:
cpp复制class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
class Derived : public Base {
public:
virtual void func1() override {}
};
编译器为Derived类生成虚函数表时,会:
- 复制Base类的虚函数表
- 用Derived::func1替换表中的Base::func1
- 保持Base::func2不变
因此,Derived的虚函数表如下:
code复制Derived的vtable:
+----------------+
| &Derived::func1|
+----------------+
| &Ba
