第2章 构造函数语义学

2.1 Default Constructor的建构操作

以下四种情况,编译器必须为class合成default Constructor!

2.1.2 带有Default Constructor的Member Class Object

如果一个class没有任何Constructor,但是它内含一个member object,且member object有default Constructor,那么这个class的implicit default Constructor就是non-trivial,编译器需要为此class合成一个 default Constructor,不过这个合成操作只有在Constructor真正需要被调用时才会发生

class Foo
{
public:
	Foo(){}
	Foo(int) {}
}

class Bar
{
public:
	Foo foo;
	char *str;
}

void foo_bar()
{
	Bar bar;
	//Bar::foo必须在此处初始化
}

如果有多个member object都要求有Constructor初始化操作,那么会按照member object在class内的声明次序来调用每个的Constructor,且这些操作被安插在explicit user code(也就是成员变量)之前

class A
{public: A();}
class B
{public: B();}
class C
{public: C();}

class D
{
public:
	//D没有定义default constructor
	A a;
	B b;
	C c;
private:
	//explicit user code
	int x;
}

如上述代码所示,D没有default Constructor,那么就会有一个nontrivial Constructor被合成出来,依次调用A B C的default Constructor,即:

D::D()
{
	a.A::A();
	b.B::B();
	c.C::C();
	
	x = 0;
}

2.1.3 带有Default Constructor的Base Class

如果基类base class带有default Constructor,派生类没有任何Constructor,那么这个派生来的default Constructor会被视为non-trivial,并因此需要被创建出来。

2.1.4 带有一个Virtual Function的Class

当一个class中有虚函数时,编译器编译时会出现的操作有:

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

2.每一个class object中,一个额外的pointer member(即vptr)会被编译器合成出来,内含相关的class vtbl的地址

3.当调用虚函数时,会出现虚拟引发操作的改写,以使用vptr、vbtl的条目,即(*x.vptr[1])(&x)

为了让以上操作生效,编译器必须为每一个class object的vptr设定初值,放置适当的vtbl的地址。因此对于没有default Constructor的类,必须合成一个Constructor,以便能够初始化每一个vptr。

2.1.5 带有一个Virtual Base Class的Class

编译器会在派生来对象的每一个虚基类中插入一个指针,所有经由指针或引用来存取虚基类的操作都可以通过该指针来完成。

被插入的这个指针会在Constructor期间完成,因此如果该class没有default Constructor,编译器会为它合成一个。

2.2 Copy Constructor的建构操作

复制构造函数可能用到的情况:

1.新建某对象时需要用另一个object来做初值拷贝

2.函数参数拷贝时

3.函数返回一个值时

2.2.1 Default Memberwise Initiailization

当class没有提供explicit copy Constructor时,class object以另一个object拷贝时内部运用的是default  memberwise initiatiltion,且当类成员中有其他member class object时,拷贝这个object时是以递归的方式施行memberwise  initialzation。

2.2.2 Bitwise Copy Semantics(位逐次拷贝)

Word noun("book");
void foo()
{
	Word verb = noun;
}

上述代码中,如果Word定义了一个copy Constructor,verb的初始化操作会调用它,但是如果未定义复制构造函数,那么编译器是否会合成一个default copy Constructor取决于class是否展现Bitwise copy semantics特性。

class Word
{
public:
	Word(const char*);
	~Word();
private:
	int cnt;
	char* str;
}

如果使用的是上面的class Word,那么就不会合成一个default copy Constructor,因为上述展现的是Bitwise copy semantics特性,无需调用一个复制构造函数。

class Word
{
    Word(const String&);
    ~Word();
private:
    int cnt;
    String str;
}
class String
{
public:
	String(const char*);
	String(const String&);
	~String();
}

如果使用的是上面的class String,那么必须合成一个copy Constructor以便调用member class String object的copy Constructor。调用过程伪代码如下所示:

Word::Word(const Word &wd)
{
	str.String::String(wd.str);
	cnt = wd.cnt;
}

2.2.3 不要Bitwise Copy Semantics!

class不展现Bitwise copy semantics特性的四种情况:

1.当class内含一个member object而后者的class声明有一个copy Constructor时(不论是被class设计者明确的声明,像前面的class String,或是被编译器合成,像class Word)

2.当class继承自一个base class而后者存在有一个copy Constructor时

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

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

2.2.4 重新设定Virtual Table的指针

class ZooAnimal
{
public:
	ZooAnimal();
	virtual ~ZooAnimal();
	
	virtual void animate();
	virtual void draw();
private:
	//ZooAnimal的animate(),draw()所需要的数据
}
class Bear:public ZooAnimal
{
public:
	Bear();
	virtual void animate(); 
    virtual void draw();
	virtual void dance();
private:
	//Bear的animate(),draw(),dance()所需要的数据
}
Bear yogi;
Bear winnie = yogi;

