C++的多态特性允许我们通过基类指针或引用调用派生类的重写函数,这一能力的核心支撑就是虚函数表和vptr指针。虚函数表是编译器在编译阶段为包含虚函数的类生成的一块静态存储区域,而vptr则是对象实例中指向对应虚函数表的指针,二者配合完成了运行时的函数动态绑定。

虚函数表的基本结构
当一个类中声明了虚函数,编译器会为这个类生成一个虚函数表,表中存放的是该类所有虚函数的地址。如果派生类重写了基类的虚函数,派生类的虚函数表中对应位置会替换为重写后的函数地址;如果派生类新增了虚函数,这些函数地址会追加到虚函数表的末尾。
我们可以通过一段简单的代码来验证虚函数表的存在和布局:
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() {
cout << "Base func1" << endl;
}
virtual void func2() {
cout << "Base func2" << endl;
}
};
class Derived : public Base {
public:
void func1() override {
cout << "Derived func1" << endl;
}
virtual void func3() {
cout << "Derived func3" << endl;
}
};
int main() {
Base base;
Derived derived;
// 通过对象地址获取vptr,再获取虚函数表首地址
// 注意:不同编译器下对象内存布局可能有差异,这里以常见布局为例
typedef void (*FuncPtr)();
long long* vptr = (long long*)&base;
long long* vtable = (long long*)*vptr;
// 调用第一个虚函数
FuncPtr f1 = (FuncPtr)vtable[0];
f1(); // 输出 Base func1
// 调用第二个虚函数
FuncPtr f2 = (FuncPtr)vtable[1];
f2(); // 输出 Base func2
long long* d_vptr = (long long*)&derived;
long long* d_vtable = (long long*)*d_vptr;
FuncPtr df1 = (FuncPtr)d_vtable[0];
df1(); // 输出 Derived func1,说明func1被重写
FuncPtr df2 = (FuncPtr)d_vtable[1];
df2(); // 输出 Base func2,未重写的虚函数保持基类实现
FuncPtr df3 = (FuncPtr)d_vtable[2];
df3(); // 输出 Derived func3,派生类新增虚函数
return 0;
}
vptr指针的内存布局
对于包含虚函数的类的对象,编译器会在对象的内存空间最前面(或最末尾,取决于编译器实现)插入一个vptr指针,这个指针指向该对象所属类的虚函数表。因此,对象的大小会比没有虚函数时多出一个指针的大小(通常是8字节,64位系统下)。
我们可以通过sizeof运算符来验证这一点:
#include <iostream>
using namespace std;
class NoVirtual {
public:
int a;
};
class WithVirtual {
public:
int a;
virtual void func() {}
};
int main() {
cout << "NoVirtual size: " << sizeof(NoVirtual) << endl; // 输出4,仅int的大小
cout << "WithVirtual size: " << sizeof(WithVirtual) << endl; // 输出16(64位系统),int4字节+对齐+vptr8字节
return 0;
}
多态的调用流程
当通过基类指针或引用调用虚函数时,程序会先通过对象的vptr找到对应的虚函数表,再从虚函数表中取出对应函数的地址,最后执行该函数。这个查找过程是在运行时完成的,因此实现了动态绑定。
具体流程可以分为三步:
- 第一步:通过指针或引用找到对象实例的起始地址
- 第二步:从对象实例中取出vptr指针,定位到对应的虚函数表
- 第三步:根据虚函数在表中的索引,获取函数地址并调用
我们来看一段多态调用的示例代码:
#include <iostream>
using namespace std;
class Shape {
public:
virtual double getArea() {
return 0.0;
}
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
double getArea() override {
return 3.14 * radius * radius;
}
private:
double radius;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() override {
return width * height;
}
private:
double width;
double height;
};
int main() {
Shape* shape1 = new Circle(2.0);
Shape* shape2 = new Rectangle(3.0, 4.0);
cout << "Circle area: " << shape1->getArea() << endl; // 输出12.56
cout << "Rectangle area: " << shape2->getArea() << endl; // 输出12
delete shape1;
delete shape2;
return 0;
}
注意事项
在使用虚函数和多态时,有几个容易出错的点需要注意:
- 构造函数不能是虚函数,因为对象构造时vptr还未初始化完成,无法实现动态绑定
- 析构函数建议声明为虚函数,否则通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源无法释放
- 虚函数的调用会有一定的性能开销,因为需要额外的查表操作,在对性能要求极高的场景下需要谨慎使用
- 静态函数不能是虚函数,因为静态函数属于类而不属于对象,没有this指针,无法通过vptr访问
理解虚函数表和vptr的实现原理,不仅能帮助我们更好地使用C++的多态特性,也能在遇到虚函数相关的内存问题、调用异常时快速定位原因,是深入掌握C++面向对象编程的重要基础。