关键词explicit之所以被导入这个语言,就是为了提供给程序员一种方法,使他们能够制止“单一参数的构造函数”被当做一个转换运算符。
默认构造函数的构建操作
以下四种情况类会被生成“有用的”默认构造函数
a.一个带有默认构造函数的成员对象类
合成的默认构造函数是以inline的方式完成的,如果函数太复杂则会做成非inline的static实体
b.带有默认构造函数的基类
和上述情况相似,子类没有构造函数,编译器会默认合成一个调用基类默认(无参)构造函数的子类默认构造函数
c.带有一个虚函数的class
以下两种情况需要合成出默认构造函数
1)类声明(或继承)一个虚函数
2)类派生自一个继承串链,其中有一个或多个虚基类
d.带有一个虚基类的类(菱形继承)
关于默认构造函数的合成两点是需要注意的:
1)不是任何没有定义构造函数的类都会被合成出一个默认构造函数
2)编译器合成出来的默认构造函数不会对类内每一个成员数据都设定默认值
拷贝构造函数的构建操作
有三种情况,会以一个对象的内容作为另一个类对象的初值
1.对一个对象做明确的初始化操作
2.当对象被当作参数交给某个函数时
3.当函数传回一个类对象时
一个类对象可以从两种方式复制得到,一种是拷贝构造,另一种是拷贝赋值操作(=)。
关于复制
其实复制涉及到C++内部模型的两个概念,即Default Memberwise Initialization 和bitwise copy semantics。
Default Memberwise Initialization: 这是C 模型的内部一种实现方案,其原理就是对于同一类的两个对象直接的赋值进行的暗箱操作。说白了,就是将一个对象的内存空间中的数据,原封不动的拷贝出另一份来填满另一个对象的内存。
如果该类内含其他类的对象作为自己的成员变量的话,那赋值操作并不会对该对象变量进行赋值,而是递归的对其内部数据成员赋值(原理还是有关对象内存布局)。如果类中有个指针成员变量,而其指向堆中的一片区域,然而在赋值过程中,根据memberwise的概念,只是将指针的值进行了赋值,这样一来,这两个对象中的指针变量自然都是指向同一片内存区域了,即所谓的浅拷贝。所以这时就需要程序员自己来实现拷贝构造函数来完成那片堆内存的拷贝赋值操作,即所谓的深拷贝。
不要bitwise copy semantic
什么时候一个类不展现出bitwise copy semantic呢?
1)当类内含有一个成员类对象,而这个成员类对象内有一个拷贝构造函数时(不论是class设计者明确声明,或者被编译器合成 )。
2)当类继承自 一个基类,而基类有拷贝构造函数时(不论是类设计者明确声明,或者被编译器合成 )。
3)当一个类声明了一个或多个virtual 函数时。
4)当类派生自一个继承串链,其中一个或者多个virtual基类。
在前两种情况中,编译器必须将成员或基类的“ 构造函数的调用操作” 安插到被合成的构造函数中。(就像构造函数中的操作那样)。
第三种情况,如果编译器对于每一个新产生的类对象的虚表不能成功而正确地设好其初值,将导致可怕的后果。因此,当编译器导入一个虚表到类之中时,该类就不再展现bitwise copy semantic了。如果没有 bitwise copy semantics的作用的话,很容易想到,编译器会用到默认复制来对两个对象的vptr进行复制,因为两个类对象的vp都指向同一个vptr,所以会导致可怕的后果。
这里我都被bitwise 和Memberwise 搞昏了。反正就是以上四种情况,复制不是单纯的直接复制。编译器会开辟新内存。
构造函数使用
在使用者层面做优化
一般来说是这么写的:
1 | X bar(const X& p1, const X& p2) |
如果这么写
1 | X bar(const X& p1, const X& p2) |
相比较而言,少了个copy constructor。换句话说,少用局部变量,能直接计算返回的,就直接计算返回,这样可以少产生一个拷贝构造函数。
在编译器层面做优化
在使用者层面做优化的例子,原先是使用者注意少用局部变量,能直接计算返回的,就直接计算返回,这样可以少产生一个拷贝构造函数。现在这件事情编译器代你做了,换句话说,即使我们仍用了xx做局部变量,保存计算结果,return xx,编译器也会自动把xx用_result替换掉,替你精简掉一个copy constructor。
然后呢,lippman把这种行为叫做“NRV优化”, NRV优化的本质是优化掉拷贝构造函数,当然去掉它的前提是作为使用者的我们用了xx做局部变量、return xx;如果我们没有这么做,而是直接return X(p1,p2) 那么这种行为也就不会发生了。
要不要构造函数?
其实就是bitwise和Memberwise的问题了。如果类对象的成员数据以数值来储存。bitwise copy既不会导致memory leak,也不会产生address aliasing,因此它既快速又安全。如果类需要大量的memberwise初始化操作,例如以传值的方式传回对象,那么就需要explicit了。
当然,你也可以手动memcpy,但使用前提是:在class不含任何编译器产生的内部members。例如virtual相关内容。
成员们的初始化队列
当你写下一个构造函数的时候,你有机会设定类成员的初值。要不是经由成员初始化列表,就是在构造函数本身之内。除了四种情况,你的任何选择其实都差不多。
必须使用初始化列表的情况
1)当初始化一个引用成员时
2)当初始化一个常量成员时
3)当调用一个基类的构造函数,而它拥有一组参数时
4)当调用一个类成员的构造函数,而它拥有一组参数时
第一第二是因为:const对象或引用只能初始化但是不能赋值。构造函数的函数体内只能做赋值而不是初始化,因此初始化const对象或引用的唯一机会是构造函数函数体之前的初始化列表中。
第三第四是因为:主要是性能问题,如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数。对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。
所以一个好的原则是,能使用初始化列表的时候尽量使用初始化列表。
还有,对于初始化列表有些地方需要注意:list中的生成次序是由类中成员的声明次序决定的,而不是initialization list的排列次序决定的。
全局对象的内存保证会在程序激活的时候被清为0。局部对象配置于程序的堆栈中,动态对象配置于自由空间中,都不一定会被清为0,它们的内容将是内存上次被使用后的遗迹。
reference
《深度探索C++对象模型》