《深度探索c++对象模型》(二)

2018-12-21  本文已影响0人  野渡渡

前述

本章的的主题是构造函数语意学,主要是挖掘编译器对于“对象构造过程”的干涉,以及对于“程序形式”和“程序效率”上的冲击。


参考书籍及链接:《深度探索c++对象模型》


一、Default Constructor的构造操作

1. 什么时候才会合成一个default construct呢?

答案是当编译器需要的时候,default constructor会被合成出来,只执行编译器所需要的任务。另外要注意程序的需要和编译器的需要之间的区别,如果程序有需要,那是程序员的责任,就需要自己实现constructor。
对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式(implicitly)声明出来...一个被隐式声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor...
一个nontrivial default constructor在ARM的术语中就是编译器需要的那种,必要的话由编译器合成出来。下面4小节分别讨论nontrivial default constructor的4种情况

2. 几种对象构建时的区别。

Global objects的内存保证会在程序启动的时候被清0。Local objects配置于程序的堆栈中,heap objects配置于自由空间,都不一定会被清零,它们的内容将是内存上次被使用的遗迹。

3. 第一种情况:“带有Default Constructor”的member class object

如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是“nontrivial”,编译器为该class合成出一个default constructor。不过这个合成操作只有在constructor真正需要被调用时才会发生。

4. 多成员对象的情况。

编译器的处理是:如果一个class A内含一个或者一个以上member class objects,那么class A的每一个constructor必须调用每一个member classes 的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code在被执行之前,先调用必要的default constructors。调用顺序与member objects在class中的声明次序一致

5. 第二种情况:“带有Default constructor”的base class。

如果一个没有任何constructors的class派生自一个“带有default constructor”的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。对于一个后继派生的class而言,这个合成的constructor和一个“被显式提供的default constructor”并没有差异。

注意一点,如果有constructor,但没有default constructor,那就会对每一个constructors进行扩充。如果亦存在Member Class Object,那些default constructor也会在base class constructor都被调用之后调用。

6. 第三种情况:“带有一个Virtual Funtion”的class。

如果class声明(或继承)一个virtual function,编译器也需要合成出default constructor或扩充construtor。下面两个扩张行动会在编译期间发生:

编译器会为每一个含有virtual function的class objects的vptr进行适当的初始化,以放置适当的virtual table地址。

7. 第四种情况:“带有一个virtual base class”的class。

如果class派生自一个继承串链,其中有一个或更多的virtual base classes编译器也需要合成出default constructor或扩充construtor。其目的在于必须使 virtual base class 在其每一个derived class object中的位置能够在执行期准备妥当。对于class所定义的每一个constructor。编译器都会安插那些“允许每一个virtual base class 的执行期存取操作”的代码。

8. 总结。

除以上四种情况外,在没有声明constructor时就默认其是无用的, 其default constructor也就不会被合成出来的。
在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data member ,如整数,整数指针,整数数组等是不会被初始化的,这些初始化操作对程序是必须的,但对编译器则并非需要的。
C++新手一般有两个误解:

二、Copy Constructor的构造操作

1. 哪些情况需要有copy constructor?

有三种情况,会以一个object的内容作为另一class object的初值,即需要有 copy constructor。

一个class object可用两种方式复制得到,一种是被初始化,另一种是赋值。从概念上看,这两种操作分别是以copy constructor和copy assignment operator完成的。
Default constructors和copy constructor在必要的时候才由编译器 产生,这里的“必要”意指当class不展现bitwise copy sematics时。

2. Default Memberwise Initialization

当class object以“相同的另一个object作为初值是,其内部是以所谓的default memberwise initialization方式完成的。也就是把每一个内建的或派生的data member(例如一个数组或指针)的值,从某个object拷贝一份到另一个object上,但不拷贝其具体内容。例如只拷贝指针地址,不拷贝一份新的指针指向的对象,这也就是浅拷贝,不过它并不会拷贝其中member class object,而是以递归的方式实行memberwise initialization。

3. 递归的memberwise initialization是如何实现的呢?

答案就是Bitwise Copy Semantics和default copy constructor。如果class展现了Bitwise Copy Semantics,则使用bitwise copy(bitwise copy semantics编译器生成的伪代码是memcpy函数),否则编译器会生成default copy constructor。

4. Memberwise copy(深拷贝)与Bitwise copy(浅拷贝)的区别

Memberwise copy: 在初始化一个对象期间,基类的构造函数被调用,成员变量被调用,如果它们有构造函数的时候,它们的构造函数被调用,这个过程是一个递归的过程。
Bitwise copy: 原内存拷贝。例子,给定一个对象object,它的类型是class Base。对象object占用10字节的内存,地址从0x0到0x9.如果还有一个对象objectTwo,类型也是class Base。那么执行objectTwo = object;如果使用Bitwise拷贝语义,那么将会拷贝从0x0到0x9的数据到objectTwo的内存地址,也就是说Bitwise是字节到字节的拷贝。

