深度探索C++对象模型笔记(二)

来源:互联网 发布:数控编程30度倒角公式 编辑:程序博客网 时间:2024/04/29 08:39

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 constructor
Bar::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
  1. class声明(或继承)一个virtual function
  2. class 派生自一个继承串链,其中有一个或更多的virtual base classes
有两个扩张会在编译期间发生:
  1. 一个virtual function table会被编译器产生出来,内放class的virtual functions地址
  2. 在每一个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 semanticsclass 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 constructorinline Word::Word(const Word& wd){ str.String::String(wd.str);cnt = wd.cnt;}
  • 不要Bitwise Copy Semantics
什么时候一个class不展现出来Bitwise Copy Semantics呢?有四种情况:
  1. 当class内含一个member object 而后者的class声明有一个copy constructor时(不论是被class设计者明确地声明,还是被编译器合成);
  2. 当class继承自一个base class而后者存在一个copy constructor时(再次强调不论是被明确声明还是被合成而得)
  3. 当class声明了一个或多个virtual functions时
  4. 当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.加上一个额外参数,类型是reference2.在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无法预知其值。


原创粉丝点击