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;
}