前言
C++编译器必须实现语言的每一个特性,其实现细节由编译器来决定,不同的编译器以不同的方式实现语言特性。在多数情况下,开发者无需理解这些隐藏于代码背后的操作。然而某些特性的实现对对象大小和其成员函数执行速度有很大的影响,所以对于这些特性有一个基本的了解,知道编译器可能执行的操作,就较为重要。
虚函数
当调用虚函数时,所执行的代码必须与调用函数的对象的动态类型一致;指向对象的指针或引用的静态类型并不重要。
编译器为了高效地提供这种行为,通常会使用virtual table与virtual table pointers.此二者通常又被称为vtbl与vptr.
vbtl
一个vbtl通常是一个函数指针数组(或链表)。只要某个类声明了虚函数或者继承了虚函数就会存在vtbl,vtbl中的元素是指向虚函数实现体的指针。1
2
3
4
5
6
7
8
9
10class C1 {
public:
C1();
virtual ~C1();
virtual void f1();
virtual int f2(char c) const;
virtual void f3(const string& s);
void f4() const;
...
};
C1的vtbl如下所示:
显然,非虚函数不会存在于vtbl中。
继承下的vbtl
现有C2继承自C1,并且重定义了部分虚函数:1
2
3
4
5
6
7class C2: public C1{
public:
C2();//非虚函数
virtual ~C2(); //重定义函数
virtual void f1();//重定义函数virtual void f5(char *str); //新的虚函数
...
};
C2的vtbl如下所示:
可以看出,C2的vbtl里面包括了没有被C2重定义的C1的虚函数的指针。
vbtl体现了虚函数所需的第一个代价:每一个包含虚函数的类都需要空间来容纳vtbl,其大小与虚函数的数量成正比。
vtbl的位置
因为每一个类只需要一个vbtl拷贝,那把它放在哪个obj里呢?编译器厂商有两种做法:
- 为所有可能需要vbtl的obj生成一个vbtl拷贝,连接程序然后删除多余的拷贝。最后的可执行文件或者程序库里只有一个vbtl实例。
- 采用启发式算法:只有obj包含该类的第一个非内联,非纯虚函数定义(也就是实现体)时才会生成vbtl.
vptr
单有vbtl并不能实现虚函数,还需要vptr.这是一个指向vbtl的指针,隐藏在对象中,其位置只有编译器知道。这是虚函数的第二个代价:对象内需要额外的空间开销来存储指针。对于较小的对象而言,这笔买卖很不划算。
假设我们有一些C1与C2的对象,对象、vbtl、vptr的关系大致如下图所示:1
2
3void makeACall(C1 *pC1){
pC1->f1();
}
为了保证调用正确,编译器做出了如下操作:
- 通过对象的vptr找到vtbl。
- 找到vbtl里面的指向被调用函数的指针。
- 调用该函数。
假设存在索引i指向vbtl中的f1函数,那么上述代码的执行类似于:1
(*pC1->vptr[i])(pC1);//pC1作为this指针作为形参
从上述过程可以看出调用虚函数并不是性能的瓶颈,因为虚函数调用所花费的成本相对于普通函数调用相差无几。在实际运行中虚函数成为性能瓶颈的原因是无法内联,原因很简单,inline是编译到此处时替换,而虚函数的确认需要到运行期,这就是虚函数的第三个不足:无法内联从而降低性能。
多继承与虚基类
一旦多继承被引入,在对象里为寻找vptr而进行的偏移量计算会变得更复杂。在单个对象中会存在多个vptr(每一个基类对应一个),此外,此外,针对base class而形成的vtbl也被生成。这直接导致了空间成本进一步扩大,并且运行期调用成本也有了轻微的增长。
多继承导致了对虚基类的需求。在不使用虚基类的情况下,如果一个派生类有一个以上从基类的继承路径,基类的数据成员将通过所有路径产生多个拷贝存在于派生类中。
但虚基类本身亦存在使用成本,因为虚基类的实现经常使用指向虚基类的指针做为避免复制的手段,从而导致一个或者更多的指针被存储在对象里。
钻石型继承体系(Effective C++ 41)
考虑如下的继承关系:
D对象的内部会呈现出这样的结构:
显然,对象内部有一个多余的指针,这也就是上文所说的虚基类的一大弊端。
当我们再加入vptr的概念,D对象的内部结构大概是这样:
四个类只有3个vptr,这是因为编译器发现BD的vptr可以共享。
运行期类型识别(RTTI)
RTTI能够让我们在运行时找到对象和类的相关信息,这样信息被存储在类型为type_info的对象里,我们可以通过typeid操作符来访问一个类的type_info对象.
一个类仅仅只需要一个RTTI的拷贝,但是必须有办法得到任何对象的类型信息。从语言规范角度而言:如果一个类型至少有一个虚函数,那我们保证可以获得一个对象动态类型信息。这似乎类似于vbtl,实际上,RTTI就是基于vbtl实现的。
具体来说,vbtl的索引0处可以包含一个指针,指向type_info对象:
也就是说vbtl又多了一个需要被占用的空间。
总结
Feature | Increases Size of Objects | Increases Per-Class Data | Reduces Inlining |
---|---|---|---|
Virtual Functions | Yes | Yes | Yes |
Multiple Inheritance | Yes | Yes | No |
Virtual Base Classes | Often | Sometimes | No |
RTTI | No | Yes | No |
要记住理解是为了更好的使用,而非因噎废食。