构造函数语意学

2.1 Default Constructor的构造操作

C++标准:对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式声明出来,一个被隐式声明出来的default constructor将是一个trivial constructor。然后在4种情况下这个implicit default constructor会被视为nontrivial。

带有Default Constructor的Member Class Object 

如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是nontrivial,编译器需要为该class合成出一个default constructor。不过这个合成操作只有在constructor真正需要被调用时才会发生。

class Foo {
public:
	Foo() {}
};
class Bar {
public:
	Foo foo;
	char *str;
};
void foo_bar() {
	Bar bar;
	cout << *(bar.str) << endl;
}
int main()
{
	foo_bar();
	system("pause");
	return 0;
}

在class Bar中,被合成的default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但它并不产生任何代码来初始化Bar::str。将Bar::foo初始化是编译器的责任,将Bar::str初始化则是程序员的责任。被合成的default constructor可能是下面这样:

inline Bar::Bar()
{
	foo.Foo::Foo();
}

现在前面那个程序是错误的,因为被合成的default constructor只满足编译器的需要,而不是程序的需要。为了让程序能够正确执行,字符指针str也需要被初始化。比如:

Bar::Bar()
{
	str = "a";
}

现在程序的需求获得了满足,但是编译器还需要初始化member object foo。由于default constructor已经被显示地定义出来,所以编译器没有办法合成default constructor。这时,编译器的做法是:

如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code被执行之前,先调用必要的default constructors。所以,接上面的例子,扩张后的constructors可能如下:

Bar::Bar()
{
	foo.Foo::Foo();//编译器附加的
	str = "a";
}

如果有多个class member objects都要求constructor初始化操作,则以member objects在class中的声明顺序来调用各个constructors。这是由编译器完成的,它为每一个constructor安插程序代码,以member声明顺序调用每一个member所关联的default constructors。这些代码被安插在explicit(显式的) user code之前。

带有Default Constructor的Base Class

如果一个没有任何constructors的class派生自一个带有default constructor的base class,那么这个derived constructor会被视为nontrival,并因此需要被合成出来。它将调用上一层base classes的default constructor(根据它们的声明顺序)。

如果设计者提供多个constructors,但其中都没有default constructor(c++的default constructor的概念,它是指没有参数的构造函数),那么编译器会扩张现有的每一个constructors,将调用要用到的default constructors的程序代码加进去。它不会合成一个新的default constructor,因为其它由user所提供的constructors存在的缘故。如果同时又存在着带有default constructors的member class objects,那些default constructor也会被调用(在所有base class constructor都被调用之后)

带有一个Virtual Function的Class

另外两种情况,也需要合成出default constructor:

1.声明(或继承)一个virtual function。

2.派生自一个继承串链,其中有一个或多个virtual base classes。

比如:

class Widget {
public:
	virtual void flip() = 0;
};
void flip(Widget& widget) { widget.flip(); }
void foo() {
	Bell b;//Bell派生自Widget
	Whistle w;//Whistle派生自Widget
	flip(b);
	flip(w);
}

下面两个扩张行动会在编译期间发生:

1.一个virtual function table会被编译器产生出来,内放class的virtual functions地址。

2.在每一个class object中,一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的虚函数表的地址。

 

widget.flip()的虚拟调用操作会被重新改写,使得能够使用widget的vptr()和vtbl(虚函数表)中的flip()条目

也就是发生转变:

widget.flip()转变为(*widget.vptr[1])(&widget)

 

为了让上述机制发挥功效,编译器必须为每一个Widget(或其派生类的)object的vptr设定初值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器会安插一些代码来做这样的事情。对于那些未声明任何constructors的classes,编译器会为它们合成一个default constructor,以便正确地初始化每一个class object的vptr。

带有一个Virtual Base Class的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( A* pa)
{
	//无法在编译时期决定pa->X::i的位置
	pa->i = 1024;
}
int main() {
	foo(new A);
	foo(new C);
	system("pause");
	return 0;
}

编译器无法固定住foo()之中“经由pa而存取的X::i”的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变“执行存取操作”的那些代码,使X::i可以延迟至执行期才决定下来。cfront的做法是靠在derived class object的每一个virtual babse classes中安插一个指针完成。

