More Effective C++ (效率)
6.4:理解临时对象
<1>为了使函数成功调用而进行隐式类型转换和函数返回对象时
<2>当传送给函数的对象类型与参数类型不匹配时会产生的一种情况
<3>当通过传值方式传递对象或者传递常量引用参数时,才会发生类型转换
示例代码如下:
1 #include<iostream> 2 using namespace std; 3 class Test 4 { 5 int a; 6 public: 7 Test(int data = 10):a(data) 8 { 9 cout<<"Construction :"<<this<<endl; 10 } 11 ~Test() 12 { 13 cout<<"Destroy :"<<this<<endl; 14 } 15 void Print() const 16 { 17 cout<<"data:"<<a<<endl; 18 } 19 }; 20 void Fun1(const Test & a) 21 { 22 a.Print(); 23 } 24 void Fun2(Test &b) 25 { 26 b.Print(); 27 } 28 void Fun3(Test c) 29 { 30 c.Print(); 31 } 32 void main() 33 { 34 Fun1(10); //传递常量引用参数 35 // Fun2(10); //error!! //非常引用 36 Fun3(20); //传值方式传递对象 37 } 38 /* 39 Construction :0012FE88 40 data:10 41 Destroy :0012FE88 42 Construction :0012FE6C 43 data:20 44 Destroy :0012FE6C 45 */
6.9:理解虚拟函数、多继承、虚基类和RTTI所需的代价
<1>virtual table只实现了虚拟函数的一半机制,仅仅只有这些是没有用的。
只有用某种方法指出每个对象对应的vtbl时,它们才能使用。其实这也是virtual table pointer 的工作,它来建立这种联系。
每个声明了虚函数的对象都带有它,它是一个看不见的数据成员,指向对应类的virtual table 。
这个看不见的数据成员也称为vptr,被编译器加在对象里,位置只有编译器才知道。
从理论上讲,我们可以认为包含有虚函数的对象的布局是这样的:
假如,我们有一个程序,包含几个C1和C2对象。对象,vptr和刚刚我们讲述的vtabl之间的关系,在程序里我们可以这样去想象:
下面考虑这段代码的执行:
1 void makeACall(C1 *pC1) 2 { 3 pC1->f1(); 4 }
通过指针 pC1调用虚拟函数f1。
仅仅看这段代码,你不会知道它调用的是那一个f1 函数?? C1::f1 或C2::f1,因为pC1可以指向C1对象也可以指向C2对象。
尽管如此编译器仍然得为在makeACall中的f1 函数的调用生成代码,它必须确保无论 pC1指向什么对象,函数的调用必须正确。
编译器生成的代码会做如下这些事情:
1:通过对象的 vptr 找到类的 vtbl。
这是一个简单的操作,因为编译器知道在对象内哪里能找到 vptr(毕竟是由编译器放置的它们) 。
因此这个代价只是一个偏移调整(以得到vptr)和一个指针的间接寻址(以得到 vtbl)。
2:找到对应vtbl内的指向被调用函数的指针(在上例中是f1) 。
这也是很简单的,因为编译器为每个虚函数在vtbl 内分配了一个唯一的索引。这步的代价只是在vtbl 数组内的一个偏移。
3: 调用第二步找到的的指针所指向的函数。
如果我们假设每个对象有一个隐藏的数据叫做vptr,而且f1 在 vtbl 中的索引为i,此语句
pC1->f1();
生成的代码就是这样的:
(*pC1->vptr[i])(pC1); //调用被vtbl中第i个单元指向的函数,而pC1->vptr指向的是vtbl;pC1被作为this指针传递给函数。
4:实际上,虚函数不能是内联函数。
因为“内联”是指“在编译期间用被调用的函数体本身来代替函数调用的指令。”而虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”
如果编译器在某个函数的调用点不知道具体是哪个函数被调用,你就能知道为什么它不会内联该函数的调用。
这是虚函数所需的第三个代价:你实际上放弃了使用内联函数。(当通过对象调用虚函数时,它可以被内联,
但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。)
<2>虚基类
多继承经常导致对虚基类的需求。没有虚基类,如果一个派生类有一个以上从基类的继承路径,基类的数据成员被复制到每一个继承类对象里,继承类与基类间的每条路径都有一个拷贝。
程序员一般不会希望发生这种复制,而把基类定义为虚基类则可以消除这种复制。
下面考虑这幅图:所谓的菱形继承
这也就是虚基类产生的原因。
<3>运行时类型识别
RTTI 能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询。
这些信息被存储在类型为 type_info 的对象里,你能通过使用typeid操作符访问一个类的type_info 对象。
在每个类中仅仅需要一个RTTI的拷贝,但是必须有办法得到任何对象的类型信息。实际上这叙述得不是很准确。
语言规范上这样描述:我们保证可以获得一个对象动态类型信息,如果该类型有至少一个虚函数。
这使得RTTI数据似乎有些象virtual function talbe(虚函数表)。每个类我们只需要信息的一个拷贝,我们需要一种方法从任何包含虚函数的对象里获得合适的信息。
这种 RTTI 和virtual function table 之间的相似点并不是巧合:RTTI被设计为在类的vtbl基础上实现。
例如,vtbl数组的索引 0 处可以包含一个 type_info 对象的指针,这个对象属于该vtbl相对应的类。上述C1 类的vtbl看上去象这样:
Good Good Study, Day Day Up.
顺序 选择 循环 坚持