本来,第二章的标题是“The Semantics of Constructors (构造函数语意学)”,晦涩难懂,但实际很简单,讲的就是constructor和copy constructor在编译阶段的构造规则。
1. 构造函数(Constructor)
首先,有两个问题,绝大多数人会认为它们是正确的:
当然,上面两个问题都不是正确的。我们先看第一个问题,如果没有定义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);
}
编译期间,如下两个操作会发生:
4)一个class有virtual base class(虚基类),即一个class以virtual方式继承父类
关于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的生成也是水到渠成了。
/* user-defined copy constructor */
X::X( const X& x);
Y::Y( const Y& y, int = 0);
注意,声明时的const及&不要忽略。
class Word
{
public:
// ... no explicit copy constructor
private:
int cnt;
String str;
};
X x0;
void foo_bar()
{
X x1(x0); // definition of x1
X x2 = x0; //definition of x2
X x3 = X(x0); //definition of x3
// ...
}
会对源码进行一个两阶段的转化:
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 );
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直接放在程序堆栈中使用,这个是不同编译器策略决定的。
X bar()
{
X xx;
// handle xx...
return xx;
}
这也许是最令人感到迷惑的地方,如果X是一个局部变量的话,那么就不可能被返回(因为返回前便已经销毁了)。我们来看看为什么可以这样做。因为会重写代码,它也经历了一个两阶段的转化:
void bar( X& __result ) //1. 加上一个额外参数
{
X xx;
xx.X::X();
// handle xx ...
// 2. 安插一个copy constructor用于返回
__result.X::X( xx );
return;
}
class Word{
public:
int name;
int age;
int cnt;
public:
Word::Word()
:cnt(0), name(0)
{
age = 5;
}
};