yogi会被Bear的构造函数初始化,而在构造过程中,yogi的vptr被设定指向Bear class的virtual table,因此把yogi的vptr值拷贝给winnie的vptr是安全的。两者的具体关系见下图:

当base class object以其Derived class的object内容做初始化时,其vptr复制操作也必须保证安全,例如

ZooAnimal franny = yogi;

franny的vptr不可以被设定指向Bear class的virtual table,否则下面代码中draw被调用时而franny被传进去,就会出现问题

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

2.2.5 处理virtual base class subobject

class Raccoon: public virtual ZooAnimal
{
public:
	Raccoon();
	Raccoon(int val);
private:
	//所有必要的数据
}
class RedPanda :public Raccoon
{
public:
	RedPanda();
	RedPanda(int val);
private:
	//所有必要的数据
}

RedPanda little_red;
Raccoon little_critter = little_red;

上述代码中,为了完成正确的little_critter初值设定,编译器必须合成一个copy Constructor来设定virtual base class pointer/offset的初值。

2.3 程序转化语义学

2.3.1 明确的初始化操作

void foo_bar()
{
	X x1(x0);
	X x2 = x0;
	X x3 = X(x0);
}

必要的程序转化有两个阶段:

1.重写每一个定义,其中的初始化操作会被剥除;

2.class的copy Constructor调用操作会被安插进去;

2.3.2 参数的初始化

这里指的是函数参数的初始化,即

void foo(X x0);
X xx;
foo(xx);

x0为形参,xx为实参,x0以memberwise的方式将xx当作初值,此时编译器会导入一个暂时性的object,并调用copy Constructor将它初始化,然后将此暂时性object交给函数,具体过程如下面为伪代码所示:

X tmp;
tmp.X::X(xx);
foo(tmp);

2.3.3 返回值的初始化

X bar()
{
	X xx;
	....
	return xx;
}

函数返回值是如何从局部对象xx中拷贝过来的?过程如下:

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

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

具体过程的伪代码如下:

void bar(X& _result)
{
	X xx;
	//编译器所产生的default constructor调用操作
	xx.X::X();
	.....
	//编译器所产生的copy constructor调用操作
	_result.X::XX(xx);
	return;
}

那么当用到bar()函数时,编译器都会进行转换工作,例如:

X xx = bar();
//转换
X xx;
bar(xx);

2.3.4 在使用者层面做优化

这种优化就是定义一个“计算用”的Constructor。

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

当设定一个class X的Constructor时,就无需像之前提到的那样调用复制构造函数来得出_result,具体过程伪代码:

void bar(X &_result)
{
	_result.X::X(y,z);
	return;
}

2.3.5 在编译器层面做优化

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

如上述代码所示,编译器层面的优化指的是以_result取代xx,并且之后的操作都是对_result来操作,这种优化被称为NRV(Named Return Value)

2.3.6 Copy Constructor:要还是不要?

根据具体情况分析即可!

2.4成员们的初始化队伍

必须使用成员初始化列表(member initialization list)的四种情况:

1.当初始化一个reference member时

2.当初始化一个const member时

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

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

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

上述代码中,class的构造函数在调用时,会产生一个暂时性的String object并初始化,再以assignment运算符将暂时性object赋予_name,然后再摧毁这个tmp,具体过程伪代码如下:

Word::Word(//参数内含一个this pointer)
{
	//调用String的default Constructor
	_name.String::String();
	//产生暂时性对象
	String temp = String(0);
	//memberwise拷贝
	_name.String::operator=(temp);
	//摧毁暂时性对象
	temp.String::~String();
	
	_cnt = 0;
}

如果使用的是初始化成员列表,那么将更有效率:

Word::Word:_name(0)
{
	_cnt = 0;
}
//伪代码
Word::Word(//参数内含一个this pointer)
{
	//调用String(int)constructor
	_name.String::String(0);
	_cnt = 0;
}

PS:member initialization list的次序是由class中的members声明次序决定的,且被安插在explicit user code之前

PS1:初始化成员列表中能否调用一个member function来初始化成员?

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

答案是可以的,但是要注意的是务必使用存在于Constructor体内的一个member,而不要使用存在于member initialization list中的member,来为另一个member设定初值。扩充过程伪代码:

X::X(/* this pointer, */ int val)
{
	i = this->xfoo(val);
	j = val;
}

PS2:如果一个派生类成员函数被调用,其返回值被当做base class Constructor的一个参数,例如

class FooBar:public X //base class X
{
	int _fval;
public:
	int fval() {return _fval;} //derived class member 
	FooBar(int val):_fval(val),X(fval()) //fval()作为base class Constructor的参数
};

这样操作是错误,扩张过程伪代码如下:

FooBar::FooBar(//this pointer)
{
	X::X(this, this->fval());
	_fval = val;
}

上述代码中,编译器会调整初始化成员列表的顺序来适配成员变量的声明次序,因此上述代码会出现错误。

猜你喜欢

转载自blog.csdn.net/sinat_25394043/article/details/81333176
今日推荐