void foo(A* pa){pa->_vbcX->i=1024;}

其中_vbcX表示编译器所产生的指针,指向virtual base class X。

_vbcX是在class object构造期间被完成的。对于class所定义的每一个constructor,编译器会安插那些允许每一个virtual base class的执行期存取操作的代码。如果class没有声明任何constructors,编译器必须为它合成一个default constructor。

总结

上述四种情况,会造成编译器必须为未声明的constructor的classes合成一个default constructor。C++ Standard把那些合成物称为implicit nontrival default constructors。被合成出来的constructor只能满足编译器(而非程序)的需要。它之所以能够完成任务,是借着调用member object或base class的default constructor或是为每一个object初始化其virtual function机制或virtual base class机制而完成的。至于没有存在那4种情况而又没有声明任何constructor的classes,我们说它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。

在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data member(如整数、整数指针等等)都不会被初始化。这些初始化操作对程序而言或许有需要,但对编译器则非必要。如果程序需要一个把某指针设为0的default constructor,那么提供它的人应该是程序员。

对于C++的两个常见误解:

1.任何class如果没有定义default constructor,就会被合成一个出来。

2.编译器合成出来的default constructor会显示设定class内每一个data member的默认值。

2.2 Copy Constructor的构造操作

有三种情况,会以一个object的内容作为另一个class object的初值。

1对一个object做显式的初始化操作:

class X {...};
X x;
X xx = x;

2当object被当作参数交给某个函数时:

void foo(X x);
void bar()
{
	X xx;
	foo(xx);
}

3当函数传回一个class object时:

X foo_bar()
{
	X xx;
	return xx;
}

Default Memberwise Initialization(Memberwise是对每一个成员施以...)

如果class没有提供一个explicit copy constructor,当class object以相同class的另一个object作为初值,其内部是以所谓的default memberwise initialization手法完成的,就是把每一个内建的或派生的data member的值,从某个obbject拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式施行

memberwise initialization,也就说,对member class object也做上面相同的工作,而不是简单地拷贝membebr class object。

和以前一样,C++ Standard把copy constructor区分为trivial和nontrivial两种。只有nontrivial的实例才会被合成与程序 之中。决定一个copy constructor是否位trivial的标准在于class是否展现出所谓的bitwise copy sumantics。如果展现出了bitwise copy sematics,则不需要合成出一个default copy constructor,否则,需要合成出一个copy constructor。

 

注意:

default memberwise initialization是从对象的整体角度出发,对构成对象的每一个成员分别进行初始化。

而bitwise copy semantics是从对象的数据成员角度出发,具体到对象的每一个数据成员的操作。

Bitwise Copy Semantics(位逐次拷贝)

class String{
public:
	String(const char *ch = NULL);//默认构造函数
	String(const String &str);//拷贝构造函数
	~String(void);
	String &operator=(const String &str);//赋值函数
private:
	char *_str;
}

位拷贝拷贝的是地址,而值拷贝拷贝的是内容。

若定义string类的两个对象为str1,str2。str1._str和str2._str分别指向一块空间。

str1._str = “zhang”,str2._str = “tian”。

 

若默认拷贝构造函数,即str1(str2)。编译器将str2进行一份位拷贝。str1和str2指向同一块空间。

若默认赋值函数,即str1 = str2。编译器将str2的值赋值给str1,进行的也是位拷贝。

 

无论是默认拷贝构造函数还是赋值函数,都将str1的内容改变了。但是可能会出现以及问题:

(1)str1._str以前的内存未释放。

(2)改变str1._str的内容时,str2._str的内容也发生了改变,因为它们指向同一块内存。

(3)同一块内存会被释放两次。

 

如果不重写拷贝函数和赋值函数,编译器会以“位拷贝”的方式自动生成缺省函数。

 

若采用自定义的拷贝构造函数,即str1(str2)。编译器只是将str2._str的内容拷贝了一份。即为值拷贝。而str1._str和str2._str是分别指向一块空间。

若采用自定义的赋值函数,即str1 = str2。编译器将str2._str的内容赋值给str1._str,虽然str1._str的内容发生了改变,但是str1._str和str2._str指向不同的内存。

不要Bitwise Copy Semantics

