构造函数与复制构造函数

本来,第二章的标题是“The Semantics of Constructors (构造函数语意学)”,晦涩难懂,但实际很简单,讲的就是constructor和copy constructor在编译阶段的构造规则。


1. 构造函数(Constructor)

首先,有两个问题,绝大多数人会认为它们是正确的:

  • 对于任何class,如果没有定义default constructor,那么编译器便会自动合成(构造)一个出来?
  • 编译器自动合成(构造)的default constructor会显式设定好类中每个data member的初始值?

当然,上面两个问题都不是正确的。我们先看第一个问题,如果没有定义default constructor,那么编译器什么时候会合成出一个constructor?

答案是:在编译器需要的时候。

程序的需要 vs 编译器的需要

我们现在的问题是,编译器需要default constructors的时候是什么时候?看下面的代码:

class Foo
{
public:
	int val;
	Foo *pnext;
};

void foo_bar()
{
	// PROGRAM need bar's members zeroed out
	Foo bar;
	if(bar.var || bar.pnext)
		// ... do something
	// ...
}
在上面的程序中,我们会想当然的认为,既然程序没有default constructor,而后面又需要constructor,那么系统当然会自动生成一个构造函数来初始化bar对象及其data members。但是,事情不是这样的!

实际运行的结果是,if语句几乎永远不会执行,因为虽然Global object的内存会保证程序运行的时候清零,但Foo bar对象是local object,它只会在堆栈中,未必会清零。

那么编译器会自动生成constructor吗?不会!原因就是Foo的构造函数是程序需要的(程序需要bar的data members来判断),而不是编译器需要的。

编译器生成默认构造函数的情况有4种,如下,

1)一个class的成员中包含了另外一个class的object

class Foo
{
public:
	Foo();
	// ...
};
class  Bar
{
public:
	Foo foo;
	char *str;
};

像这样,编译器必须为Bar初始化一个default constructor,来为其member foo提供class Foo的构造调用,可能会有如下的形式:

/* compiler synthesized */
inline Bar::Bar() { foo.Foo::Foo(); }

为什么会用内联的形式来定义呢?我们接着看,假如程序员也定义了一个Bar的构造函数,如下:

/* programmer synthesized */
Bar::Bar(){ str = 0; }
那么,inline的作用就来了(为了扩展生成的效率,像copy constructor, destructor, assignment copy operator都会以这种方式完成,当然也有函数太复杂,不适合做inline的情况);

“如果编译器含有一个以上的member class objects(类成员对象),那么它就必须调用每个default constructor”;所以编译器最终扩张生成的代码,可能会按照声明顺序(可能有多个其他类的对象成员),在用户的explicit user code之前加上自动生成的代码,以进行必要的初始化:

Bar::Bar()
{
	foo.Foo::Foo();	// added compiler code
	str = 0;	// explicit user code
}

从这里也可以看出,对于Bar::foo的初始化是编译器的责任(即编译器需要的),而对于Bar::str的初始化是程序员的责任,两者不会互相干预,编译器只管自己的那部分,不会对Bar::str做初始化。

2)一个class的父类有default constructor

编译器调用default constructor的顺序是:父类的constructor --> 对象成员的constructor(如果存在) --> 用户的constructor初始化代码;像上面 1)一样,如果程序员自己定义了constructor,那么编译器会在user code之前扩展其代码(调用其它constructor),而不是生成。

3)一个class中含有virtual function

看如下代码:

class Widge
{
public:
	virtual void flip() = 0;
	// ...
};

void flip(const Widge& widge) {widge.flip();}

// 假设Bell和Whistle都派生自Widge
void foo()
{
	Bell b;
	Whistle w;

	flip(b);
	flip(w);
}
编译期间,如下两个操作会发生:

  • 编译器产生一个virtual function table,来存放virtual functions的地址;
  • 编译器为每一个object中产生一个pointer member(即vptr),指向virtual function table;
从上面两点看来,如果没有constructor的话,编译器显然要生成一个default constructor,来初始化object中的vptr。

4)一个class有virtual base class(虚基类),即一个class以virtual方式继承父类

关于virtual base 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; };

//无法再编译期间决定 pa-> X::i 的位置
void foo(const A* pa) { pa->i = 1024 }

main()
{
	foo(new A);
	foo(new B);
	// ...
}
上面的代码中,继承关系很容易就看出来。由于pa的真正类型可以改变,所以编译期间无法确定其偏移位置。所以编译器可能会在子类对象中安排一个指向virtual base class的指针,当然这个指针需要constructor来初始化。那么此时,default constructor的生成也是水到渠成了。

总的来说,上面四条是在没有constructor的情况下,“编译器需要的时候”自动产生constructor的四种情况;这里面的重点是编译器与程序员“责任”的分工问题,编译器并不为“想当然的”那些class生成constructor来初始化objects,初始化对象成员是程序员的事,编译器只会做自己“该做的事(如上4种)”。


