面试题答案
一键面试虚函数表结构
- 每个包含虚函数的类都有一个虚函数表:虚函数表是一个存储虚函数指针的数组。当一个类定义了虚函数,编译器会为该类创建一个虚函数表。
- 表中存放虚函数指针:虚函数表中的每个条目都是一个指向该类虚函数实现的指针。如果子类重写了父类的虚函数,那么子类虚函数表中对应的条目将指向子类重写后的函数实现;若未重写,则指向父类的虚函数实现。
- 对象中的虚表指针:每个包含虚函数的类的对象都包含一个指向其所属类虚函数表的指针(vptr)。这个指针通常位于对象内存布局的起始位置(对于大多数编译器而言)。
程序运行时开销
- 空间开销
- 虚表本身:每个包含虚函数的类都有一个虚函数表,这需要额外的内存空间来存储虚函数指针数组。如果有大量包含虚函数的类,虚表占用的内存会逐渐增加。
- 虚表指针:每个包含虚函数的类的对象都有一个虚表指针,这会增加每个对象的大小。对于一些对象数量众多且对象本身较小的场景,虚表指针带来的额外空间开销可能比较显著。
- 时间开销
- 间接函数调用:由于虚函数调用是通过虚函数表指针间接进行的,首先要通过对象的虚表指针找到虚函数表,然后从虚函数表中获取对应虚函数的指针,最后才能调用函数。这一系列间接操作比直接函数调用(非虚函数调用)多了额外的内存访问,导致性能下降。特别是在性能敏感的循环中频繁调用虚函数时,这种时间开销会更加明显。
优化策略
- 减少虚函数使用
- 仅在必要时使用虚函数:如果一个函数在继承体系中不需要被重写,就不要将其定义为虚函数。这样可以避免虚函数表带来的空间和时间开销。
- 使用模板代替虚函数:在某些情况下,模板可以提供编译期多态,避免运行时的虚函数表开销。例如,对于一些算法相同但数据类型不同的操作,可以使用模板实现。
- 对象布局优化
- 将虚函数较少的类放在继承体系的上层:这样可以减少子类虚函数表的大小,因为子类只需要重写需要的虚函数,从而减少虚函数表占用的内存。
- 对象池技术:对于频繁创建和销毁包含虚函数的对象的场景,可以使用对象池技术。对象池预先创建一定数量的对象,避免频繁的内存分配和释放,从而减少虚表指针创建和销毁带来的开销。
- 内联虚函数
- 对于简单的虚函数:如果虚函数的实现比较简单,可以将其声明为内联函数。编译器会尝试在调用处展开内联虚函数,减少间接函数调用的开销。不过要注意,内联函数可能会增加代码体积,所以对于复杂的虚函数不适合使用内联。
- 缓存虚函数调用
- 在频繁调用虚函数的场景下:可以缓存虚函数的调用结果。例如,如果一个虚函数的返回值在一定条件下不会改变,可以在第一次调用后缓存结果,后续直接返回缓存值,避免重复的虚函数调用开销。