运行时多态的设计思想:
对于相关的类型,确定它们之间的一些共同特征,(属性和方法),将共同特征被转移到基类中,
然后在基类中,把这些共同的函数或方法声明为公有的虚函数接口。然后使用派生类继承基类,并且在派生类中重写这些虚函数,以完成具体的功能。这种设计使得共性很清楚,避免了代码重复,将来容易增强功能,并易于长期维护。
客户端的代码(操作函数)通过基类的引用或指针来指向这些派生类型对象,对虚函数的调用会自
动绑定到派生类对象上重写的虚函数。
虚函数的定义:
虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数表);
关键字virtual指明该成员函数为虚函数。只能将类的成员函数定义为虚函数。当某一个类的成员函
数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。
总结:运行时的多态性: 公有继承 + 虚函数 + (指针或引用调用虚函数)。
定义虚函数的规则
**类的成员函数定义为虚函数,但必须注意以下几条:
- 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
- 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
- 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
- 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
- 构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
- 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
- 实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
- 在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价, 但通用性是一个更高的目标。
- 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。**
1. 运行时多态的核心:虚函数表(vtable)
1.1 基本概念
当类中包含虚函数时,编译器会为该类生成一个虚函数表(vtable),每个对象会包含一个指向这个表的指针(vptr)。
cpp
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
virtual void func3() { cout << "Base::func3" << endl; }
int base_data;
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func2() override { cout << "Derived::func2" << endl; }
// 不重写 func3,使用基类的版本
int derived_data;
};
1.2 内存布局详解
Base类的内存布局:
text
Base对象:
+----------------+
| vptr | --> 指向Base的vtable
+----------------+
| base_data |
+----------------+
Base的vtable:
+----------------+
| &Base::func1 |
+----------------+
| &Base::func2 |
+----------------+
| &Base::func3 |
+----------------+
Derived类的内存布局:
text
Derived对象:
+----------------+
| vptr | --> 指向Derived的vtable
+----------------+
| base_data | (继承自Base)
+----------------+
| derived_data | (Derived自有成员)
+----------------+
Derived的vtable:
+-------------------+
| &Derived::func1 | // 重写的函数
+-------------------+
| &Derived::func2 | // 重写的函数
+-------------------+
| &Base::func3 | // 继承基类的函数
+-------------------+
2. 底层实现机制
2.1 编译器如何生成vtable
让我们通过实际代码来观察:
cpp
#include <iostream>
using namespace std;
// 用于观察内存布局的辅助类
class MemoryInspector {
public:
static void printVTable(void** vptr) {
cout << "vtable地址: " << vptr << endl;
for (int i = 0; i < 3; ++i) {
cout << " vtable[" << i << "]: " << vptr[i]
<< " -> 函数地址: " << (void*)vptr[i] << endl;
}
}
};
class Base {
public:
virtual void func1() {
cout << "Base::func1" << endl;
}
virtual void func2() {
cout << "Base::func2" << endl;
}
virtual void func3() {
cout << "Base::func3" << endl;
}
virtual ~Base() {
cout << "Base destructor" << endl;
}
int base_data = 100;
};
class Derived : public Base {
public:
void func1() override {
cout << "Derived::func1" << endl;
}
void func2() override {
cout << "Derived::func2" << endl;
}
// 不重写func3,使用基类版本
int derived_data = 200;
};
int main() {
Base base;
Derived derived;
cout << "=== Base对象 ===" << endl;
cout << "对象地址: " << &base << endl;
cout << "vptr地址: " << (void**)(&base) << endl;
MemoryInspector::printVTable(*(void***)(&base));
cout << "\n=== Derived对象 ===" << endl;
cout << "对象地址: " << &derived << endl;
cout << "vptr地址: " << (void**)(&derived) << endl;
MemoryInspector::printVTable(*(void***)(&derived));
return 0;
}
可能的输出:
text
=== Base对象 ===
对象地址: 0x7ffd4a8b6a10
vptr地址: 0x7ffd4a8b6a10
vtable地址: 0x4022d0
vtable[0]: 0x4016aa -> 函数地址: 0x4016aa
vtable[1]: 0x4016e4 -> 函数地址: 0x4016e4
vtable[2]: 0x40171e -> 函数地址: 0x40171e
=== Derived对象 ===
对象地址: 0x7ffd4a8b6a20
vptr地址: 0x7ffd4a8b6a20
vtable地址: 0x4022f0
vtable[0]: 0x401758 -> 函数地址: 0x401758
vtable[1]: 0x401792 -> 函数地址: 0x401792
vtable[2]: 0x40171e -> 函数地址: 0x40171e
注意:Derived的vtable中前两个条目指向Derived的重写版本,第三个条目指向Base的func3。
3. 函数调用过程分析
3.1 虚函数调用的底层步骤
cpp
Base* ptr = new Derived();
ptr->func1(); // 这个调用在底层发生了什么?
调用过程的汇编级别分析:
assembly
; 1. 获取对象的vptr
mov rax, qword ptr [ptr] ; 获取对象地址
mov rax, qword ptr [rax] ; 获取vptr(对象的前8字节)
; 2. 通过vptr找到vtable中的函数地址
mov rax, qword ptr [rax] ; 获取vtable第一个条目(func1的地址)
; 3. 调用函数
call rax ; 间接调用
等效的C++伪代码:
cpp
// ptr->func1() 的实际执行过程:
void*** vptr = (void***)ptr; // 获取vptr
void* func_address = (*vptr)[0]; // 获取func1的地址
typedef void (*FuncPtr)(); // 函数指针类型
FuncPtr func = (FuncPtr)func_address; // 转换为函数指针
func(); // 调用函数
4. 多级继承的vtable结构
4.1 多层继承示例
cpp
class A {
public:
virtual void funcA() { cout << "A::funcA" << endl; }
int a_data;
};
class B : public A {
public:
virtual void funcA() override { cout << "B::funcA" << endl; }
virtual void funcB() { cout << "B::funcB" << endl; }
int b_data;
};
class C : public B {
public:
virtual void funcA() override { cout << "C::funcA" << endl; }
virtual void funcB() override { cout << "C::funcB" << endl; }
virtual void funcC() { cout << "C::funcC" << endl; }
int c_data;
};
4.2 内存布局分析
C对象的内存布局:
text
C对象:
+----------------+
| vptr | --> 指向C的vtable
+----------------+
| a_data | (来自A)
+----------------+
| b_data | (来自B)
+----------------+
| c_data | (来自C)
+----------------+
C的vtable:
+----------------+
| &C::funcA | // 重写A::funcA
+----------------+
| &B::funcB? | // 不对!应该是&C::funcB
+----------------+
| &C::funcC | // C的新虚函数
+----------------+
实际验证代码:
cpp
void analyzeHierarchy() {
C c;
A* a_ptr = &c;
B* b_ptr = &c;
C* c_ptr = &c;
cout << "A* 调用funcA: "; a_ptr->funcA(); // C::funcA
cout << "B* 调用funcA: "; b_ptr->funcA(); // C::funcA
cout << "B* 调用funcB: "; b_ptr->funcB(); // C::funcB
cout << "C* 调用funcA: "; c_ptr->funcA(); // C::funcA
cout << "C* 调用funcB: "; c_ptr->funcB(); // C::funcB
cout << "C* 调用funcC: "; c_ptr->funcC(); // C::funcC
}
5. 多重继承的vtable复杂性
5.1 多重继承示例
cpp
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
int data1;
};
class Base2 {
public:
virtual void func2() { cout << "Base2::func2" << endl; }
int data2;
};
class Derived : public Base1, public Base2 {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func2() override { cout << "Derived::func2" << endl; }
virtual void func3() { cout << "Derived::func3" << endl; }
int data3;
};
5.2 多重继承的内存布局
text
Derived对象:
+----------------+
| vptr1 | --> 指向Derived/Base1的vtable
+----------------+
| data1 | (Base1的数据)
+----------------+
| vptr2 | --> 指向Derived/Base2的vtable
+----------------+
| data2 | (Base2的数据)
+----------------+
| data3 | (Derived的数据)
+----------------+
vtable1 (对应Base1):
+-------------------+
| &Derived::func1 |
+-------------------+
| &Derived::func3 | // Derived的新虚函数
+-------------------+
vtable2 (对应Base2):
+-------------------+
| &Derived::func2 |
+-------------------+
| 特殊thunk函数? | // 可能包含调整this指针的代码
+-------------------+
5.3 指针调整的验证
cpp
void testMultipleInheritance() {
Derived d;
Base1* b1 = &d;
Base2* b2 = &d;
cout << "Derived地址: " << &d << endl;
cout << "Base1* 地址: " << b1 << endl;
cout << "Base2* 地址: " << b2 << endl;
// 两个Base指针的地址不同!
// b2的地址 = b1的地址 + sizeof(Base1) + vptr大小
}
6. 性能分析与优化
6.1 虚函数调用开销
虚函数调用的开销主要来自:
-
间接内存访问:通过vptr访问vtable
-
指令缓存不命中:函数地址不固定
-
无法内联:编译时无法确定具体函数
6.2 性能测试对比
cpp
#include <chrono>
using namespace std::chrono;
class NonVirtual {
public:
void func() { /* 做一些工作 */ }
};
class Virtual {
public:
virtual void func() { /* 做同样的工作 */ }
};
void performanceTest() {
const int iterations = 1000000000;
NonVirtual nv;
Virtual v;
Virtual* v_ptr = &v;
// 测试非虚函数
auto start1 = high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
nv.func();
}
auto end1 = high_resolution_clock::now();
// 测试虚函数
auto start2 = high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
v_ptr->func();
}
auto end2 = high_resolution_clock::now();
cout << "非虚函数: "
<< duration_cast<milliseconds>(end1 - start1).count()
<< "ms" << endl;
cout << "虚函数: "
<< duration_cast<milliseconds>(end2 - start2).count()
<< "ms" << endl;
}
7. 虚析构函数的原理
7.1 为什么需要虚析构函数
cpp
class Base {
public:
// virtual ~Base() { cout << "Base析构" << endl; }
~Base() { cout << "Base析构" << endl; } // 非虚析构函数
};
class Derived : public Base {
public:
~Derived() { cout << "Derived析构" << endl; }
};
int main() {
Base* ptr = new Derived();
delete ptr; // 只调用Base的析构函数!内存泄漏!
}
7.2 虚析构函数在vtable中的位置
text
Base的vtable:
+----------------+
| &Base::~Base | // 虚析构函数
+----------------+
| &Base::func1 |
+----------------+
Derived的vtable:
+-------------------+
| &Derived::~Derived| // 完整的析构序列
+-------------------+
| &Derived::func1 |
+-------------------+
8. 总结
运行时多态的核心原理:
-
vtable(虚函数表):每个包含虚函数的类都有一个虚函数表
-
vptr(虚函数表指针):每个对象包含指向对应vtable的指针
-
动态绑定:通过vptr在运行时查找并调用正确的函数
-
继承关系:派生类的vtable基于基类的vtable构建
关键特点:
-
✅ 灵活性强,支持运行时类型确定
-
✅ 接口统一,易于扩展
-
❌ 有性能开销(间接调用)
-
❌ 内存开销(每个对象多一个vptr)
-
❌ 无法内联优化