为什么不能将基类中所有函数声明为虚函数?

作者 Shilei Tian 日期 2016-09-30
C++
为什么不能将基类中所有函数声明为虚函数?

今天在实验室听师兄讲到滴滴技术岗今年校招的一个面试题,面试官问,为什么不能将基类中所有函数声明为虚函数?我来尝试回答一下这个问题。

首先我觉得这个问题问的很奇怪(当然不排除学长传达信息有误),完全没有必要将所有的函数声明为虚函数。只需要将那些想要获得动态绑定特性的函数声明为虚函数就行了,将所有函数都声明为虚函数会造成性能上的损失。在《深度探索 C++ 对象模型》这本书中第 6 页作者有提到:

C++ 在布局以及存取时间上主要的额外负担是由 virtual 引起的,包括:

  1. virtual function 机制:用以支持一个有效率的“执行期绑定”(runtime binding)。

  2. virtual base class:用以实现“多次出现在继承体系中的 base class,有一个单一而被共享的实例”。

此外还有一些多重继承下的额外负担,发生在“一个 derived class 和其第二或后继之 base class 的转换”之间。

这里提到了虚函数相关。我们都知道,C++ 对象模型对于虚函数的支持是通过两个步骤:

  1. 每一个 class 产生出一堆指向 virtual functions 的指针,放在表格之中,就是我们所说的 virtual table(vtbl)。
  2. 每一个class object 被安插一个指针,指向相关的 virtual table。通常这个指针被称为 vptr。

也就是说,C++ 对象为了支持运行时的动态绑定,在每一个类里面添加了很多信息,具体的对象模型大家可以参照《深度探索 C++ 对象模型》中第 10 页的内容。这样就会导致这个类体积的增加。《Effective C++》中第 42 页中使用了一个例子来解释。

1
2
3
4
5
6
7
class Point {
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};

如果 int 占用 32 bits,那么 Point 对象可塞入一个 64-bit 缓存器中。更有甚者,这样一个 Point 对象可被当作一个“64-bit 量”传给其他语言如 C 或 FORTRAN 撰写的函数。但是,如果加入了虚函数,整个形势就会发生改变。因为类要增加很多信息,在 32-bit 计算机题结构中将占用 64 bits(为了存放两个 ints)至 96 bits(两个 ints 加上 vptr);在 64-bit 体系结构中可能占用 64~128 bits,因为指针在这样的计算机结构中占 64 bits。因此,为 Point 添加一个 vptr 会增加其对象大小达 50%~100%!Point 对象不再能够塞入 64-bit 缓存器,而 C++ 的 Point 对象也不再和其他语言(如 C)内的相同声明有着一样的结构(因为其他语言的对应物并没有 vptr),因此也就不再可能把它传递至(或接受自)其他语言所写的函数,除非你明确补偿 vptr,这样也不再具有移植性。

因此,将不必要的函数声明为虚函数会增加对象的体积,并且结合上面的对象模型图可以发现,为了取一个虚函数,需要增加访问内存的次数(因为对象存储的是指针),并且还增大了体积,这样必然会造成性能上的缺失,所以不能将所有的函数声明为虚函数。