对于默认的拷贝构造函数不会使用深拷贝,它只是使用浅拷贝。这意味着类的所有的成员是一层深度的拷贝而已。如果你的类或结构体成员中只是包含基本的数据类型例如int, float, char,那么Memberwise copy与Bitwise copy基本是相同的。但如果类中有指针存在,那么你可能会遇到问题。
例如下面的例子:

class A
{
   int m;
   double d;
   char *Str;
};

如果你创建两个这样的类对象,class A  a, b;并且你给a赋值,      
a.m = 6;   
a.d = 10.123;   
a.Str = new char[10];   
astrcpy(a.Str, "test");//这里是浅拷贝   
如果执行b = a;那么会把对象a的每一个成员的值赋值给b的每个成员。   
b.m = a.m;    
b.d = a.d;   
b.Str = a.Str;//现在对象a和b的成员Str都执向相同的内存,删除任一个内存都会析放另一个对象的内存。   

所以你需要深拷贝,它不是拷贝的内存地址而是拷贝内存地址的内容。一个默认的拷贝构造函数经常执行浅拷贝,只有拥有自己的拷贝函数才可以实现深拷贝。

5. 什么时候一个class不展现出“bitwise copy semantics”呢?

有四种情况:

6. 重新设定Virtual Table的指针(virtual funtion的情况)

当编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了。编译器需要合成出一个copy constructor,以求将vptr适当地初始化。
当一个base class object以其derived class的object内容做初始化操作时,其vptr复制操作也必须要保证安全(非pointer和reference)。也就是说,合成出来的基类构造函数会显式设定object的vptr指向基类对应的virtual table,而不是直接将右手边的class object中将其vptr现值拷贝过来。

7. 如何处理virtual base class subobject的情况?

virtual base class的存在需要特别处理。一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使“bitwise copy semantics”失效。
这时需要合成一个copy constructor,从而安插一些代码以设定virtualbase class pointer/offset的初值,对每一个members执行必要的memberwise初始化操作,以及执行其他的内存相关工作。

三、程序转化语意学(Program Transformation Semantics)

1. class object的显式初始化操作。

初始化object时,必要的程序转化有以下两个阶段:

2. 参数的初始化所做的程序转换。

C++ Standard说,把一个class object当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作:

X xx = arg;//其中xx代表形式参数(或返回值)而arg代表真正的参数值

//因此,若已知如下函数:
void foo(X xo);
 
//转换的结果为:
X xx;
//xo以memberwise的方式将xx当作初值...
foo(xx);

有一种策略是导入所谓的临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数,临时性object会在函数结束处被析构。

3. 返回值的初始化所做的程序转换。

函数bar()的返回值为一个对象,那该怎么把局部对象xx拷贝过来? Stroustrup在cfront中的解决办法是一个双阶段的转化:

4. 在编译器层面所做的优化。

编译器会以result参数取代name return val。这样的编译器优化操作,有时被称为Named Return Value(NRV)优化。NRV优化如今被视为是标准C++编译器的一个义不容辞的优化操作。NRV需要一定的条件,即对应的类要有copy constructor
一般而言,面对“以一个class object作为另一个class object的初值”的情形,语言允许编译器有大量的自由发挥空间。其优点当然是导致机器码产生时有明显的效率提升。缺点则是你不能安全地规划你的copy constructor的副作用,必须视其执行而定。

NRV与返回值初始化的区别在于:NRV中不产生local object,直接以_result带入其中进行各种处理,减少调用copy constructor。而返回值初始化则是在最后用copy constructor将local object的值拷贝给_result, 中间不处理_result。一个是优化,一个是程序转换。

5. 那Copy Constructor要还是不要?

copy constructor的应用,迫使编译器多多少少对你的程序代码做部分优化。尤其当一个函数以传值(by value)的方式传回一个class object,而该class有一个copy constructor(不论是明确定义出来的,或是合成的)时。这将导致深奥的程序转化——不论在函数的定义或使用上,此外编译器也将copy constructor的调用操作优化,以一个额外的第一参数(数值被直接存放在其中)取代NRV。

四、成员们的初始化队伍(Memeber Initialization List)

1. 在下列情况下,为了让你的程序能够顺利编译,你必须使用member initialization list:

2.member initialization list中到底会发生什么事情?

编译器会一一操作initialization list,以适当顺序在constructor之内安插初始化操作,并且在任何explicit user code之前。
initialization list中的项目顺序是由class中的members声明顺序决定的,不是由initialization list中的排列顺序决定的。

上一篇下一篇

猜你喜欢

热点阅读