1、Default Constructor的构建操作
C++standard:对于class X ,如果没有任何user-declared constructor,那么会有一个default constructor 被暗中(implicitly)声明出来........一个被暗中声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor......
一个nontrivial default constructor在ARM的术语中就是编译器所需要的那种,必要的话会由编译器合成出来。下面讨论nontrivial default constructor的四种情况。
- 带有default constructor的member class object:如果一个类没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是”nontrivial“的。
既然编译器会合成默认构造函数,那么在C++各个不同的编译模块中,编译器如何避免合成出多个默认构造函数(比如一个是a.c文件,另一个是b.c文件)呢?解决办法是吧合成的default constructor、copy constructor、destructor、assignment copy operator都以inline方式完成,一个inline函数有静态链接,不会被文件以外看到。如果函数太复杂,不适合做成inline,就会合成出一个explicit non-inline static实体。
class Foo
{
public:
Foo(),Foo(int)....
};
class Bar
{
public:
Foo foo; //内含,不是继承
char *str;
};
void foo_bar()
{
Bar bar;//Bar::foo必须在此初始化
}
被合成的Bar default constructor内含必要代码,能够调用class Foo的default constructor,但是它并不产生任何代码来初始化Bar::str,将Bar:foo初始化是编译器的责任,将Bar::str初始化是程序员的责任
程序员定义default constructor,完成初始化Bar::Bar() { str = 0; }编译器会扩张已经存在的constructors,在其中安插一些代码,使得user code在执行之前,先调用必要的default constructor://扩张之后的default constructorBar::Bar(){foo.Foo::Foo();str = 0;}如果有多个class member object都要求constructor初始化操作,C++语言要求以”member object在class中被声明的次序“来调用各个constructor。
- 带有Default Constructor的Base Class:如果一个没有任何constructor的class派生自一个带有default constructor的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。
- 带有一个VIrtual Function的Class:有两种情况,也需要合成出default constructor:
- class声明(或继承)一个virtual function
- class 派生自一个继承串链,其中有一个或更多的virtual base classes
有两个扩张会在编译期间发生:
- 一个virtual function table会被编译器产生出来,内放class的virtual functions地址
- 在每一个class object中,一个额外的pointer member会被编译器合成出来,内含相关的class vtbl的地址
此外虚拟操作会被改写,如widget.flip();会被改写成*widget.vptr[1](&widget) 1表示flip在vtbl中的固定索引,&widget代表要交给被调用的某个flip的函数实体的this指针。为了让此机制发挥功效,编译器必须为每一个widget(或其派生类)object的vptr设定初值,放置适当的virtual table地址,对于class所定义的每一个constructor,编译器会安插一些代码来做这样的事情。对于未声明任何constructor的classes,编译器合成default constructor,以便正确初始化每一个class object的vptr。
- 带有一个VIrtual Base Class的Class:Virtual Base Class的实现法在不同的编译器之间有极大差异,然而,每一种实现法必须使virtual base class在每一个derived class object中的位置,能够于执行期准备妥当。
有四种情况会导致编译器合成default constructor,C++standard把那些合成物称为implicit nontrivial default constructor,至于没有存在那四种情况而又没有声明任何constructor的classes,我们说它拥有的是implicit trivial default constructor,它们实际上并不会被合成出来。C++新手一般有两个常见的误解:1.任何class如果没有定义default constructor,就会被合成出来一个。2.编译器合成出来的default constructor会明确设定class内每一个data member的默认值
2、Copy Constructor的构建操作
当class object以相同class的另一个object作为初值时,其内部是以所谓的default memberwise(按成员) initialization手法完成的。就像default constructor一样,C++standard上说,如果class没有声明一个copy constructor,就会有隐含的声明或隐含的定义出现,C++standard把copy constructor区分为trivial何nontrivial两种,只有nontrivial的实体才会被合成于程序之中。决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的”bitwise copy semantics“
- Bitwise Copy Semantics(逐位拷贝)
Word noun("book"); void foo() { Word verb = noun; } 很显然verb是根据noun来初始化,如果class Word没有定义一个explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就得看class是否展示出”bitwise copy semantics“。举个例子,如下Word的声明: class Word { public: Word(const char*); ~Word(){ delete[] str; } private: int cnt; char* str; }; 这种情况下并不需要合成一个default copy constructor,因为上述声明展现了bitwise copy semantics,而verb的初始化操作也不需要以一个函数调用收场。 如果class Word如下声明: //以下声明并未展示出bitwise copy semantics class Word { public: Word(const String&); ~Word(); private: int cnt; String str; }; 其中String声明了一个explicit copy constructor: class String { public: String(const char*); String(const String&); ~String(); }; 这种情况下,编译器必须合成出一个copy constructor以便调用member class String object的copy constructor: //一个被合成出来的copy constructor inline Word::Word(const Word& wd) { str.String::String(wd.str); cnt = wd.cnt; }
- 不要Bitwise Copy Semantics
什么时候一个class不展现出来Bitwise Copy Semantics呢?有四种情况:
- 当class内含一个member object 而后者的class声明有一个copy constructor时(不论是被class设计者明确地声明,还是被编译器合成);
- 当class继承自一个base class而后者存在一个copy constructor时(再次强调不论是被明确声明还是被合成而得)
- 当class声明了一个或多个virtual functions时
- 当class派生自一个继承串链,其中有一个或多个virtual base class时
前两种情况,编译器需将member或base class的copy constructor调用操作安插到被合成的copy constructor中,情况3和4有点复杂。重新设定Virtual Table的指针
- 增加一个virtual function table(vtbl),内含每一个有作用的virtual function地址
- 将一个指向vtbl的指针(vptr),安插在每一个class object内
处理Virtual Base Class Subobject一个class object以另一个object作为初值,而后者有一个virtual base class subobject,也会使bitwise copy semantics失效
3、程序转化语意学
明确的初始化操作
X x0; 下面有三个定义,每一个都明显地以x0来初始化 void foo_bar() { X x1(x0); X x2 = x0; X x3 = X(x0); } 转化阶段有两个 1.重写每一个定义,初始化操作被剥除 2.class的copy constructor调用操作会被安插进去 //可能的程序转换 void foo_bar() { X x1;//定义被重写,初始化被剥除 X x2;//定义被重写,初始化被剥除 X x3;//定义被重写,初始化被剥除 //编译器安插 x1.X::X(x0); x2.X::X(x0); x3.X::X(x0); }
参数的初始化
C++Standard说,把一个class object当做参数传递给一个函数(或作为一个函数的返回值),相当于以下形式的初始化操作:X xx = arg; //xx代表形式参数(或返回值),arg代表实参,因此编译器会引入所谓临时对象,并调用copy constructor将其初始化,然后传给函数。因此,若已知函数及其调用方式如下:void foo(X x0);X xx;foo(xx);
转码之后如下:X _temp0;_temp0.X::X(xx);foo(_temp0);然而这样的转换只做了一半功夫而已,问题出在foo()的声明,临时对象先以class X的copy constructor正确设定初值,然后以bitwise方式拷贝到x0这个局部对象中。解决的办法是修改foo的声明为foo(X& x0);返回值的初始化
已知函数定义: X bar() { X xx; //... return xx; } bar()返回值如何从局部对象拷贝? Stroustrup在cfront的解决方法是一个双阶段转化: 1.加上一个额外参数,类型是reference 2.在return前安插一个copy constructor操作 //函数转化 void bar(X& _result)//加上一个额外参数 { X xx; xx.X::X();//编译器产生的default constructor调用操作 //....处理xx _result.X::XX(xx);//编译器产生的copy constructor调用 return; } 一个bar()调用被转化为: X xx = bar(); 转为: X xx; bar(xx);//注意不必调用default constructor
使用者层面做优化
定义一个“计算用”的constructor,直接计算xx的值: X bar(const T &y, const T &z) { return X(y, z); } 于是bar()定义被转换之后,效率会比较高: void bar(X &_result) { _result.X::X(y, z); return; } _restult被直接计算出来,而不是经过copy constructor拷贝
在编译器层面做优化
以_result参数取代named return value,即编译器将以上的xx以_result替代:
void bar(X &_result)
{
_result.X::X();//default constructor被调用
//....
return;
}
称为Named Return Value(NRV)优化。 由于NRV需要调用默认拷贝构造,因为需要注意不满足此前说的四种情况下,编译器不会合成默认拷贝构造,此时需要我们手动添加拷贝构造,如pulbic :inline test(const test& t);
拷贝构造要还是不要?
已知下面的3D坐标点类: class Point3d { public: Point3d(float x, float y, float z); //... private: float _x, _y, _z; }; 这个class的设计者应该提供一个explicit copy constructor吗? 上述的默认拷贝构造被视为trivial,它没有任何member(或base)class object带有copy constructor,也没有任何virtual base class或virtual function,所以,默认情况下会导致“bitwise copy”。这样效率很高,但安全吗? 答案是yes。 如果class需要大量的memberwise初始化操作,例如以传值方式返回object,那么提供一个copy constructor的explicit inline函数实体就非常合理(因为需要编译器NRV,参考上一条) 实现copy constructor的最简单方法像这样: Point3d::Point3d(const Point3d &rhs) { _x = rhs._x; _y = rhs._y; _z = rhs._z; } 这没问题,但使用C++ library的memecpy()会更有效率: Point3d::Point3d(const Point3d &rhs) { memecpy(this,&rhs, sizeof(Point3d)); } 然而不管用memcpy还是memeset,如果Point3d声明一个或以上virtual functions,或内含一个virtual base class,那么上述函数将会导致那些“被编译器产生的内部members的初值被改写(如vptr)”: 编译器的扩张看起来像是: Shape::Shape() { _vptr_shape = _vtbl_Shape; //memset会将vptr清零 memset(this, 0, sizeof(Shape)); } 因此,若要正确使用memeset和memecpy,需要掌握某些C++ObjectModel语意学知识。(什么时候能用,什么时候不能用,怎么用)
4、成员初始化列表
一下情况,必须使用member initialization list:
- 初始化一个reference member时
- 初始化const member
- 调用base class 的 constructor,而它拥有一组参数
- 调用member class的constructor,而它拥有一组参数
效率不好的使用方式:class Word { String _name; int _cnt; public: Word() { _name = 0; _cnt = 0; } }; Word constructor会先产生一个临时String object,然后将它初始化,再以一个assignment运算符指定给_name,再销毁临时object。 更有效的方法: Word::Word : _name(0) { _cnt = 0; } 会被扩展成: Word:Word() { _name.String::String(0); _cnt = 0; } 这会引导某些程序员十分积极进取地坚持所有member初始化操作必须在初始化列表中完成,甚至是一个行为良好的member如_cnt; 成员初始化列表中到底会发生什么事情?C++新手误以为它是一组函数调用,当然它不是! 编译器--操作初始化列表,在constructor内添加初始化操作,list中项目的次序是由class中member声明次序决定的,不是由初始化列表的排列次序决定的。 因此需要注意一下问题: class X { int i; int j; public: //看出问题了吗? X(int vla) : j(val), i(j) { } }; 由于声明次序的缘故,初始化列表中i(j)其实比j(val)更早执行,结果导致i无法预知其值。