第2章 构造函数语意学
2.1 Default Constructor的构建操作
1. global objects的内存保证会在程序激活的时候被清为0。local objects配置于程序的堆栈中,heap objects配置于自由空间中,都不一定会被清为0,它们的内容将是内存上次被使用后的遗迹。
2. 以下四种情况,编译器必须为未声明constructor的classes合成一个implicit nontrivial default constructor:
1)带有default constructor的member class object:成员变量的类带有默认构造函数,这是,编译器会扩张已经存在(或并不存在的)构造函数,插入代码来调用成员变量的构造函数。如果被调用的类没有默认构造函数,会报错;
2)带有default constructor的base class:基类带有默认构造函数,这时,按照声明顺序依次调用基类的构造函数;
3)带有virtual function的类:新定义的或继承而得到虚函数,Vptr(每个对象)和Vtbl(每个类)会被构造出来;
4)带有virtual base class的类:有直接虚拟基类或继承链上有虚拟基类。
上述四种情况合成的implicit nontrivial default constructor
只能满足编译器而非程序的需要。
其它各种情况且没有声明任何constructor的classes,它们拥有的是implicit trival default constructors,它们实际上并不会被合成出来。
编译器合成implicit nontrivial default constructor,不过是暗地里作了一些重要的事情以保证程序正确合理地运行。如果程序员提供了多个constructors,但其中都没有default constructor,编译器同样会在这些constructors中插入一些相同功能的代码,这些代码都将被安插在explicit user code之前。
3.误解1:任何class如果没有定义默认构造函数,都会被合成出一个来。
错,只能是2中所述的四种情况。
误解2:编译器合成出来的默认构造函数会明确设定类中每一个数据成员的默认值。
错。在合成的默认构造函数中,只有base class subobjectes和member class objects会被初始化。所有其它的非静态数据成员,如:整数、整数指针、整数数组等等都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则并非必要。
2.2 Copy Constructor的建构操作
1.有三种情况,会以一个object的内容作为另一个 class object 的初值:
1)对一个object做明确的初始化操作;
2)当object被当做参数交给某个函数时;
3)当函数传回一个class object时。
即:X x2 = x1;
foo(x2);
return x2;
若class设计者明确定义了一个copy constructor,大部分情况下,该constructor会被调用。这可能导致一个暂时性的class object的产生或程序代码的蜕变,或者两者皆有。
2.如果class没有提供一个explicit copy constructor,当class object以相同class的另一个object作为初值时,其内部是以所谓的default memberwise initialization手法完成的,即把每一个内建的或派生的data member的值,从一个object拷贝到另一个object。不过,它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。
3. 一个class object可以从两种方式复制得到:初始化和指定,从概念上而言,这两个操作分别是以copy constructor和copy assignment operator完成的。
4.如果class没有声明一个copy constructor,就会有隐含的声明implicitly declared或隐含的定义implicitly defined出现。C++把copy constructor分为trivial和nontrivial两种。只有nontrivial的实体才会被合成出来。决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。
5.以下四种情况,一个class不展现出“bitwise copy semantics”,这时,nontrival copy constructor会被构造出来:
1)成员变量的类带有(不论是何原因而有)拷贝构造函数,即:有“对象”成员,而非只有基本数据类型成员;
2) 基类带有拷贝构造函数;
3)带有或继承而得到虚函数。重新设置vptr,并且,如果该对象是第一个对象的话还会构造vtbl;
4)有直接虚拟基类或继承链上有虚拟基类。
前两种情况中,编译器必须将member或base class的copy constructors调用操作安插到被合成的copy constructor中。
6.编译期间的两个程序扩张操作(只要有一个class声明了一个或者多个virtual functions就会如此):
(1)增加一个virtual funtion table(vtbl),内含每一个有作用的virtual function的地址;
(2)将一个指向vtbl的指针vptr,安插在每一个class object内。
7.一
旦一个
class object
中必须引入
vptr
,编译器就必须为它的
vptr
正确地设置好初值。此时,该
class
就不再展现
bitwise semantics
。
8.
当一个
base class object
以其
derived class object
内容作初始化操作时,其
vptr
复制操作必须保证安全。
2.3 程序转化语意学
1. 每一个明确的初始化操作都会有两个必要的程序转化阶段:先重写每一个定义,剥除其中的初始化操作,然后安插class的copy constructor调用操作。
2.把一个class object 当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作:
X xx = arg;
其中xx代表形式参数(或返回值)而arg代表真正的参数值。
3.函
数定义如下:
X bar()
{X xx;
return xx;
}
bar()
的返回值通过一个双阶转化从局部对象
xx
中拷贝出来:
首先为
bar
添加一个额外参数,类型是
class object
的一个
reference
,这个参数用来放置被拷贝构建而得的返回值。
ü 然后在return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当作上述新增参数的初值,同时重写函数使它不返回任何值。
4.Named Return Value(NRV)优化如今被视为是标准C++编译器的一个义不容辞的优化操作,它的特点是直接操作新添加的额外参数。注意只有copy constructor的出现才会激活C++编译器的NRV优化!NRV优化虽然极大地改善了效率,但还是饱受批评:一是优化由编译器默默完成,而是否完成以及其完成程度完全透明;二是一旦函数变得比较复杂,优化就变得较难施行;三是优化由可能使程序产生错误——有时并不是对称地调用constructor和destructor,而是copy constructor未被调用!
5.在编译器提供NRV优化的前提下,如果可以预见class需要大量的memberwise初始化操作,比如以by value的方式传回objects,那么提供一个explicit inline copy constructor的函数实体就非常合理。此种情况下,没有必要同时提供explicit assignment operator定义。
6.不管使用memcpy()或memset(),都只有在“classs不含任何由编译器产生的内部members”时才能有效运行。如果class声明一个或一个以上的virtual functions,或内含一个virtual base class,那么使用上述函数会导致那些“被编译器产生的内部members”的初值被改变。
2.4 成员们的初始化队伍
1.当写下一个constructor时,就有机会设定class members的初值。不是经由member initialization list,就是在constructor函数本身之内。
2.下列情况,为了让程序能被顺利编译,必须使用member initialization list:
1)初始化一个reference member时;
2)初始化一个const member时;
3)调用一个base class的constructor,而它拥有一组参数时;
4)调用一个member class的constructor,而它拥有一组参数时。
3.编译器会对initialization list一一处理并可能重新排序,以反映出members的声明次序,它会安插一些代码到constructor内,并置于任何explicit user code之前。
4.一个忠告:请使用“存在于constructor体内的一个member”,而不是“存在于member initialization list中的一个member”,来为另一个member设定初值。