编程交流与学习--More Effective C++的学习-Item M24:理解虚拟函数、多继承、虚基类和RTTI所需的代价

本文探讨了在C++中使用虚函数、多继承、虚基类及RTTI所带来的开销,包括对象大小的增加、每类数据的增长以及内联减少等方面。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
Item M24:理解虚拟函数、多继承、虚基类和RTTI所需的代价
当调用一个虚拟函数时,被执行的代码必须与调用函数的对象的动态类型相一致;大多数编译器是使用virtual table和virtual table pointersvirtual tablevirtual table pointers通常被分别地称为vtbl和vptr。
一个vtbl通常是一个函数指针数组。(一些编译器使用链表来代替数组,但是基本方法是一样的)在程序中的每个类只要声明了虚函数或继承了虚函数,它就有自己的vtbl,并且类中vtbl的项目是指向虚函数实现体的指针。
这个论述引出了虚函数所需的第一个代价:你必须为每个包含虚函数的类的virtual talbe留出空间。类的vtbl的大小与类中声明的虚函数的数量成正比(包括从基类继承的虚函数)。
避免把虚函数声明为内联函数。有一些原因导致现在的编译器一般总是忽略虚函数的的inline指令。
virtual table pointer指出每个对象对应的virtual table,每个声明了虚函数的对象都带有它,它是一个看不见的数据成员。这个看不见的数据成员也称为vptr,被编译器加在对象里,位置只有才编译器知道。不同的编译器放置它的位置也不同。存在继承的情况下,一个对象的vptr经常被数据成员所包围。如果存在多继承(Multiple inheritance),这幅图片会变得更复杂。
虚函数所需的第二个代价是:在每个包含虚函数的类的对象里,你必须为额外的指针付出代价。如果对象很小,这是一个很大的代价。较大的对象有可能不适合放在缓存(cache)或虚拟内存页中(virtual memory page),这就可能使得系统换页操作增多。
通过对象的vptr找到类的vtbl。找到对应vtbl内的指向被调用函数的指针,编译器为每个虚函数在vtbl内分配了一个唯一的索引。调用第二步找到的的指针所指向的函数。(*pC1->vptr[i])(pC1); 这几乎与调用非虚函数效率一样。在大多数计算机上它多执行了很少的一些指令。调用虚函数所需的代价基本上与通过函数指针调用函数一样。虚函数本身通常不是性能的瓶颈。
在实际运行中,虚函数所需的代价与内联函数有关。实际上虚函数不能是内联的。这是虚函数所需的第三个代价:你实际上放弃了使用内联函数。
在多继承里,在对象里为寻找vptr而进行的偏移量计算会变得更复杂。在单个对象里有多个vptr(每一个基类对应一个);除了我们已经讨论过的单独的自己的vtbl以外,还得为基类生成特殊的vtbl。因此增加了每个类和每个对象中的虚函数额外占用的空间,而且运行时调用所需的代价也增加了一些。
多继承经常导致对虚基类的需求。没有虚基类,如果一个派生类有一个以上从基类的继承路径,基类的数据成员被复制到每一个继承类对象里,继承类与基类间的每条路径都有一个拷贝。程序员一般不会希望发生这种复制,而把基类定义为虚基类则可以消除这种复制。然而虚基类本身会引起它们自己的代价,因为虚基类的实现经常使用指向虚基类的指针做为避免复制的手段,一个或者更多的指针被存储在对象里。
运行时类型识别(RTTI)。RTTI能让我们在运行时找到对象和类的有关信息,这些信息被存储在类型为type_info的对象里,你能通过使用typeid操作符访问一个类的type_info对象。在每个类中仅仅需要一个RTTI的拷贝。保证可以获得一个对象动态类型信息,如果该类型有至少一个虚函数。这使得RTTI数据似乎有些象virtual function talbe(虚函数表)。RTTI被设计为在类的vtbl基础上实现。例如,vtbl数组的索引0处可以包含一个type_info对象的指针,这个对象属于该vtbl相对应的类。
下面这个表各是对虚函数、多继承、虚基类以及RTTI所需主要代价的总结:
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
一些人看到这个表格以后,会很吃惊,他们宣布“我还是应该使用C”。很好。但是请记住如果没有这些特性所提供的功能,你必须手工编码来实现。在多数情况下,你的人工模拟可能比编译器生成的代码效率更低,稳定性更差。例如使用嵌套的switch语句或层叠的if-then-else语句模拟虚函数的调用,其产生的代码比虚函数的调用还要多,而且代码运行速度也更慢。再有,你必须自己人工跟踪对象类型,这意味着对象会携带它们自己的类型标签(type tag)。因此你不会得到更小的对象。
理解虚函数、多继承、虚基类、RTTI所需的代价是重要的,但是如果你需要这些功能,不管采取什么样的方法你都得为此付出代价,理解这点也同样重要。有时你确实有一些合理的原因要绕过编译器生成的服务。例如隐藏的vptr和指向虚基类的指针会使得在数据库中存储C++对象或跨进程移动它们变得困难,所以你可能希望用某种方法模拟这些特性,能更加容易地完成这些任务。不过从效率的观点来看,你自己编写代码不可能做得比编译器生成的代码更好。
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值