虚函数机制详解

只有通过指针/引用间接访问时,才能保持动态类型。

1. 虚表指针(vptr)的本质

  • 每个包含虚函数的类有自己的虚函数表(vtable)
  • 每个对象实例在内存开头有一个隐藏的vptr指针,指向一个虚函数表,在对象创建时由构造函数设置

2. 虚函数表的本质

  • 编译期创建:虚函数表是编译器在编译阶段为每个包含虚函数的类生成的静态数据
  • 全局唯一:每个类只有一个虚函数表,存放在程序的只读数据段
    1
    &Base::vtable # 获取虚函数表地址

RTTI 指针

  • 指向 type_info 结构
  • 包含类型名、继承关系等信息
  • 用于 typeiddynamic_cast

虚函数指针

  • 按声明顺序排列
  • 覆盖函数替换基类槽位
  • 纯虚函数通常指向pure_virtual_called(终止程序)

偏移量条目(多重继承)

  • 用于 this 指针调整,
  • 格式:{offset, 0}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Derived的Base1虚表:
┌───────────────┐
│ RTTI (Derived)│
├───────────────┤
│ &Derived::f1 │ // 覆盖Base1::f1
├───────────────┤
│ &Derived::fd │ // 新增函数放入第一个基类虚表
└───────────────┘

Derived的Base2虚表:
┌───────────────┐
│ 偏移量 │ → 到完整对象的偏移(通常-8或-12)
├───────────────┤
│ RTTI (Derived)│
├───────────────┤
│ &Derived::f2 │ // 覆盖Base2::f2
└───────────────┘
1
2
3
4
5
6
7
8
9
10
11
内存地址: 0x402000
┌──────────────┬─────────────────────────┬──────────────────────────────┐
│ 偏移量(字节) │ 内容 (8字节) │ 说明 │
├──────────────┼─────────────────────────┼──────────────────────────────┤
│ 0x00 │ 0x403100 (RTTI) │ 指向Derived的type_info │
│ 0x08 │ 0x402100 (&Derived::func1)│ 覆盖Base::func1的地址 │
│ 0x10 │ 0x401200 (&Base::func2) │ 继承Base::func2的地址 │
│ 0x18 │ 0x402300 (~Derived完整析构)│ 完整析构函数地址 │
│ 0x20 │ 0x402350 (~Derived基析构)│ 基本析构函数地址 │
│ 0x28 │ 0x402400 (&Derived::func3)│ 新增虚函数func3的地址 │
└──────────────┴─────────────────────────┴──────────────────────────────┘

3. 构造链中的vptr变化

考虑继承关系:Child : public Child1, public Child2

构造Child对象时的vptr变化:

Child* obj = new Child();

  1. 分配内存:获得一块未初始化的内存
  2. 构造最基类
1
2
3
4
// 编译器插入:构造Base部分
Base::Base(obj + Child1_offset) {
this->vptr = &Base::__vtable; // (1) 初始指向Base虚表
}
  1. 构造Child1部分
1
2
3
4
5
Child1::Child1(obj + Child1_offset) {
// 先调用Base构造(已执行)
this->vptr = &Child1::__vtable; // (2) 覆盖为Child1虚表
b = -1;
}
  1. 构造Child2部分

  2. 构造Child自身

1
2
3
4
5
6
Child::Child(obj) {
// 先调用Child1/Child2构造(已执行)
this->vptr = &Child::__vtable; // (3) 最终指向Child虚表
d = 1;
}

4.拷贝构造与赋值运算符

拷贝构造函数

1
2
3
4
5
6
7
8
9
10
// 编译器生成的拷贝构造函数伪代码
Base::Base(const Base& src) {
// STEP 1: 初始化vptr (构造函数特权!)
this->vptr = &Base::vtable; // ← 注意这里!

// STEP 2: 复制成员数据
this->x = src.x;

// STEP 3: 不处理派生类额外数据!
}
  • 致命细节:拷贝构造函数作为构造函数,必须初始化vptr
  • 它使用当前类的vtable(Base的vtable),而不是源对象的vtable

赋值构造

1
2
3
4
5
6
7
8
9
10
// 编译器生成的赋值运算符伪代码
Base& Base::operator=(const Base& src) {
// STEP 1: 复制成员数据
this->x = src.x;

// STEP 2: 明确禁止修改vptr!
// this->vptr = ... ← 标准禁止此操作!

return *this;
}
  • 赋值操作发生时,对象已完全构造
  • C++标准强制规定:赋值操作不能改变对象的动态类型

4. 内存复制真相

当复制派生类对象时:
派生类对象d的内存布局:

1
2
3
4
5
6
7
┌───────────────┐
│ vptr │ → 指向Derived::vtable
├───────────────┤
│ Base::x │
├───────────────┤
│ Derived::y │ ← 派生类特有成员
└───────────────┘
拷贝构造过程 Base b1(d)
  1. 分配新内存给b1
  2. 初始化b1的vptr:设置为&Base::vtable (重要!)
  3. 仅复制d的Base部分数据:
1
2
3
4
5
6
b1内存布局:
┌───────────────┐
│ vptr │ → 指向Base::vtable (新建的!)
├───────────────┤
│ Base::x │ ← 从d复制过来
└───────────────┘
赋值操作过程 b2 = d
  1. b2已存在,内存固定:

    1
    2
    3
    4
    5
    6
    b2原始布局:
    ┌───────────────┐
    │ vptr │ → 指向Base::vtable
    ├───────────────┤
    │ Base::x │
    └───────────────┘
  2. 复制d的Base部分数据到b2
    b2赋值后:

    1
    2
    3
    4
    5
    ┌───────────────┐
    │ vptr │ → 仍然指向Base::vtable (不变!)
    ├───────────────┤
    │ Base::x │ ← 更新为d.x的值
    └───────────────┘

为什么

C++的类型分为动态类型和静态类型,虚函数表中type_info决定类C++的动态的类型,如果使用赋值运算符时复制了虚函数指针,就会导致对象的动态类型转变,违反了类型安全的原则,破坏C++类型系统

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2023-2025 John Doe
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信