您的位置:首页 > 编程语言 > C语言/C++

《深入探索C++对象模型》读书笔记——第二章 构造函数语意学

2017-11-29 15:42 381 查看
第二章 构造函数语意学The Semantics of Constructors

1. Jerry Schwarz,iostream函数库建构师,曾为了让cin能够求得一个真假值,于是他为它定义了一个conversion运算符operator int()。但在语句cin << intVal中,其行为出乎意料:程序原本要的是cout而不是cin!但是编译器却找到一个正确的诠释:将cin转型为整型,现在left shift operator <<就可以工作了!这就是所谓的“Schwarz Error”。Jerry最后以operator void *()取代operator int()。

2. 引入关键词explicit的目的,就是为了提供程序员一种方法,使他们能够制止单一参数的constructor被当作一个conversion运算符。其引入是明智的,但其测试应该是残酷的!

2.1 Default Constructor的构建

1.global objects的内存保证会在程序激活的时候被清为0。local objects配置于程序的堆栈中,heap objects配置于自由空间中,都不一定会被清为0,它们的内容将是内存上次被使用后的遗迹。

2.“default constructors在需要的时候被编译器产生出来”。关键字眼是“在需要的时候”。是被编译器需要。

对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式声明出来。一个被被隐式声明出来的default constructor将是一个trivial(没啥用的)constructor……

3. 以下四种情况,编译器必须为未声明constructor的classes合成一个implicit nontrivial default constructor:

(1)带有default constructor的member class object

(2)带有default constructor的base class

(3)带有virtual function

(4)带有virtual base class

其它各种情况且没有声明任何constructor的classes,它们拥有的是implicit trival default constructors,它们实际上并不会被合成出来。

4、带有default constructor的member class object

(1)在各个不同的版本模块中,编译器避免合成出多个default constructor的方法如下:

把合成的default constructor、copy constructor、assignment copy operator都以inline方式完成。一个inline函数有静态链接,不会被档案以外者看到。如果函数过于复杂,不适合做成inline,就会合成一个explicit non-inline static实体。

(2)

被合成出来的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但它并不产生任何代码来初始化Bar::str。

将Bar::foo初始化是编译器的责任,将Bar::str初始化则是程序员的责任。

被合成出来的default constructor只满足编译器的需要,而不是程序员的需要。为了让程序片段正确执行,字符指针str也需要被初始化。

编译器会扩张已存在的constructor,在其中安插一些代码,是的user code被执行之前,先调用必要的default constructors。

(3)如果有多个class member objects都要求constructor初始化操作,C++语言要求以“member objects在class中的声明顺序”来调用各个constructors。

5、带有default constructor的base class

如果一个没有任何constructor的class派生自一个“带有default constructor”的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base classes的default constructor(根据他们的声明顺序)。

6、带有一个Virtual Function的class

有两种情况,也需要合成出default constructor

(1)class声明(或继承)一个virtual function

(2)class 派生自一个继承串链,其中有一个或更多的virtual base classes。

由于缺乏由user声明的constructors,编译器会详细记录合成一个default constructor的必要信息。

此外,widget.flip()的虚拟调用操作会被重新改写,以使用widget的vptr和vtbl中的flip()条目:

(*widget.vptr[1])(&widget)

其中:(1)1表示flip()在virtual table中的固定索引

(2)&widget代表要交给“被调用的某个flip()函数实例”的this指针。

7、带有一个virtual base class的class

必须使virtual base class在每一个derived class object中的位置,能够于执行期准备妥当。

所有经由reference或pointer来存取一个virtual base class的操作都可以通过相关指针来完成。

其中_vbcX表示编译器所产生的指针,指向virtual base class X

_vbcX(或编译器所做出的某个东西)是在class object构造期间被完成的。对于class所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行器存取操作”的代码。如果class没有声明任何constructors,编译器必须为它合成一个default constructor。

总结:

有四种情况会造成“编译器必须为声明constructor的classes合成一个default constructor”。把这些合成物称为implicit nontrivial default constructors。被合成出来的constructor只能满足编译器(而非程序)的需要。