2. 复制构造函数(Copy Constructor)
对于初学者来说,虽然复制构造函数看似用处不多,但也是很重要的,我们常见到如下形式的复制构造函数:
/* user-defined copy constructor */
X::X( const X& x);
Y::Y( const Y& y, int = 0);
注意,声明时的const及&不要忽略。

(1)copy constructor有什么作用?
一个class object以同class的另一个object作为初值时,便会调用copy constructor。一般一个class object有两种复制方式,
  • 一种是copy constructor(也是我们现在关注的);
  • 另一种是copy assignment operator(我们会在以后讨论)。
类似于default constructor,如果没有explicit copy constructor,编译器在需要的时候,会生成copy constructor。

(2)如何进行复制?—— Bitwise C opy Semantics(位拷贝)
当没有显式地声明copy constructor的时候,object拷贝以default memberwise initialization手法完成,看如下代码:
class Word
{
public:
	// ... no explicit copy constructor
private:
	int cnt;
	String str;
};
default memberwise initialization是指:对data member(比如 cnt),直接copy;对其中的类对象成员(比如 str),以递归的方式进行memberwise initialization。
复制的过程即是bitwise copy,它的意思是把一个object中的所有member依次复制到另一个object中。

(3)不进行bitwise copy的4种情况
  • 一个class中含有member object,而member object的类声明中有copy constructor;
如上例,进行递归的copy,而不是bitwise copy。
  • 一个class继承自一个base class,并且基类中有copy constructor;
不论base class中的copy constructor是显式声明还是自动合成的,其子类都不进行bitwise copy;
  • 一个class中有virtual functions;
比如说下图,显然不能将object中的vptr直接复制过去,这需要编译器对vptr进行重新指定,该图中Bear与ZooAnimal之间是public继承关系,代码执行 ZooAnimal franny = yogi; 这里我们只关注vptr的重定向:

  • 一个class含有一个多多个virtual base class;
每一个编译器对于虚拟继承的承诺是,必须让子类对象中的virtual base class subobject位置在执行期准备妥当。当virtual base class的指针指向子类对象时,编译器必须合成一个copy constructor,在其中安插代码来设定subobject的信息(pointer/offset,这会在下一篇讲到)。

(4)用到copy constructor的三种情况
以下是以一个object的内容作为另外一个object初始值的几种情况。
a. 显式的对象初始化操作;
比如有以下程序:
X x0;

void foo_bar()
{
	X x1(x0);	// definition of x1
	X x2 = x0;	//definition of x2
	X x3 = X(x0);	//definition of x3
	// ...
}
会对源码进行一个两阶段的转化:
  • 重写定义,去除初始化操作,定义即内存分配的过程;
  • 安插copy constructor代码,进行初始化。
转化后的代码可能类似于这样:
void foo_bar()
{
	// 1. 重写定义
	X x1;
	X x2;
	X x3;

	// 2. 安插copy constructor调用
	x1.X::X(x0);
	x2.X::X(x0);
	x3.X::X(x0);
}
上述的 x1.X::X(x0),即表现为复制构造函数:
X::X( const X& xx );

b. 对象参数传递初始化
void foo(X x0);

X xx;
// ...
foo(xx);
针对以上代码,编译器可能会对其做如下的修改,即建立一个临时的对象把它初始化,并在foo函数中使用,当foo结束之前,调用destructor销毁它:
X __temp0;
__temp0.X::X(xx);
foo( __temp0 );
其实上述转化只做了一半功夫而已,编译器也许会重新改变foo的声明,类似于这样:
void foo( X& x0 );
也许会将构造出的临时object直接放在程序堆栈中使用,这个是不同编译器策略决定的。

c. 函数的对象返回值
X bar()
{
	X xx;
	// handle xx...
	return xx;
}
这也许是最令人感到迷惑的地方,如果X是一个局部变量的话,那么就不可能被返回(因为返回前便已经销毁了)。我们来看看为什么可以这样做。因为会重写代码,它也经历了一个两阶段的转化:
  • 加上一个额外的参数,类型是object 的reference,用来取得返回的对象;
  • return之前安插一个copy constructor的调用操作。
void bar( X& __result )	//1. 加上一个额外参数
{
	X xx;
	xx.X::X();

	// handle xx ...

	// 2. 安插一个copy constructor用于返回
	__result.X::X( xx );

	return;
}

(5)Member Initialization List
这是一个编码风格影响效率的问题。首先,什么是initialization list?
class Word{
public:
	int name;
	int age;
	int cnt;

public:
	Word::Word()
	:cnt(0), name(0)
	{
		age = 5;
	}
};
这里面,cnt和name就是initialization list了。这里面要注意的两个问题是:
  • initialization list的初始化是放在explicit code之前的(即age的顺序是放在最后的);
  • initialization list中的顺序是按照class中member的声明顺序来的,与排列顺序无关(即先name后cnt);
其它的,便不详细阐述了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值