构造函数语意学
程序转化语意学
显式的初始化操作
有这样的定义
cpp
X x0;
X x1(x0);
X x2 = x0;
X x3 = X(x0);
必要的程序转化有两个阶段:
- 重写每一个定义,其中的初始化操作会被剥除。
- class的拷贝构造调用操作会被安插进去。
参数的初始化
C++标准(Section 8.5)说,把一个class object当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作
cpp
X xx = arg;
其中xx代表形式参数(或返回值)而arg代表真正的参数值。
若已知这个函数以及下面这样的调用方式
cpp
void foo(X x0);
X xx;
foo(xx);
在编译器实现技术上,有一种策略是导入所谓的临时性 object,并调用拷贝构造将它初始化,然后将此临时性object交给函数。例如将前一段程序代码转换如下:
cpp
X __temp0;
__temp0.X::X(xx);
foo(__temp0);
临时性object先以class X的拷贝构造正确地设定了初值,然后再以bitwise方式拷贝到x0这个局部实例中。噢,真讨厌,foo()的声明因而也必须被转化,形式参数必须从原先的一个X对象改变为一个X引用,像这样:
cpp
void foo(X&x0);
其中class X声明了一个析构函数,它会在foo()函数完成之后被调用,对付那个临时性的object。
另一种实现方法是以"拷贝建构"(copy construct)的方式把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。
返回值的初始化
cpp
X bar(){
X xx;
...
return xx;
}
bar()的返回值如何从局部对象xx中拷贝过来
- 首先加上一个额外参数,类型是 class object的一个引用。
- 在 return指令之前安插一个拷贝构造调用操作,以便将欲传回之object的内容当做上述新增参数的初值。
现在编译器必须转换每一个bar()调用操作,以反映其新定义。
cpp
X xx = bar();
|
|
V
X xx;
bar(xx);
在编译器层面做优化
在一个像bar()这样的函数中,因此编译器有可能自己做优化,方法是以result参数取代named return value。
cpp
void bar(X &__result){
//构造函数被调用
__result.X::X();
...//直接处理__result
return;
}
这样的编译器优化操作,有时候被称为Named Return Value(NRV)优化。NRV优化如今被视为标准C++编译器的一个义不容辞的优化操作------虽然其需求其实超越了正式标准之外。
有的个程序的第不能实施 NRV 优化,因为 test class 缺少一个拷贝构造。
虽然NRV优化提供了重要的效率改善,但它还是饱受批评。
想象你已经摆好了你的拷贝构造的阵势,使你的程序"以copying方式产生出一个object时",对称地调用析构
cpp
void foo{
//这里希望有个拷贝构造
X xx = bar();
...
//这里调用析构
}
在此情况下,对称性被优化给打破了;程序虽然比较快,却是错误的。
Copy Constructor:要还是不要?
cpp
class Point3d{
public:
point3d(float x , float y , float z);
private:
float _x, _y, _z;
}
这个class的设计者应该提供一个显式拷贝构造吗?
答案当然很明显是no。没有任何理由要你提供一个拷贝构造函数实例,因为编译器自动为你实施了最好的行为。比较难以回答的是,如果你被问及是否预见class需要大量的memberwise初始化操作,例如以传值(by value)的方式传回objects?如果答案是yes,那么提供一个构造的显式inline函数实例就非常合理------在"你的编译器提供NRV优化"的前提下。
成员们的初始化队伍(成员初始化列表)
在下列情况下,为了让你的程序能够被顺利编译,你必须使用成员初始化列表
- 当初始化一个 reference成员时;
- 当初始化一个 const成员时;
- 当调用一个 base class的构造,而它拥有一组参数时;
- 当调用一个 member class的构造,而它拥有一组参数时。
- list内部的真正操作是什么
编译器会一一操作初始化列表,以适当顺序在构造之内安插初始化操作,并且在任何显示用户代码之前。
事实上,有一些微妙的地方要注意:list中的项目顺序是由class中的members声明顺序决定的,不是由初始化列表中的排列顺序决定的。