没有存在那四种情况而又没有声明任何constructor的classes,我们说他们拥有的是implicit trivial default constructors,他们实际上不会被合成出来。

在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data member(如整数、整数指针、整数数组等)都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则非必要。

新手误解:

(1)任何class如果没有定义default constructor,就会被合成出一个来。

(2)编译器合成出来的default constructor会显式设定“class内每一个data member的默认值”。

——都不对。

8、编译器合成implicit nontrivial default constructor,不过是暗地里作了一些重要的事情以保证程序正确合理地运行。如果程序员提供了多个constructors,但其中都没有default constructor,编译器同样会在这些constructors中插入一些相同功能的代码,这些代码都将被安插在explicit user code之前。

2.2 Copy Constructor的构造操作

0、一个class object可以从两种方式复制得到:初始化和指定,从概念上而言,这两个操作分别是以copy constructor和copy assignment operator完成的。

1、Default memberwise initialization

如果class没有提供一个explicit copy constructor又当如何?当class object以“相同class的另一个object”作为初值,其内部是以所谓的default memberwise initialization首发完成的,也就是把每一个内建的或派生的data member的值,从某一个object拷贝一份到另一个object身上。不过他并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。

Default constructors和copy constructors在必要的时候才由编译器产生出来。“必要”是指当class不展现bitwise copy semanics时。

决定一个copy constructor是否为trival的标准在于class是否展现出所谓的“bitwise copy semanics”。

2、Bitwise Copy Semanics(位逐次拷贝)

这种情况并不需要一个default copy constructor,因为上述声明展现了“default copy semanics”

这种情况下,编译器必须合成出一个copy constructor,以便调用member class string object的copy constructor:

3.不要 “bitwise copy semantics”

以下四种情况,一个class不展现bitwise copy semantics:

(1)class内含一个member object而后者的class声明有或被编译器合成有一个copy constructor时;

(2)class继承自一个base class而后者存在或被编译器合成有一个copy constructor时;

(3)当class声明了一个或多个virtual functions时;

(4)当class派生自一个继承串链,其中有一个或多个virtual base classes时。

前两种情况中,编译器必须将member或base class的copy constructors调用操作安插到被合成的copy constructor中。

4、重新设定virtual Table的指针

(1)一旦一个class object中必须引入vptr,编译器就必须为它的vptr正确地设置好
4000
初值。此时,该class就不再展现bitwise semantics。

(2)当一个base class object以其derived class object内容作初始化操作时,其vptr复制操作必须保证安全。

5、处理virtual Base Class Subject

在每一个编译器对于虚拟继承的支持承诺,都代表必须让“derived class object中的virtual base class subobject位置”在执行期就准备妥当。维护位置的完整性是编译器的责任。

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. copy constructor的应用迫使编译器多多少少对程序代码作部分优化,尤其是当一个函数以by value的方式传回一个class object,而该class有一个copy constructor(或定义或合成)时,无论在函数的定义还是在使用上将导致深奥的程序转化。此外,编译器将实施NRV优化。

7. 注意正确使用memset()和memcpy(),它们都只有在classes不含任何由编译器产生的内部members如vptr时才能有效运行!

2.4 成员初始化列表

1. 当写下一个constructor时,就有机会设定class members的初值。不是经由member initialization list,就是在constructor函数本身之内。

2. 下列情况,为了让程序能被顺利编译,必须使用member initialization list:

ü 初始化一个reference member时;

ü 初始化一个const member时;

ü 调用一个base class的constructor,而它拥有一组参数时;

ü 调用一个member class的constructor,而它拥有一组参数时。

3. 编译器会对initialization list一一处理并可能重新排序,以反映出members的声明次序,它会安插一些代码到constructor内,并置于任何explicit user code之前。

4. 一个忠告:请使用“存在于constructor体内的一个member”,而不是“存在于member initialization list中的一个member”,来为另一个member设定初值。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++
相关文章推荐