1.如果设计者提供多个constructors,但其中都没有default constructor呢?编译器会扩张现在的每一次constructors,将“用以调用所有必要之default constructor”的程序代码加进去。它不会合成一个新的default constructor,因为其他由user所提供的constructors存在的缘故。如果同时亦存在着“带有default constructor”的member class objects,那些default constructors也会被调用--在所有base class constrcutor都被调用之后。
2.当一个类有虚函数时下面两个扩张操作会在编译期间完成:
① 1个virtual function table会被编译器产生出来,内放class的virtual functions的地址。
② 在每一个class object中,一个额外的pointer member (vptr)会被编译器合成出来,内含相关之class vtbl的地址。
当用指针或者引用调用虚函数时:widget->filp(); 会被编译器改写为*(widget->vptr[1])(widget)
1代表flip在virtual table中的固定索引
widget代表要交给“被调用的某个flip函数实例”的this指针
为了让着机制发挥功效,编译器必须为每一个Widget(或其派生类的)object的vptr设定初始值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器都会安插一些代码来做这样的事情。对于那些未声明任何constructor的class,编译器会为它们合成一个default constructor,以便正确地初始化每一个class object的vptr。
3.当类有虚基类时,编译器也会生成一个额外的虚基类指针,以指向其虚基类。根据不同的编译器实现机制,虚基类指针可能指向的是一个跟虚函数表类似的虚基类表,也可能将虚基类的地址放在虚函数表中,当使用虚函数表时,索引为正时调用的是虚函数,索引为负时使用的是虚基类。总而言之,虚基类指针的作用就是必须让所指向的虚基类地址在执行期间确定下来。在编译期间由于指针可能指向的真正类型无法确定,因为虚基类在不断继承的过程中偏移地址会发生改变的缘故,所以虚基类指针才显得那么重要。所以构造函数需要初始化以便在执行期可以正确找到那个虚基类的地址。
例如 类A有一个虚基类x,x有一个成员a。类A有一个派生类B。A* p;p可能指向类B也可能指向类A,而虚基类x在类A与类B中的位置是不同的,所以p->a这样调用虚基类的成员时,编译器会变成p->vbtr->a。通过一个虚基类指针来追踪虚基类X,这就是构造函数在这里的作用,正确设定好虚基类的位置。
4.Default constructor和copy constructor在必要的时候才由编译器合成出来。必要指的是class不展现bitwise copy semantics
是否展现bitwise copy semantics也可以参考构造函数的哪四种情况。基类和成员类对象很好理解,主要是后两种。当类有虚函数时,我们需要正确初始化vptr,而使用bitwise copy semantics不能保证。所以编译器需要合成一个copy constructor来正确初始化vptr。
Bear yogi;
Bear winnie=yogi;拷贝构造函数可以使得winnie正确初始化,vptr也指向yogi的vptr。
但是当ZooAnimal franny=yogi;这会发生切割,如果施行了bitwise copy semantics 的话 会使得franny的vptr指向yogi的vptr,而多态是无法通过对象施行的(编译器会拒绝)。所以会有一个copy constructor 合成出来,使得yogi的基类部分的vptr赋值给winnie。
最后一种情况则是 当含有一个虚基类时:例如Racoon有一个虚基类,RedPanda继承Racoon
Racoon rocky;
Racoon litter_critter=rockey;这样使用bitwise就行了
而如果 RedPanda litter_red;
Racoon litter critter=litter_red;由于虚基类在不同子类中的偏移位置不同,所以编译器需要合成一些代码来正确地初始化虚基类指针。在例子中,虚基类在litter_red中的偏移与在litter_critter中不一致,所以进行赋值时需要进行调整。这步由编译器合成的copy constructor完成。
5.参数的初始化:
把一个类对象当做参数传给一个函数(或者作为一个函数的返回值)相当于下面这种形式的初始化操作:
X xx=arg;
若已知这个函数:
void foo(X x0);
下面这种调用方式:
X xx;
foo(xx);
局部变量初始化的方式有两种:①拿xx来拷贝构造一个临时对象,将这个临时对象以bitwise的方式拷贝到x0上
② 直接拷贝构建,将实参构建在形参上。
返回值的初始化:
X bar()
{
X xx;
return xx;
}
bar()的返回值如何从局部对象xx中拷贝过来?
1.首先加上一个额外参数,类型是class object的一个reference。这个参数用来放置被“拷贝构建”而得的返回值。
2.在return 指令之前安插一个copy constructor调用操作,以便将欲传回object的内容当做上述新增参数的初值。
这里会看你是如何使用这个函数再看编译器会如何操作:
如果 是 X x=Bar();
函数将会变为void bar(X& result)
{
X xx;
xx.X::X();
result.X::XX(xx);//编译器加上的拷贝构建操作
return;
}
X x=Bar(); 将会变为 X xx;bar(xx);第一步并未调用构造函数
而当你这样去使用时:bar().memfunc()会产生一个临时对象。在局部对象返回前先拷贝给临时对象,然后再对这个临时对象进行操作。
还有一种在使用者上可做的优化就是:
当你返回一个对象时 直接return X(1,2);//忽略参数
这样我们将会省去一个局部对象的创建,以及编译器加的那步拷贝构建的操作,而将对象直接构建在等待被拷贝构造的函数外的对象、X x=bar();
第三种优化是NRV优化:
X bar()
{
X xx;
return xx;
} 既然我们是要将xx拷贝给函数外的对象,我们为什么不直接让类外的对象代替这个xx,在函数内就执行完所有的操作,而且类外的对象是以传引用的方式进入函数的。
6.初始化列表的操作总是放在显式的用户代码之前。