构造函数语意学
对于class X,如果没有任何user-declared constructor, 那么会有一个default constructor被隐式(implicit)声明出来,但是这个default construct是无用的。关键词explicit被导入,就是给我们提供一种方法,他们能够制止“单一参数的construct”被当成一个conversion运算符。
一、Default Construct的构造操作
class Foo { public: int val; Foo *pnext; };
default construct在编译器需要的时候自动产生一个,而在程序需要的时候编译器不会产生,因为这是程序员的责任。
void foo_bar() { Foo bar; if(bar.val||bar.pnext) //do something //... }
上述代码并不会自动生成default construct,所以要想使程序正常工作,必须显示的声明一个default construct。
Global object的内存保证会在程序启动时候被请清为0,。local object配置于程序的堆栈中,heap object配置于自由空间中,都不一定会被清为0,它的内容是上次使用后的。
1.“带有Default Construct”的Member Class Object
如果一个class没有任何construct,但是它含有一个member object,而后者有default construct,那么这个class的implicit default construct是有用的,编译器会合成一个default construct,只不过这个construct真正被调用的时候才会发生。但是c++有不同的编译模块,编译器为了避免合成多个default construct,把合成的default construct,copy construct,destructor,assignment copy operator都以inline的方式完成。
class Foo {public:Foo(), Foo(int) }; class Bar {public: Foo foo;char *str;}//内含 void fun() { Bar bar;//此时会合成默认构造函数,会调用Bar的默认构造函数 } //编译器在Bar中插入代码,合成default construct inline Bar::Bar() { foo.Foo::Foo(); }; //但不会初始化str,需要程序员来进行初始化;
编译器合成的default construct只是满足编译器的需要,而不会满足程序的需要,所以bar.str并不会被初始化,要想初始化此成员,要手工定义default construct。
但是如果程序员自己定义构造函数,如下,构造函数被现实定义,那么编译器就不会再合成第二个default construct。
Bar::Bar() { str=0; }
那么为了编译器的需要,编译器会扩充已存在的construct(如下)。也就是:如果class A内含有一个或一个以上的member class object,那么class A的每个construct都必须调用每个member classes的default construct。在user conde被调用之前,先调用default construct。
Bar::Bar() { foo.Foo::Foo();//附加compiler code str=0;//explicit user code }
如果有多个class member objects都要求contructor初始化操作。C++语言要求以“member objects在class中的声明顺序“来调用各个constructors。这一点由编译器完成,它为每个constructor安插程序代码,以”member声明顺序“调用每一个member所关联的default constructors。这些代码将被安插在explicit user code之前。
class Dopey {public: Dopey();...}; class Sneezy {public: Sneezy(int); Sneezy();...}; class Bashful {public: Bashful();...}; //以及一个class Snow_White: class Snow_White { public: Dopey dopey; Sneezy sneezy; Bashful bashful; private: int mumble; }; //如果Snow_White没有定义default constructor,就会有一个nontrivial constructor被合成出来, //依序调用Dopey、Sneezy、Bashful的default constructors。然而如果Snow_White定义了下面这样 //的default constructor: //程序员所写的default constructor Snow_White::Snow_White(): sneezy(1024) { mumble = 2048; } //编译器扩张后的default constructor Snow_White::Snow_White():sneezy(1024) { //插入member class object //调用其constructor dopey.Dopey::Dopey(); sneezy.Sneezy::Sneezy(1024); bashful.Bashful::Bashful(); //expilict user code mumble = 2048; }
总结:
- 如果类无member object,那么会在编译器需要的时候产生一个无用的default construct。
- 如果类有member object,1.如果类无default construct,那么编译器会自动合成一个,初始化member object,但是类的member data的初始化任务合成的构造函数不会做;2.如果类有construct,那么编译器会在现有的construct加入一些代码,调用member object的default construct。
2."带有Default Constructor“的Base Class
类似的道理,如果没有任何constructors的class派生自一个”带有default constructor“的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base classes的default constructor(如果是多继承,根据他们声明的顺序)。对一个后继的class而言,这个合成的constctor和一个”被显式提供的default constructor“没有什么差异。
如果设计者提供多个constructors,但其中都没有default constructor呢?编译器会扩张现有的每一个constructors,将“用以调用所有必要之default constructors”的程序代码加进去。它不会合成一个新的default constructor,因为其他“由user所提供的constructors”存在的缘故。如果同时亦存在着“带有default constructor”的member class objects,那些default constructor也会被调用--在所有base class constructor之后。
3.“带有一个virtual Function”Class
另有两种情况,也需要合成出default constructor:
- class声明(或继承)一个virtual function
- class继承自一个继承串链,其中有一个或多个的virtual base classes
假设基类中还有虚函数,派生类继承基类时编译期间会发生两步扩张活动:
- 一个virtual function table(在cfont中被称为vtbl)会被编译出来,内放class的virtual functions地址
- 在每个class object中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关之class vtbl的地址
编译器会为每一个派生类的vptr设定初始值,放置适当的virtual table地址,对于class定义的每个construct,编译器都会安插一些代码做这些事,对于未声明construct的class,编译器会合成construct来做这些事。
4.“带有一个virtual Base Class”的Class
virtual base class的实现在不同编译器间有很大的差异。
class X { public: int i; }; class A:public virtual X { public: int j; }; class B:public virtual X { public: double d; }; class C:public A,public B { public: int k; }; void foo(const A* pa) { pa->i=1024; } int main() { foo(new A); foo(new C); }
编译器无法固定住foo中“由pa而存取的X::i”的实际偏移位置,因为pa的真正类型可以改变,编译器必须改变“执行存取操作”的那些代码。使得X::i延迟到执行期才决定下来。cfront的做法是在derived class object的每一个virtual base classes中安插一个指针完成。所有经由reference或pointer来存取的一个virtual base class的操作通过相关的指针来完成。
//可能编译器转变的操作 void foo(const A* pa) { pa->_vbcX->i=1024;//_vbcX表示编译器所产生的指针,指向virtual base class X,在class object的构造期被完成 }
对于class 所定义的每个construct,编译器会安插一些“允许每个virtual base class的执行期存取的操作”的代码,如果class没有声明任何constructors,编译器必须为它合成一个default constror。
总结,有4种情况,会造成“编译器必须为未声明constructor 的classes合成default constructor”。它们分别是:
- “带有Default Constructor”的Member Class Object
- “带有Default Constructor”的Base Class
- “带有一个Virtual Function”的Class
- “带有一个Virtual Base Class”的Class
C++Standard把那些合成物称为implicit nontrivial default constructors。被合成出来的constructos只能满足编译器(而非程序)的要求。它之所以完成任务,是借着“调用member object或base object的default constructor”或是“为每一个object初始化其virtual function机制或virtual base class机制”而完成的。至于没有存在那4种情况而又没有声明任何constructor的classes,我们说它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。
在合成default constructor中,只有base class constructor和member class objects会被初始化。所有其他的nonstatic data member(如整数,整数指针、整数数组等等)都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则非必要。如果一程序需要一个“把某指针设为0”的default constructor,那么提供它的人应该是程序员。
C++新手一般有两个常见的误解:
- 任何class如果没有定义default constructor,就会被合成出一个来。
- 编译器合成出来的default constructor会显式设定“class内每一个data member的默认值”
如你所见没一个是真的。
二、Copy Constructor的构造操作
以下三种情况会调用copy construct
- 一个object的内容作为另一个class object的初始值
- object被当做参数传递给某个函数时
- 函数返回个class object时
class X{...}; X x; X xx=x;//1 void fun(X x);//2 void fun() { X xx; //... return xx;//3 }
1.Default Memberwise Initialization
如果class没有提供explicit copy constructor,当编译器需要的时候,把每一个内建的或派生的data member(指针或数组的值),从一个object拷贝到另一个object。但是她并不会拷贝其中的member class object。
Default constructor和Copy constror在必要(指当class不展现bitwise copy semantics时)的时候由编译器产生出来。
也就是当我们没有定义copy construct时,编译器会帮我们生成一个。c++ standard 把copy construct分为trivial和nontrivial,只有nontrivial才能被应用,是否为trivial的标准为class是否出现“bitwise copy semantics”。
2.Bitwise Copy Semantics(位逐次拷贝)
如果member data有指,在没有显示定义拷贝构造函数的时候,编译器会自动生成一个默认的拷贝构造函数,此时class object的初始化时会把指针直接赋值,当局部object被析构时,会执行析构函数,使得全局object指向一堆无用的数据。
//以下声明未展示出bitwise copy semantics class Word { public: Word(const String& ); ~Word(); private: int cnt; String str; }; //String声明了一个explicit copy construct class String { public: String(const char*); String(const String&); ~String(); }; //此时编译器会合成一个copy construct以便调用member class String inline Word::Word(const String& wd) { str.String::String(wd.str); cnt=cnt; }
被合成的copy construct指针数组等nonclass member也都会被复制。
3.不要Bitwise Copy Semantics
- 当class 中内含一个member object ,而该object内含copy constructor(无论是显示声明就像String那样;或被编译器合成,像class Word那样)
- class 继承一个base class,而后者存在copy construct(无论是显示声明还是编译器合成)
- class 声明了一个或者多个virtual functions
- class 派生自一个继承链只能够,其中一个或者多个virtual base classes时
前两种情况编译器必须将member或base class的“copy construct调用操作”安插到被合成的copy construct中。就像class Word那样。接下来讨论情况3.4.
4.重新设定VIrtual Table的指针
看以下代码
class ZooAnimal { public: ZooAnimal(); virtual ~ZooAnimal(); virtual draw(); private: //... }; class Bear:public ZooAnimal { public: Bear(); void draw();//不注明也是virtual }; /* 当ZooAnimal class object以另一个ZooAnimal class object作为初始化,或者Bear class object 以另一个Bear class object作为初始值,都可以直接依靠 bitwise copy semantics 完成,除了可能 有member pointer外 */ Bear yogi;//被default Bear construct初始化,yogi的vptr被设定为指向Bear class 的virtual table Bear winnie=yogi;//把yogi的vptr设定为指向Winnie的vptr是安全的,因此yogi和Winnie的vptr指向同一个virtual table
当一个base class object以其derived class 的object内容做初始化操作时,其vptr复制操作也必须保证安全。
ZooAnimal franny=yogi;//会发生切割
但是franny的vptr不指向yogi的virtual table(但是如果yogi的vptr被直接 bitwise copy 的话,就会导致此结果)。也就是说合成出来的ZooAnimal copy construct会显示设定object的vptr指向ZooAnimal class的virtual table,而不是直接从右手边的class object复制其中的vptr。
5.处理Virtual Base Class Subobject
如果一个class object以另一个class object作为初始值,而后者有一个virtual base subobject,那么也会使“bitwise copy semantics”失效。
每一个编译器对虚继承的支持,代表必须让“derived class object 中的virtual base class subobject位置”在执行期就准备好。维护位置的完整性是编译器的责任,而“bitwise copy semantics”可能会破坏这个位置。所以编译器必须在他自己合成出来的copy construct中做出仲裁。
class Raccoon:public virtual ZooAnimal { public: Raccoon();//编译器会产生新代码调用ZooAnimal default construct,被安插在Raccoon construct的开头 Raccoon(int val); private: //... };
virtual base class的存在会是的bitwise copy semantics无效,但问题不发生在“一个class object以另一个同类的object作为初始值”时,而是发生在“一个class object以其derived classes的某个object作为初始值”时,如下:
class RedPanda:public Raccoon { public: RedPanda(); RedPanda(int val); private: //... }; Raccoon ra; Raccoon raa=ra;//一个Raccoon object作为另一个Raccoon object的初始值,bitwise copy绰绰有余 RedPanda rp; Raccoon rpa=rp;//简单的bitwise还不够,必须显示的将rpa的virtual base class pointer/offset初始化
下面这种情况无法断定“bitwise copy semantics”是否还存在,因为无法分析ptr是否指向一个真正的Raccoon类型。
Raccoon *ptr;
Raccoon ra=*ptr;
如果一个初始化操作存在着并且保持着“bitwise copy semantics”的状态,如果编译器能够保证object正确的初始化,他是否该压制copy construct的调用,使其产生的代码优化?如果是合成的copy construct那么程序的副作用是0,优化是合理的,若是class的设计者提供的呢?
三、程序转化语意学(program transformation semantics)
X foo() { X xx; //... return xx; }
- 每次foo被调用,就传回xx的值——视X的定义而定
- 如果class X定义了copy constructor,那么每当foo被调用时,保证该copy constructor也被调用——视X的定义和编译器的进去层优化
1.显示的初始化操作
void foo() { X x1(x0);//定义了x1,定义指的是占用内存的行为 X x2=x0;//定义了x2 X x3=X(x0);//定义了x3 }
- 重写每一个定义,其中的初始操作被删除
- class的copy constructor调用操作被安插进去
转化后的代码为
void foo() { X x1;//定义了x1 X x2;//定义了x2 X x3;//定义了x3 //编译器安插X copy constructor调用 x1.X::X(x0); x2.X::X(x0); x3.X::X(x0); }
2.参数的初始化(Argument Initialization)
当一个class object当做参数传给一个函数时(或作为一个函数返回值时)相当于初始化操作:X xx=arg;xx代表形参(或返回值),arg代表真正的参数。
void foo(X x0); X xx; foo(xx); //会导致局部实例x0以memberwise方式将xx当做初始值 //调用操作会发生以下更改 X __temp0;//编译器产生出来的临时代码 __temp0.X::X(xx);//调用copy constructor foo(__temp0);//改写调用操作 void foo(X& x0);//函数声明也被改写
3.返回值的初始化(Return Value Initialization)
如果从函数中返回一个局部对象,则存在如下转化
- 加上一个额外参数,类型是class object的一个reference。这个参数用来放置“拷贝构建”而得的返回值
- 在return之前安插一个copy constructor调用,以便传回的object内容当做上述新增参数的返回值
//转化前 X fun() { X xx; //... return xx; } //转化后 void fun(X& _result) { X xx; xx.X::X(); //...处理xx _result.X::XX(xx); return ; } X xx=fun() //转化每一个调用操作 X xx; fun(xx);
4.使用者层面做优化
X fun(const T &y,const T &z) { X xx; //用y和z来处理xx return xx; } //可以定义构造函数直接计算xx的值 X fun(const T& y, const T& z) { return X(y,z); } //可能的转化为 void fun(X &__result) { __result.X::X(y,z); return ; }
5.在编译器层面做优化
所有的return指令传回相同的具名数值,以result参数取代named return value,称为NRV优化。
//转化前 X fun() { X xx; //... return xx; } //转化后 void fun(X& _result) { X xx; xx.X::X(); //...处理xx _result.X::XX(xx); return ; }
在使用memcpy或memset时,都只有在classes不含有任何编译器内部产生的members时才能有效运行(不含virtual functions或virtual base class)。
四、成员们的初始化队伍(member Initialization List)
必须使用的情况:
- 当初始化一个reference member时
- 当初始化一个const member时
- 当调用一个base class的constructor,而他拥有一组参数时
- 当调用一个member class的constructor,而他拥有一组参数时
class Word { private: String name; int cnt; public: Word() { name=0; cnt=0; } }; //这会导致产生临时的String object,代码会产生以下扩充 Word::Word() { name.String::String(); //产生临时对象 String temp=String(0); //“memberwise”地拷贝name name.String::operator=(temp); //销毁临时对象 temp.String::~String(); cnt=0; }
template code可能存在陷阱
template <class type> foo<type>::foo(type t) { //视type的情况而定 _t=t; }
编译器会一一操作initialization list,以适当的顺序在constructor之内安插初始化操作,并在任何explicit user code之前,list中的顺序是由class members声明的顺序决定的。
看如下操作
X::X(int val):j(val) { i=j; }
j的初始化操作会被安插在explicit user assignment操作(i=j)之前。也就是initialization list的项目被放在explicit user code之前。
用一个member function初始化一个member
X::X(int val):i(xfoo(val)),j(val){}
要使用“存在于constructor体内的一个member”,而不要使用“存在于member initialization list中的member”,来为另一个member设定初始值,因为不知道xfoo对X object的依赖性有多高,如果把xfoo放在constructor之内,那么对于“到底哪一个member在xfoo执行时被设立初始值”这件事,可以保证不会发生模棱两可的事。
如果derived class member function被调用,返回值是base class constructor的一个参数
class FooBar:public X { private: int _fval; public: int fval() { return _fval; } FooBar(int val):_fval(val),X(fval());//fval()作为基类构造函数参数,不是一个好主意 //可能会导致以下代码扩充 FooBar() { X::(this,this->fval()); _fval=val; } };