什么情况下一个class不展示出bitwise copy semantics(位逐次拷贝),有下面4中情况:

1.当class内含一个member object而后者的class声明有一个copy constructor时(不论是被程序员显式说明,还是被编译器合成的)。

2.当class继承自一个base class而后者存在一个copy constructor时(不论显式声明或是被合成而得)。

3.当一个class声明了一个或多个virtual functions时。

4.当一个class派生自一个继承串链,其中有一个或多个virtual base classes时。

重新设定Virtual Table的指针

如果编译器对于每一个新产生的class object的vptr不能成功而正确地设好其初值,将导致严重的后果。因此,当编译器导入一个vptr到class之中时,该class就不能再展现bitwise semantics了。编译器需要合成出一个copy constructor以求将vptr适当地初始化。

class ZooAnimal {
public:
	ZooAnimal();
	virtual ~ZooAnimal();
	virtual void animate();
	virtual void draw();
};
class Bear :public ZooAnimal {
public:
	Bear();
	void animate();
	void draw();
	virtual void dance();
};

ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以靠bitwise copy semantics完成。比如:

Bear yogi;

Bear winnie=yogi;
yogi会被default Bear constructor初始化。而在constructor中,yogi的vptr被设定指向Bear class的virtual table(靠编译器安插的代码完成)。因此,把yogi的vptr值拷贝给winnie的vptr是安全的。

 

下面这张图是说明yogi和winnie的关系:

当一个base class object以其derived class的object内容做初始化操作时,其vptr复制操作也必须保证安全。比如:

ZooAnimal franny=yogi;

此时,franny的vptr不可以被设定指向Bear class的virtual table(但如果yogi的vptr被直接“bitwise copy”的话,就会导致此结果)。

void draw(const ZooAnimal& zoey) { zoey.draw(); }
void foo() {
	//franny的vptr应指向ZooAnimal的virtual table,而非Bear的virtual table
	ZooAnimal franny = yogi;
	draw(yogi);//调用Bear::draw()
	draw(franny);//调用ZooAnimal::draw()
}

也就说,合成出来的ZooAnimal copy construvtor会显式设定object的vptr指向ZooAnimal class的virtual table,而不是直接从右手边的class object中将其vptr现值拷贝过来。

处理Virtual Base Class Subobject

Virtual base class的存在需要特别处理。一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使bitwise copy semantics失效。

如果以一个Raccoon object作为另一个Raccoon object的初值,那么"bitwise copy"就足够了。

Raccoon rocky;

Raccoon little_critter=rocky;

然而如果企图以一个RedPanda object作为little_critter的初值,编译器必须判断后续当程序员企图存取其ZooAnimal subobject时是否能够正确执行

//简单的bitwise copy还不够
//编译期必须显式地将little_critter的
//virtual base class pointer/offset初始化
RedPanda little_red;
Raccoon little_critter=little_red;

在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个copy constructor,安插一些代码以设定virtual base class pointer/offset的初值。

2.3 程序转化语意学

显式的初始化操作

已知有下面的定义:

X x0;

下面的三个定义,每一个都明显地以x0来初始化其class object:

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;
	//编译器被安插X copy construction的调用操作
	x1.X::X(x0);
	x2.X::X(x0);
	x3.X::X(x0);
}

其中的x1.X::X(x0);就表现出以下的copy constructor的调用:

X::X(const X& xx);

参数的初始化

比如:若已知下面这个函数:

void foo(X x0);

下面的调用形式:

X xx;

foo(xx);

C++ Standard将会要求局部实例x0以memberwise的方式将xx当作初值。在编译器实现技术上,有一种策略是导入所谓的临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数。在这种策略下,前面的代码可能被编译器处理成如下形式:

//编译器产生出来的临时对象,在这里只负责占用内存
X __temp0;
//编译器对copy constructor的调用
__temp0.X::X(xx);
//重新改写函数调用操作,以便使用上述的临时对象
foo(__temp0);

然而这样的转换只做了一半功夫而已。因为__temp0是一个临时对象,所以foo()的声明必须发生转化,形式参数需要从原先的一个class X object转变为一个class X reference:

void foo(X& x0);

其中class X声明了一个destructor,它会在foo()函数完成之后被调用,处理那个临时性的object。

