深度探索C++对象模型 2构造函数语意学
第二章 构造函数语意学
本章大量出现的英语术语:
trivial: 没有用的
nontrivial:有用的
memberwise: 对每一个member施以…
bitwise: 对每一个bit施以…
named return value optimization (NRV)
2.1Default Constructor的构造操作
对于一个类,如果没有任何构造函数的声明,那么会有一个default constructor被隐式声明出来。一个隐式声明出来的default constructor是trivialconstructor。但编译器需要时,会合成一个nontrivialdefault constructor。有四种情况会合成nontrivialdefault constructor。
1. 带有defaultconstructor的member class object
如果一个class没有任何constructor,但它内含一个member object,而后者有defaultconstructor,那么这个class的implicit default constructor就是nontrivial,编译器需要为该class合成一个default constructor。不过合成操作只有在constructor真正需要被调用时才会发生。
在各个C++不用模块中如何避免合成出多个default constructor:解决办法是把合成的defaultconstructor、copy constructor、assignment copy operator都以inline方式完成。一个inline函数有静态链接,不会被文件外者看到。如果函数太复杂,不会适合做inline,就会合成一个explicitnon-inline static实例。例如:
class Foo {public: Foo(), Foo( int ) … };
class Bar {public: Foo foo; char *str;};
Bar bar; //Bar::foo必须在此处初始化,Bar::foo是一个memberobject,并且有default //constructor。故编译器合成defaultconstructor。
此处,将Bar::foo初始化是编译器的责任,将Bar::str初始化则是程序员的责任。故合成
的default constructor看起来像这样:
inline Bar::Bar(){
foo.Foo::Foo();
}
但如果程序员提供了一个default constructor,如下:
Bar::Bar(){ str= 0; }
由于已经存在一个default constructor,所以编译器没法合成第二个。编译器的行动是:如果类内含有一个或一个以上的member class objects,那么类的每一个constructor必须调用每一个member classes的defaultconstructor;编译器会扩展已存在的constructors,在其中安插一些代码,是的user code被执行之前,先调用必要的defaultconstructor。
则上面扩张后可能像这样:
Bar::Bar(){
foo.Foo::Foo();
str = 0;
}
如果有多个class member objects都要求constructor初始化操作,C++语言要求member objects在class中声明的顺序来调用各个constructors。这一点由编译器完成,它为每一个constructor安插代码程序,以member声明顺序调用每一个member所关联的default constructors。这些代码将被安插在explicit user code之前。
2.带有defaultconstructor的base class
如果一个没有任何constructor的class派生自一个带有default constructor的base class,那么这个default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base class的defaultconstructor(根据它们的声明顺序)。
如果有多个constructors,但其中都没有defaultconstructor,编译器会扩张先有的每一个constructors,将用以调用所有必要的default constructors的程序代码加进去。如果同时又存在着带有default constructor的member classobjects,那些default constructors也会在所有base class constructors都被调用之后被调用。
3.带有一个virtualfunction 的class(声明或继承)
class Widget{
public:
virtualvoid flip () = 0;
};
void flip( const Widget & widget ){widget.flip();}
//假设Bell和Whistle是Widget的派生类
void foo(){
Bellb;
Whistle w;
flip( b );
flip( w );
};
下面两个扩张行动会在编译期间发生:
1) 一个virtual function table(vtbl)会在编译器产生出来,内放class的virtual function地址。
2)在每一个class object中,一个额外的pointermember会被编译器合成出来,内含相关的classvtbl地址。
此外,widget.flip()的虚拟调用操作会被重写,以使用widget的vptr和vtbl中的flip()条目。
(*widget.vptr[ 1 ] )( &widget );
为了让这个机制(虚拟机制)发挥功效,编译器必须为每一个Widget object的vptr设定初值,放置适当的virtual table地址。对于class 所定义的每一个constructor,编译器会安插一些代码来做这样的事情(见5.2节)。对于那些未声明任何constructors的classes,编译器会为它们合成一个defaultconstructor,以便正确初始化每一个class object的vptr。
4.带有一个virtual baseclass的class
Virtual base class的实现方法在不同的编译器之间有极大的差异。然而,每一种实现法的共同特点在于使virtual base class在其每一个derivedclass object中的位置,能够于执行期准备妥当。例如:
class X { public: int i; };
class A:public virtual X { public: int j;};
class B:public virtual X { public: doubled;};
class C:public A, public B { public: intK;};
void foo( const A * pa){ pa->i = 1024; } //无法再编译时期决定出pa->X::i的位置
main(){
foo(new A );
foo(new C );
}
编译器无法固定foo()之中“经由pa而存取的X::i”的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变执行存取操作的那些代码,是X::i可以延迟至执行期才决定下来。原先cfront的做法是靠“在derived class object的每一个virtual baseclasses中安插一个指针”完成。所有“经由reference或pointer来存取一个virtualbase class”的操纵都可以通过相关指针完成。在我的例子中,foo()可以被改写如下,以符合这样的策略:
//可能的编译器转变操作
void foo( const A* pa ){ pa->_vbcX->I= 1024; }
其中,_vbcX表示编译器所产生的指针,指向virtualbase class X。
_vbcX(或编译器所作出的某个东西)是在class object构造期间被完成的。对于class所定义的每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码。如果class没有声明任何constructors,编译器必须为它合成一个defaultconstructor。
总结:有四种情况编译器必须为未声明constructor的class合成一个default constructor。其他情况,我们说class拥有implicit trivaldefault constructor,他们实际上并不会被合成出来。
在合成的default constructor中,只有base classsubobject和member class object会被初始化。所有其他的nonstatic data member(如整数、整数指针、整数数组)都不会被初始化。这些nonstatic data member初始化操作队程序而言或许有需要,但对编译器而言非必要。
C++新手常见的两个误解:
1.任何class如果没有定义default constructor,就会合成一个
2.编译合成出来的defaultconstructor会显式设定“class 内每一个data member的默认值”。
如你所见,没有一个是真的!
2.2 CopyConstructor的构造操作
以一个object的内容作为另一个class object的初值的三种情况:
- 对一个object做显示初始化操作
- 当object被当做参数交给某个函数时
- 当函数传回一个class object时
假如class设计者显示定义了一个copyconstructor,大部分情况下,一个object的内容作为另一个同类object的初值时,上述copy constructor会调用。
Default Memberwise Initialization
当class object以“相同class的另一个object”作为初值,其内部是以所谓的defaultmemberwise initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的memberclass object,而是以递归的方式施行memberwiseinitialization。
C++Standard上说,如果class没有声明一个copy constructor,就会有隐式的声明或隐式的定义出现。和以前一样,C++把copy constructor区分为trivial和nontrivial。只有nontrivial的实例才会被合成与程序中。决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的bitwise copysemantics。
bitwisecopy semantics(位逐次拷贝)
classWord{
public:
Word(const char* );
~Word(){ delete [] str; }
private:
intcntl
char*str;
};
上面这个类的一个实例作为另一个该类的实例的初值时,不会合成一个default copy constructor,因为上述声明展现了bitwise copy semantics。
classWord{
public:
Word(const String& );
~Word();
private:
intcnt;
Stringstr;
};
上面这个类中的string有一个explicit copyconstructor,当该类一个实例作为另一个该类的实例的初值时,会合成一个default copy constructor,以便调用member class String object的copy constructor。不展现bitwise copysemantics。被合成出来的copy constructor的伪代码如下:
inlineWord::Word( const Word& wd )
{
str.String::String( wr.str );
cnt = wd.cnt;
}
在合成出来的copy constructor中,如整数,指针,数组等的nonclassmembers也都会被复制。
不展现Bitwise Copy Semantics!
1.当一个class内含一个member object而后者的class声明有一个copy constructor时(不论是显示声明还是编译器合成)
2.当class继承自一个base class而后者存在一个copy constructor时
3.当class声明一个活多个virtual functions时
4.当class派生自一个继承串链,其中有一个或多个virtualbase classes时
前两种情况,编译器必须将member或base class的copy constructor调用操作安插到被合成的copyconstructor中。后两种情况接下来讨论。
重新设定virtual table的指针
当一个class声明一个或多个virtualfunctions,编译器会进行两个扩张操作:
1.增加一个virtualfunction table(vtbl),内含每一个有作用的virtualfunction的地址。
2.一个指向virtualfunction table的指针,安插在每一个class object内。
如果编译器对于每一个新产生的class object的vptr不能成功而正确地设好其初值,将导致可怕的后果。因此,当编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了,过需要合成一个copy constructor以将vptrs适当地初始化。如:
当父类对象一另一个父类对象作为初值或子类对象以另一个子类对象作为初值时,都可以直接依靠bitwise copy semantics完成。如
Bearyogi;
Bearwinnie = yogi; //把yogi的vptr直接拷贝给winnie的vptr是安全的。
当父类对象以子类对象作为初始化时,其vptr复制操作也必须保证安全。如
ZooAnimalfranny = yogi; //这里会发生切割行为
不能将franny 的vpt指向Bear claass的virtual table。也就是说合成出来的ZooAnimal copy constructor会显示设定object的vptr指向ZooAnimal Class的virtual。
处理VirtualBase Class Subobject
一个class object如果以另一个object作为初值,而后者有一个virtual base class subject,那么也会使“bitwise copy semantics”失效,所以编译器必须合成一个copyconstructor,安插一些代码以设定virtual baseclass pointer/offset的初值,对每一个members执行必要的memberwise初始化操作,以及执行其他的内存相关工作。如果是两个同一级别的同类对象(也就是说两个对象都是一个类的实例,不分别是子类和父类的实例),那么bitwise copy就绰绰有余了。
2.3程序转化语意学
#include “X.h”
X foo()
{
Xxx;
Returnxx;
}
已知上述程序,一个人可能会做出如下假设:
1.每次foo()被调用,就传回xx的值
2.如果class X定义了一个copy constructor,那么当foo()被调用时,保证该copy constructor也会被调用。
实际上,上诉两个假设都不一定正确。第一个假设必须看class X如何定义而定,见本节如下的返回值初始化部分。第二个假设可能会被NVR优化,见本节在编译器层面做优化部分。
显示的初始化操作:
X x0;
Void foo_bar(){
Xx1(x0);
Xx2 = x0;
Xx3=X(x0);
}
必要的程序转化有两个阶段:
1.重写每一个定义,其中的初始化操作会被剥除。(这里的定义指“占用内存的行为”)
2.class的copy constructor调用操作会被安插进去。
//可能的程程序转化,伪码
Void foo_bar(){
Xx1; //定义被重写,初始化操作被剥除,也就是没有调用默认构造函数,下同。
Xx2; //定义被重写,初始化操作被剥除
X x3; //定义被重写,初始化操作被剥除
//安插X copy constructor调用操作。
x1.X::X(x0);
x1.X::X( x0);
x1.X::X( x0); //其实这里会x0不正确。会首先通过x0产生一个临时变量,临时变量通//过copy constructor赋值给x1.
}
X xx0(1024);
X xx1=X(1024);
X xx2=(X)1024;
第二行和第三行语法明显提供了两个步骤的初始化操作:
1.将一个临时性的object设以初值1024
2.将临时性的object以拷贝构造的方式作为explicit object的初值
换句话说xx0是被单一的constructor设定初值
xx0.X::X(1024);
而xx1和xx2的初始化转化如下:
X _temp0;
_temp0.X::X(1024);
xx1.X::X(_temp0);
_temp.X::~X();
参数的初始化:
已知函数
Void foo( X x0);
下面这样调用函数
X xx;
foo(xx);
则其中一种实现策略是导入临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数,如下:
X _temp0;
_temp0.X::X( xx );
foo( _temp0 );
但这样的转换还不到位,foo()的声明也需要被转化,形式参数必须从原先的一个class X object改变为class X 引用。如下:
void foo( X& x0);
在foo函数完成后,会掉用析构函数对付临时性的object。
另一宗实现策略是拷贝建构,把实际参数直接建构在相应位置上,记录于程序堆栈中。
返回值的初始化:
X bar(){
X xx;
//处理xx
return xx;
}
返回值如何从局部对象拷贝过来,Stroustrup解决办法是一个双阶段转化
1.首先加上一个额外参数,类型是class object的一个reference,用来存放拷贝建构而得的返回值。
2.在return指令之前安插一个copy constructor调用操作,以便将欲传回的object的内容当做上述新增参数的初值。
则上述代码转化如下:
void bar(X&_result){//加上一个额外参数
X xx;
//编译器所产生的default constructor调用操作
xx.X::X();
//…处理xx;
//编译器所产生的copy constructor调用擦做
_result.X::X( xx );
Return;
}
现在编译器必须转化每一个bar()调用操纵,以反映其新定义。例如:
X xx = bar();
将被转化为下列两个指令语句:
X xx; //注意不实行default constructo
bar( xx );
在使用者层面做优化:
程序优化的观念:定义一个“计算用”的constructor,换句话说程序员不再写
X bar( const T&y,const T &z){
X xx;
//…以y和z来处理xx
return xx;
}
那会要求xx被“memberwise”的拷贝到编译器所产生的_resul中。Jonathan定义另一个constructor,可以直接计算xx的值:
X bar( const T &y, const T &z){
returnX ( y, z );
}
于是当bar被转化后效率比较高:
void bar( X &_result, const T &y,const T &z) ){
_result.X::X(y, z );
return;
}
_result被直接计算出来,而不是经由拷贝构造函数拷贝而得!但有时定义这样的构造函数没有实际意义。
在编译器层面优化:
X bar(){
X xx;
//处理xx
return xx;
}
Named Return Value (NRV) optimization,具名返回值优化,实现这种优化有个前提,就是必须提供copy constructor,因为NRV优化的目的就是为了剔除copy constructor的使用。只有有了才能被剔除,否则谈不上剔除。一般的如果不优化NRV,上诉实现就是类似于返回值的初始化中的过程,而实现了优化的过程则如下所示,避免了copy constructor的使用。
void bar( X &_result ){
//defaultconstructor被调用
_result.X::X();
//直接处理_result
return;
}
CopyConstructor:要还是不要?
如果对象面临大量的拷贝操作[ 比如这个class的object需要经常以传值的方式返回],有必要实现一个拷贝构造函数以支持NRV优化。但是如果想使用底层的memcpy之类的直接进行bit wise copy,注意是否真的是bit wise copy拷贝,比如如果是virtual,这样可能破坏调vptr,如下:
class Shape{
public:
Shape(){memset(this,0,sizeof( Shape )); }
Virtual~Shape();
}
编译器会为此constructor扩张的内容看起来像这样:
Shape::Shape(){
_vptr_Shape= vtbl_Shape;
Memset(this, 0, sizeof( Shape )); //memset会将vptr清为0,出错
};
2.4成员们的初始化队伍(MemberInitialization List)
当你写下一个constructor时,就有机会设定class members的初值,要不是经由memberinitialization list就是在constructor函数本体之内。但有4中情况必须用memberinitialization list来初始化:
1.当初始化一个referencemember时
2.当初始化一个constmember时
3.当调用一个base class的constructor,而它拥有一组参数时
4.当调用一个memberclass的constructor,而它拥有一组参数时
有些情况可以在constructor函数本体之内设定classmembers的初值,但效率不高,如:
class Word{
String _name;
int _cnt;
public:
Word(){
_name = 0;
_cnt = 0;
}
}
将会被转化成:
Word::Word(){
_name.String::String();
Stringtemp = String(0);
_name.String::operator=(temp);
temp.String::~String();
_cnt = 0;
}
而如下两种方式:
Word::Word : _name(0){
_cnt= 0;
}
或Word::Word: _cnt (0),_name(0){
}
都将被转化为
Word::Word(){
_name.String::String(0); //编译器会一一处理initialization list,顺序按class中的
_cnt = 0; //members的声明顺序决定,而不是由initializationlist中排列顺序决定。
}
另外需说明的一点是编译器对initializationlist处理所安插在constructor内中的代码都置于用户用户定义的代码之前。