2.1 默认构造函数

首先指出两个误解:

1)任何类如果没有定义默认构造函数,就会被合成出来一个。

2)编译器合成出来的默认构造函数会显式设定“类内每一个数据成员的默认值”。

上述两种说法都是错误的!

四种nontrivial的默认构造函数:

1)类成员中有成员是类对象,并且该成员的类含有默认构造函数。那么C++编译器会给这个类也生成一个默认构造函数,用来调用其成员对象的构造函数,完成该成员的初始化构造,但不会初始化当前类中所含的其他值(是程序员的责任)。P42-44写的比较明白

//避免合成出多个默认构造函数:使用inline方式完成。

2)这个类的基类有默认构造函数。那么C++编译器也会帮你生成该派生类的默认构造函数,以调用基类的默认构造函数,完成基类的初始化。如果基类没有提供这个默认构造的函数,那么编译器也不会为派生类生成默认的构造函数(这里包括两层意思,第一,基类没有任何形式构造函数;第二,基类存在其他形式的非默认构造函数,这种类型就是编译不过的,道理很明显)。

3)类中存在虚函数(新定义或继承而得到)。那么C++编译器会为你生成默认构造函数,在编译期生成虚表和虚表指针。

4)存在虚基类(有直接虚拟基类或继承链上有虚基类)。那么C++编译器会为你生成默认构造函数,以初始化虚基类表(vbtable)。

这四种情况之外,且没有声明任何constructor的类,可以说它有无用的构造函数,但实际上它根本就不会被构建出来。

------------------------------------------------------------------

2.2 拷贝构造函数

三种情况出现默认拷贝构造函数:

1.显示的对对象进行初始化

2.当object被当作参数交给某个函数

3.当函数返回一个class object时

位逐次拷贝(bitwise copy semantics)

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

也就是,如果class中展现了位逐次拷贝,编译器就不会产生出拷贝构造函数。当没有产生拷贝构造的时候,我们的类怎么产生呢。就是通过 bitwise copy 来搞定,也就是将源类中成员变量中的每一位都逐次拷贝到目标类中,这样我们的类就构造出来了。

来看一下什么是位逐次拷贝

对于第二种情况,类中含有String类的对象,因此此时并没有位逐次拷贝,需要一个拷贝构造函数

类在下述四种情况下不展现位逐次拷贝(这时才会而且有必要生成拷贝构造):

1)类中含有成员类对象,并且此类对象含有默认构造函数。即:有“对象”成员,而非只有基本数据类型成员。

2)基类带有拷贝构造函数。

3)类中存在虚函数(新定义或继承而得到)。重新设置vptr,并且,如果该对象是第一个对象的话还会构造vtbl。

4)存在虚基类(有直接虚拟基类或继承链上有虚基类)。

前两种情况下,都是需要生成该类的拷贝构造去调用类成员对象或基类的拷贝构造函数;后两种情况下,需要编译器来完成虚函数表(vbtl)的初始化和虚表指针(vptr)的初始化,所以如果没有显式的定义构造函数,需要编译器构造默认的构造函数。

重新设定虚表指针

如果同类对象对同类对象进行初始化,没有问题,位逐次拷贝已经足够,但是如果是例如使用一个派生类的对象对基类的对象进行初始化(此时会发生切割),那么必须使用拷贝构造函数

------------------------------------------------------------------

2.3程序转化语义学

1.参数的初始化

对于这里P63的例子我有一些疑问,是否一定需要把参数从X类对象改为X类对象的引用?

个人感觉是不用的,只是说如果传的不是引用类型的话,会使得参数需要先复制一份,那么如果传引用参数,可以节省拷贝构造函数和析构函数的开销??

2.返回值的初始化

 

对于这样的一个函数bar,内部的一个X类对象xx,那么对于其返回值,是如何从这个局部对象xx中拷贝过来的呢?

(1)首先加上一个额外的参数,类型是类对象的一个引用

(2)在return之前其实调用了拷贝构造函数,将局部对象xx的值拷贝给了引用对象

(3)对于真实的return,其实并没有返回值

也就是说,真实的过程如下:

 那么同样的,对于下面的调用,其实也进行了同样的转化实现

对于上面这个,其实不太理解后一个_temp0这样的写法,个人觉得是为了示意前一部分产生了一个X类的对象,调用的是这个对象的menfunc函数?

 3.在使用者层面做优化

对于第一种写法,就像我们前面提到过的,实际的bar函数return是void的,因此对于内部这个xx临时对象,实际上我们是先生成他,然后处理完之后拷贝给_result的引用,这样比较慢;而对于第二种,我们没有生成这个临时对象xx,直接使用y和x来进行处理,因此效率比较高

4.在编译器层面做优化

NRV优化,也就是前面说到的,本来我调用bar函数,我先对临时对象xx进行操作,然后在拷贝给_result,但此时编译器不进行xx的操作,直接对于_result进行操作

这里来看三个初始化的操作:


对于第一个xx0,没有疑问,产生一个临时对象,然后设定初值1024。那么对于第二第三个来说呢,将临时对象以拷贝构造的方式作为explict对象的初值(也就是说调用了两个构造函数)。

使用memcpy()或者memset()函数的时候,如果类中含有虚函数或者虚基类,此时要特别注意,这两个函数不能有效运行,会导致vptr的初值被改写(因为例如用memset来进行清零操作,由于vptr需要在所有代码执行之前安排好,那么当vptr确定之后,我使用memset置零,此时会导致vptr又被置零了,此时是会发生错误的!!)

2.4成员们的初始化队伍

初始化列表

下面四种情况必须使用初始化列表来初始化class 的成员:

1.当初始化一个reference member时;

2.当初始化一个const member时;

3.当调用一个base class 的 constructor ,而它拥有一组参数(其实就是自定义的构造函数)时;

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

那么来看为什么要使用初始化列表:

对于一般的在函数体内进行初始化,操作过程应该是先产生一个临时的类对象,然后对这个类对象进行初始化,之后由这个临时对象来进行初始化值,最后摧毁这个临时对象。

来看:

要注意的一点:list中的项目顺序是类中的成员声明顺序决定的,而不是由参数列表中的排列顺序所决定的。所以写的时候注意养成良好的习惯,两者的顺序一致!!

对于类似下面这样的情况,比如说声明是i,j的顺序,之后使用j来初始化i,val来初始化j

 上面这个第二种写法其实是正确的,因为参数列表的项目都是放在explict的自写代码之前的,因此j先被赋值,再来赋值i

另外要注意的一点:

能否调用一个函数来设定一个成员的值?

此时建议:使用构造函数体内的成员函数可以,但是尽量不要使用存在于参数列表中的成员函数,因此此时并不知道这个成员函数对于类对象的依赖性有多高。

另外一点:对于派生类成员函数进行调用,其返回值作为基类构造函数的一个参数的情况,不推荐。