文章目录
1. 多态的定义与作用
多态指的是同一操作在不同对象上具有不同的表现。C++ 中多态分为两类:
- 编译时多态(静态多态):如函数重载、运算符重载,通过编译器在编译时决定调用哪个函数。
- 运行时多态(动态多态):主要通过继承和虚函数实现,调用函数时根据实际对象的类型动态决定调用哪个函数。
本文重点讨论 运行时多态,它是通过虚函数机制实现的。
2. 虚函数与虚函数表(vtable)
C++ 中,虚函数 是用 virtual
关键字修饰的成员函数,表示这个函数可以在派生类中被重写,并且可以通过基类指针或引用调用时执行派生类的函数版本。
class Base {
public:
virtual void Print() {
std::cout << "Base::Print" << std::endl;
}
};
class Derived : public Base {
public:
void Print() override {
std::cout << "Derived::Print" << std::endl;
}
};
在上面的例子中,Base
类的 Print
函数是虚函数,Derived
类重写了这个函数。当我们通过基类指针调用 Print
时,会动态决定调用 Base
还是 Derived
的版本。
底层实现上,虚函数的机制依赖于虚函数表(vtable)和虚函数指针(vptr)。
3. 虚函数表(vtable)
每个包含虚函数的类,编译器会为其生成一个 虚函数表(vtable)。虚函数表是一个存放虚函数地址的数组。当对象是某个类的实例时,它会有一个指向该类的虚函数表的指针,称为 虚函数指针(vptr)。
- 虚函数表 (vtable):每个包含虚函数的类都有一个虚函数表,表中存储的是该类的虚函数指针。如果派生类重写了某个虚函数,虚函数表中对应的指针会指向派生类的版本。
- 虚函数指针 (vptr):每个对象实例都有一个虚函数指针
vptr
,它指向该对象所属类的虚函数表。
当通过基类指针调用虚函数时,编译器根据对象的 vptr
找到对应的 vtable
,然后查找表中的虚函数指针并进行调用。这样,实现了动态绑定(运行时多态)。
4. 虚函数调用的底层过程
假设我们有如下代码:
Base* ptr = new Derived();
ptr->Print();
这段代码的底层执行过程如下:
- 对象构造:当
Derived
类对象构造时,编译器会在对象内存布局中加入一个vptr
,并将其指向Derived
类的虚函数表。 - 调用虚函数:当
ptr->Print()
被调用时,程序会根据ptr
指向的对象类型,通过vptr
查找Derived
类的虚函数表,找到Print
函数的地址并进行调用。
虚函数的调用可以简化为以下伪代码:
void (*vptr)() = this->vptr[Print];
vptr();
这里,vptr
指向的是 Derived
类的虚函数表,vptr[Print]
表示从虚函数表中获取 Print
函数的地址,然后通过该地址调用实际的函数。
5. 内存布局中的虚函数指针
为了更好地理解虚函数表在内存中的表现,来看一下对象的内存布局。在一个对象中,虚函数指针通常是对象布局的第一个成员,它指向该对象所属类的虚函数表。
+--------------------+
| vptr (虚函数指针) |
+--------------------+
| 非静态数据成员 |
+--------------------+
在运行时,当调用虚函数时,程序首先通过对象的 vptr
找到对应类的 vtable
,然后查找并调用虚函数。因此,每次调用虚函数都会产生一次间接函数调用开销,这就是为什么虚函数会比普通成员函数稍慢一些的原因。
6. 多重继承中的虚函数表
在 C++ 中,多重继承 会导致一个类拥有多个基类。这时,每个基类都会有自己的虚函数表。为了支持多重继承,编译器会为每个基类维护不同的虚函数表和 vptr
。
例如:
class Base1 {
public:
virtual void Func1() {}
};
class Base2 {
public:
virtual void Func2() {}
};
class Derived : public Base1, public Base2 {
public:
void Func1() override {}
void Func2() override {}
};
在 Derived
类的对象中,它会有两个 vptr
,分别指向 Base1
和 Base2
的虚函数表。这种情况下,虚函数调用会更加复杂,因为编译器需要根据继承层次和实际对象类型找到正确的 vtable
。
7. RTTI 与动态类型识别
C++ 运行时类型识别(RTTI)是通过虚函数表实现的。RTTI 包括 typeid
和 dynamic_cast
等操作,它们可以在运行时检查对象的实际类型。编译器会在虚函数表中增加额外的信息,用于支持类型识别操作。
typeid
:用于获取对象的类型信息。dynamic_cast
:用于安全地将基类指针转换为派生类指针。它依赖于虚函数表中的信息,确保转换的正确性。