返回值的初始化

已知下面这个函数定义:

X bar()
{
	X xx;
	//处理xx
	return xx;
}

编译器所做的处理是:

1.首先加一个额外参数,类型是class object的一个reference。这个参数将用来放置被拷贝建构而得的返回值。

2.在return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当作上述新增参数的初值。

根据上述算法,bar()转换如下:

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;

bar(xx);

在使用者层面做优化

比如:

X bar(const T &y, const T &z)
{
	X xx;
	//...以y和z来处理xx
	return xx;
}

此时,xx被memberwise地拷贝到编译器所产生的__result中。在使用者层面的优化如下:

X bar(const T&y, const T &z)
{
	return X(y, z);
}

编译器经过处理转换如下:

void babr(X &__result,const T &y,const T &z)
{
	__result.X::X(y, z);
	return;
}

此时,__result被直接计算出来,而不是经由copy constructor拷贝而得。

在编译器层面做优化

X bar()
{
	X xx;
	//...处理xx
	return xx;
}

在一个像bar()这样的函数中,所有的return指令都传回相同的named value,其实是在说xx,因此编译器有可能自己做优化,方法是以result参数取代named return value。

编译器把其中的xx以__result取代:

void bar(X& __result)
{
	//default constructor被调用
	__result.X::X();
	//...直接处理__result
	return;
}

这样的编译器优化操作,被称为Named Return Value(NRV)优化。
NVR优化的最大好处就是不会再去调用那次多余拷贝构造函数了(把__temp0拷贝到c),因此《深入探索C++对象模型》67页最下面才会说第一版没有拷贝构造函数,所以不能进行优化。其实是指优化的意义不大,或者说没有什么可优化的。

2.4 成员们的初始化队伍(初始化列表)

当你写下一个constructor时,就有机会设定class members的初值。要不是经由member initialization list(成员初始化列表),就是在constructor函数本体之内。除了4种情况,你的任何选择其实都差不多。

在下列情况下,为了让程序能够被顺利编译,必须使用member initialization list:

1.当初始化一个reference member时;

2.当初始化一个const member时;

3.当调用一个base class的constructor,而它拥有一组参数;

4.当调用一个member class的constructor,而它拥有一组参数时。

在这4种情况下,程序可以被编译并执行,但是效率不高。例如:

class Word {
	string _name;
	int _cnt;
public:
	Word() {
		_name = 0;
		_cnt = 0;
	}
};

下面是constructor可能的内部扩张结果:

Word::Word()
{
	//调用string的default constructor
	_name.string::string();
	//产生临时性对象
	string temp = string(0);
	//memberwise地拷贝_name
	_name.String::operator=(temp);
	//摧毁临时性对象
	temp.string::~string();
	_cnt = 0;
}

接下来是初始化列表的使用:

Word::Word: _name(0)
{
	_cnt = 0;
}

它会被扩张成下面的样子:

Word::Word()
{
	_name.string::string(0);
	_cnt = 0;
}

 

那么在member initialization list中到到底发生了什么事情?编译器会一一调用initialization list,以适当顺序在constructor之内安插初始化操作,并且在任何explicit user code之前。 注意:list中的项目顺序是由class中members声明顺序决定的,不是由initialization list中的排列顺序决定的。

class X {
	int i;
	int j;
public:
	X(int val) :j(val), i(j) {}
};

上面这个例子看起来是要把j设初值val,再把i设初值j。问题是,由于声明顺序的缘故,initialization list中的i(j)其实比j(val)更早执行。但因为j一开始未有初值,所以i(j)的执行结果导致i无法预知其值。这个“臭虫”是很难被发现的。

再来看另外一个问题:

X::X(int val) :j(val)
{
	i = j;
}

initialization list中的项目被安插到constructor中,会继续保存声明顺序吗?如果声明顺序继续被保存,则上述代码发生错误,因为i将被先初始化。然而这份代码其实是正确的,因为initialization list的项目被放在explicit user code之前。

另外:调用一个member function以设定一个member的初值也是可以的,但不建议。如下:

X::X(int val) :i(xfoo(val)), j(val) {}

constructor被扩充后的结果如下:

X::X()
{
	i = this->xfoo(val);
	j = val;
}


 


 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值