有三种情况,会以一个object的内容作为另一个class object的初值:
-
对一个object做明确的初始化操作,像这样:
class X{...}; X x; //明确地以一个object的内容作为另一个class object的初值 X xx = x;
-
当object 被当作参数交给某个函数时,例如:
extern void foo(X x); void test(){ X xx; foo(xx); //以xx作为foo()第一个参数的初值(不明显的初始化操作) }
-
当函数传回一个class object时,例如:
X foo_bar(){ X xx; return xx; }
Default Memberwise Initialization
如果函数并没有提供一个explicit copy constructor
,那么其拷贝同类型对象的操作由default memberwise initialization
完成,其执行策略为:对每一个内建或派生的data member
的值,从某一个object
拷贝到另一个object
。不过它不会拷贝其中的member class object
,而是实施递归式的memberwise initialization
(对每一个对象依次执行default memberwise initialization
)。
Default constructors 和 copy constructors在必要的时候才由编译器产生出来。
Bitwise Copy Semantics(位逐次拷贝)
看看下面的程序段:
#include"Word.h"
Word noun("book");
void foo()
{
Word verb = noun; //...
}
我们不可能预测这个初始化操作的程序行为。如果class Word的设计者定义了一个copy constructor,verb的初始化操作会调用它。但如果该class 没有定义explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就得视该class 是否展现"bitwise copy semantics”而定。
举个例子,已知下面的class Word 声明:
//以下声明展现了bitwise copy semantics
class Word{
public:
Word(const char*);
~Word(){ delete []str;} //..
private:
int cnt;
char *str;
};
这种情况下并不需要合成出一个default copy constructor,因为上述声明展现了“default copy semantics”。
再如下面的例子:
//以下声明并未展现出bitwise copy semantics
class Word{
public:
Word(const String&);
~Word(){ delete []str;} //..
private:
int cnt;
String str;
};
其中String声明了一个显示的拷贝构造函数
class String{
public:
String(const char*);
~String();
//...
};
在这个情况下,编译器必须合成出一个copy constructor 以便调用member class String object的 copy constructor:
什么时候一个class不展现出“bitwise copy semantics”呢?有四种情况:
- 当class内含一个member object而后者的class声明有一个copy constructor时(不论是被class 设计者明确地声明,就像前面的String那样;或是被编译器合成,像class Word那样)。
- 当class继承自一个base class而后者存在有一个copy constructor时(再次强调,不论是被明确声明或是被合成而得)。
- 当class声明了一个或多个virtual functions时。
- 当class派生自一个继承串链,其中有一个或多个virtual base classes时。
前两种情况中,编译器必须将member或base class的“copy constructors调用操作”安插到被合成的copy constructor中。
虚函数指针
编译时期有两个程序的扩张操作:
■ 增加一个virtual function table(vtbl),内含每一个有作用的 virtual function的地址。
■ 将一个指向virtual function table的指针(vptr),安插在每一个class object内。
很显然,如果编译器对于每一个新产生的class object
的vptr
不能成功而正确地设好其初值,将导致可怕的后果。因此,当编译器导入一个vptr
到class
之中时,该class
就不再展现bitwise semantics
了。现在,编译器需要合成出一个copy constructor
,以求将vptr
适当地初始化,下面是个例子。
以一个派生类对象作为其父类对象的初始值,编译器需要”判断“
程序转化语义
下面的程序:
#include <iostream>
X foo(){
X xx;
// ...
return xx;
}
可能会做出以下假设:
- 每次
foo0
被调用,就传回xx的值。 - 如果
class X
定义了一个copy constructor
,那么当foo0
被调用时,保证该copy constructor
也会被调用。
第一个假设的真实性,必须视
class X
如何定义而定。第二个假设的真实性,虽然也有部分必须视class X
如何定义而定,但最主要还是视你的C++编译器所提供的进取性优化程度(degree of aggressive optimization)而定。你甚至可以假设在一个高品质的C++编译器中,上述两点对于class X的nontrivial definitions都不正确。
显式初始化操作(Explicit Initialization)
下面的例子是三种显示的拷贝构造操作:
void test() {
X x0;
X x1(x0); // 第一种显示拷贝构造
X x2 = x0; // 第二种显示拷贝构造
X x3 = X(x0); // 第三种显示拷贝构造
}
上面的程序转化可能会有两个阶段:
- 重写每一个定义,其中的初始化操作会被剥除。
- class的copy constructor 调用操作会被安插进去。
可能会变成下面这样
void test_Cpp()
{
// ... x0的构造函数
// 三个声明
X x1,x2,x3;
// 三个定义
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
}
参数的初始化(Argument Initialization)
C++标准说,把一个class object 当做参数传给一个函数,相当于一下形式的初始化操作:
(定义一个类X,作为test函数的参数)
X xx;
test(xx);
其中test函数的声明如下:
void test(X x0);
上面的参数xx这个变量作为参数传到函数test中,会产生一个临时对象,并且会调用拷贝构造函数将这个临时对象初始化,经过这些步骤这个参数才真正传入函数中进行使用。
所以上面的代码可能会转换为:
X __temp; // 临时对象产生
__temp.X::X(xx); //编译器对拷贝构造函数的调用
test(__temp); //重新改写函数的调用操作
然而上面的做法只完成了一般,因为函数xx的声明还是一个传值的参数,test的声明也也须被转化,形式参数必须从原先的一个class X object改变为一个class X reference,像这样:
void test(X& x0);
返回值的初始化(Return Value Initialization)
例如下面的例子:
X test()
{
X xx;
return xx;
}
编译器可能会做如下NRV
优化:
X __result;
void test(X& __result)
{
__result.X::X();
return;
}
当类显示Bitwise的情况下,应该不去定义一个拷贝构造函数,使用编译器合成就非常高效和安全!