多态场景
多态这个词能看到这个博客的肯定都很熟了,这边来回顾下。
class Base {
public:
virtual void func1() { /* Base's func1 */ }
virtual void func2() { /* Base's func2 */ }
int base_data;
};
class Derived : public Base {
public:
void func1() override { /* Derived's func1 */ } // 重写 func1
virtual void func3() { /* Derived's func3 */ } // 新的虚函数
int derived_data;
};
虚函数表 (vtable):
• 位置: 只读数据段 (.rodata) 或类似的只读内存区域。
• 性质: vtable是类级别的元数据。同一个类的所有对象共享同一个 vtable。
• 内容: 包含该类所有虚函数的函数指针(地址)。这些地址在编译时或程序加载时就已经确定。
• 可写性: 通常是只读的。程序运行时不应该修改 vtable的内容,否则会导致未定义行为。编译器将其放在 .rodata段就是为了保证这一点。
虚函数表指针 (vptr):
• 位置: 对象实例内部。具体位置取决于对象实例本身存储在内存的哪个区域。
• 如果对象是全局/静态对象,vptr就在全局/静态数据区。
• 如果对象是局部自动对象 (栈上),vptr就在栈 (stack) 上。
• 如果对象是动态分配的对象 (堆上),vptr就在堆 (heap) 上。
• 性质: vptr是对象实例级别的数据。每个具有虚函数的对象实例都拥有自己的 vptr。
• 内容: 存储一个指针,指向该对象所属类的 vtable。
• 可写性: 可写。在对象的构造和析构过程中,vptr会被修改(例如,在构造子类对象时,先指向父类 vtable,再指向子类 vtable)。这就是为什么它不能放在只读段。
代码段 (.text):
• 位置: 存放程序执行的机器指令(即函数体的实际代码)。
• 与虚函数的关系: vtable中存储的函数指针,最终指向的就是位于 .text 段 中的这些虚函数的具体实现代码。
关于父类和子类的虚表:
• 父类: 如果父类声明了虚函数(或继承了虚函数),编译器会为父类生成一个独立的 vtable。这个 vtable存储在 .rodata段。
• 子类: 编译器也会为子类生成一个独立的 vtable,同样存储在 .rodata段。
• 子类的 vtable通常是在父类 vtable的基础上扩展而来:
• 前 N 个条目(N 是父类虚函数的数量)对应继承自父类的虚函数。
• 如果子类重写了某个父类虚函数,则子类 vtable中对应的条目指向子类的实现。
• 如果子类没有重写,则子类 vtable中对应的条目指向父类的实现。
• 之后的条目对应子类自己新声明的虚函数。
• 指针指向:
• 所有父类对象的 vptr都指向父类的 vtable(位于 .rodata)。
• 所有子类对象的 vptr都指向子类的 vtable(位于 .rodata)。
• 当父类指针指向子类对象时,它访问到的 vptr是子类对象内部的 vptr,而这个 vptr指向的是子类的 vtable。这就是多态调用的关键!
总结内存布局图
例子 Base* ptr = new Derived();
内存区域 | 内容
----------------|-------------------------------------------------------
.text (代码段) | &Base::func1 (代码) &Base::func2 (代码)
| &Derived::func1 (代码) &Derived::func3 (代码)
----------------|-------------------------------------------------------
.rodata (只读数据段)| Base's vtable: [ &Base::func1, &Base::func2 ]
| Derived's vtable: [ &Derived::func1, &Base::func2, &Derived::func3 ]
----------------|-------------------------------------------------------
Heap (堆) | Derived 对象实例:
| +---------------------+
| | vptr -------------->|----> (指向 .rodata 中的 Derived's vtable)
| +---------------------+
| | base_data (int) | <-- Base 子对象部分 (ptr 指向这里)
| +---------------------+
| | derived_data (int) | <-- Derived 特有部分
| +---------------------+
----------------|-------------------------------------------------------
Stack (栈) | Base* ptr = address_of_above_Derived_object
调用过程 ptr->func1();的底层步骤 (细化内存访问):
-
ptr的值: 存储在栈上的指针变量 ptr保存着堆上 Derived对象中 Base子对象的起始地址。
-
获取 vptr:
• 通过 ptr的值找到堆上的对象内存。
• 编译器知道 vptr在对象布局中的偏移量(通常是对象起始位置)。读取这个内存位置的值。这个值就是 vptr,它指向 .rodata段中的 Derived’s vtable。
- 定位 vtable条目:
• 编译器知道 func1在 vtable中的索引(比如索引 0,由编译器在编译类定义时确定)。
• 计算目标条目的地址:vptr + (index * sizeof(function_pointer))。
• 读取 .rodata段中这个地址处的值。这个值就是 &Derived::func1(一个指向 .text段中代码的指针)。
- 调用函数:
• 处理器跳转到上一步取到的函数指针 (&Derived::func1) 所指向的地址,该地址位于 .text段。
• 执行 Derived::func1()的机器指令。
整个过程的核心在于:通过对象内部的 vptr(存储在堆/栈/全局区)找到类级别的 vtable(存储在只读段),再通过 vtable找到函数代码(存储在代码段),最终实现运行时动态绑定。