虚函数原理
1. 虚函数表(Virtual Table)
什么是虚函数表?
虚函数表(vtable) 是C++编译器为每个包含虚函数的类创建的一个静态数组,用于存储该类所有虚函数的地址。这是实现动态多态的关键机制。
虚函数表的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
virtual void func1() { std::cout << "Base::func1\n"; }
virtual void func2() { std::cout << "Base::func2\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void func1() override { std::cout << "Derived::func1\n"; }
// func2 继承自 Base,没有重写
virtual void func3() { std::cout << "Derived::func3\n"; }
};
vtable 结构示意:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Base类的vtable:
+-------------------+
| Base::func1 | <- vtable[0]
| Base::func2 | <- vtable[1]
| Base::~Base | <- vtable[2]
+-------------------+
Derived类的vtable:
+-------------------+
| Derived::func1 | <- vtable[0] (重写了Base::func1)
| Base::func2 | <- vtable[1] (继承Base::func2)
| Derived::~Derived | <- vtable[2] (重写析构函数)
| Derived::func3 | <- vtable[3] (新增虚函数)
+-------------------+
虚函数指针(vptr)
每个包含虚函数的类的对象都会包含一个隐藏的指针——虚函数指针(vptr),它指向该类的虚函数表。
1
2
Base b; // b对象包含一个vptr,指向Base的vtable
Derived d; // d对象包含一个vptr,指向Derived的vtable
对象内存布局:
1
2
3
4
5
6
7
8
9
10
11
12
Base对象内存布局:
+-------------------------+
| vptr (指向Base vtable) | <- 8字节(64位系统)
| 其他成员变量 |
+-------------------------+
Derived对象内存布局:
+-----------------------------+
| vptr (指向Derived vtable) | <- 8字节(64位系统)
| Base部分的成员变量 |
| Derived新增的成员变量 |
+-----------------------------+
2. 动态绑定原理详解
动态绑定(也称为后期绑定或运行时绑定)是通过虚函数表实现的。
动态绑定的执行过程
1
2
Base* ptr = new Derived(); // 基类指针指向派生类对象
ptr->func1(); // 动态绑定调用
执行步骤分解:
- 编译时准备:
- 编译器识别到
func1()是虚函数调用 - 不直接生成函数调用指令,而是生成通过vtable查找的代码
- 编译器识别到
- 运行时执行:
1 2 3 4 5 6 7 8 9 10 11
// 伪代码展示底层执行过程 // ptr->func1() 实际执行的步骤: // 1. 获取对象的vptr void** vtable_ptr = *(void***)ptr; // ptr指向的对象的前8字节 // 2. 通过vtable查找func1的地址 (func1在vtable的索引0位置) void (*func_ptr)() = (void(*)())vtable_ptr[0]; // 3. 调用实际的函数 func_ptr(); // 这里会调用Derived::func1
与静态绑定的对比
静态绑定(编译时绑定):
1
2
Base obj;
obj.func1(); // 编译时就确定调用Base::func1,直接函数调用
动态绑定(运行时绑定):
1
2
Base* ptr = getObject(); // 运行时才知道具体类型
ptr->func1(); // 运行时通过vtable确定调用哪个函数
3. 性能考虑
虚函数调用的开销
- 内存开销:每个对象多占用一个指针大小(8字节)
- 时间开销:
- 一次额外的内存访问(读取vptr)
- 一次vtable查找
- 间接函数调用(可能影响CPU分支预测)
1
2
3
4
5
6
7
8
9
#include <chrono>
class Base {
public:
virtual void virtualFunc() {} // 虚函数
void normalFunc() {} // 普通函数
};
// 虚函数调用通常比普通函数调用慢2-3纳秒
如何优化?
- 多使用final
4. 多重继承中的vtable
在多重继承情况下:
情况1:C类只重写基类虚函数,不引入新的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
public:
virtual void funcA() {}
};
class B {
public:
virtual void funcB() {}
};
class C : public A, public B {
public:
void funcA() override {}
void funcB() override {}
};
C类对象的内存布局:
1
2
3
4
5
6
7
8
9
10
C对象内存布局:
+---------------------------------+
| vptr(指向C-as-A的vtable) |
| A的成员变量 |
+---------------------------------+
| vptr(指向C-as-B的vtable) |
| B的成员变量 |
+---------------------------------+
| C的成员变量 |
+---------------------------------+
对应的vtable布局:
1
2
3
4
5
6
7
8
9
10
11
C-as-A的vtable:
+-------------------+
| C::funcA | <- vtable[0] (重写了A::funcA)
| C::~C (调整版) | <- vtable[1] (析构函数)
+-------------------+
C-as-B的vtable:
+-------------------+
| C::funcB | <- vtable[0] (重写了B::funcB)
| C::~C (调整版) | <- vtable[1] (析构函数)
+-------------------+
情况2:C类引入新的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
virtual void funcA() {}
};
class B {
public:
virtual void funcB() {}
};
class C : public A, public B {
public:
void funcA() override {}
void funcB() override {}
virtual void funcC() {} // C类新引入的虚函数
virtual void funcC2() {} // 另一个新虚函数
};
方案一:扩展主vtable
C类对象的内存布局(方案一 - 扩展主vtable):
1
2
3
4
5
6
7
8
9
10
C对象内存布局:
+----------------------------------------------------+
| vptr (主vtable,指向C-as-A的vtable,包含C的新虚函数) |
| A的成员变量 |
+----------------------------------------------------+
| vptr (指向C-as-B的vtable) |
| B的成员变量 |
+----------------------------------------------------+
| C的成员变量 |
+----------------------------------------------------+
对应的vtable布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
C-as-A的扩展vtable(主vtable):
+-------------------+
| C::funcA | <- vtable[0] (重写)
| C::~C | <- vtable[1] (析构函数)
| C::funcC | <- vtable[2] (C新增的虚函数)
| C::funcC2 | <- vtable[3] (C新增的虚函数)
+-------------------+
C-as-B的vtable(辅助vtable):
+-------------------+
| C::funcB | <- vtable[0] (重写)
| C::~C (调整版) | <- vtable[1] (需要地址调整的析构函数)
+-------------------+
方案二:独立的C vtable
C类对象的内存布局(方案二 - 独立的C vtable,某些编译器实现):
1
2
3
4
5
6
7
8
9
10
11
C对象内存布局:
+------------------------------------+
| vptr (指向C-as-A的vtable) |
| A的成员变量 |
+------------------------------------+
| vptr (指向C-as-B的vtable) |
| B的成员变量 |
+------------------------------------+
| vptr 指向C独有的vtable(含新增虚函数)|
| C的成员变量 |
+------------------------------------+
虚函数调用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C c;
// 通过不同的指针类型访问
A* pa = &c;
B* pb = &c;
C* pc = &c;
pa->funcA(); // 通过A部分的vptr调用C::funcA
pb->funcB(); // 通过B部分的vptr调用C::funcB
pc->funcC(); // 通过A部分的扩展vptr调用C::funcC(方案一)
// 或通过C独有的vptr调用C::funcC(方案二)
// 指针转换和地址调整
A* pa2 = pc; // pa2 == pc,指向同一地址
B* pb2 = pc; // pb2 != pc,编译器自动调整地址偏移
地址调整(Address Adjustment)
在多重继承中,当进行指针转换时可能需要地址调整:
1
2
3
4
5
6
7
8
9
10
11
12
C c;
std::cout << "C对象地址: " << &c << std::endl;
A* pa = &c;
B* pb = &c;
std::cout << "A*指针值: " << pa << std::endl; // 与&c相同
std::cout << "B*指针值: " << pb << std::endl; // 可能与&c不同,有偏移
// 转换回C*时需要反向调整
C* pc1 = static_cast<C*>(pa); // 无需地址调整
C* pc2 = static_cast<C*>(pb); // 需要地址调整
编译器实现差异
不同的编译器可能采用不同的vtable布局策略:
- GCC/Clang:通常采用方案一,扩展主vtable
- MSVC:可能采用不同的布局方式
- 优化考虑:编译器会根据类的复杂度选择最优方案
实际测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
class A {
public:
virtual void funcA() { std::cout << "A::funcA\n"; }
virtual ~A() = default;
};
class B {
public:
virtual void funcB() { std::cout << "B::funcB\n"; }
virtual ~B() = default;
};
class C : public A, public B {
public:
void funcA() override { std::cout << "C::funcA\n"; }
void funcB() override { std::cout << "C::funcB\n"; }
virtual void funcC() { std::cout << "C::funcC\n"; }
};
void testLayout() {
C c;
std::cout << "对象大小: " << sizeof(C) << " bytes\n";
std::cout << "C对象地址: " << &c << std::endl;
A* pa = &c;
B* pb = &c;
std::cout << "A*地址: " << pa << std::endl;
std::cout << "B*地址: " << pb << std::endl;
std::cout << "地址偏移: " << ((char*)pb - (char*)pa) << " bytes\n";
}
关键要点:
- 新虚函数的存储:通常存储在主vtable(第一个基类的vtable)中
- 地址调整:多重继承中的指针转换可能需要运行时地址调整
- 内存开销:每个基类的vtable都需要一个vptr,增加了内存开销
- 性能影响:虚函数调用的性能与单继承基本相同,但指针转换可能有额外开销
5. 实际应用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <vector>
#include <memory>
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const override {
std::cout << "Drawing Circle with radius " << radius << std::endl;
}
double area() const override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() const override {
std::cout << "Drawing Rectangle " << width << "x" << height << std::endl;
}
double area() const override {
return width * height;
}
};
// 多态的威力
void processShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
for(const auto& shape : shapes) {
shape->draw(); // 动态绑定到具体的draw实现
std::cout << "Area: " << shape->area() << std::endl; // 动态绑定到具体的area实现
}
}
关键要点总结:
- vtable是静态的:每个类只有一个vtable,所有该类的对象共享
- vptr是动态的:每个对象都有自己的vptr,指向对应类的vtable
- 继承关系影响vtable:派生类的vtable包含基类的虚函数(可能被重写)
- 性能权衡:虚函数提供灵活性,但有轻微的性能开销
- 编译器优化:现代编译器会尽可能优化虚函数调用,甚至在某些情况下将其内联