【深度探索C++对象模型】(2)构造函数语意学
1.Default Constructor的构造操作
默认构造函数是在编译器需要的时候构建出来的,被合成的默认构造函数只执行编译器所需的动作。被合成的默认构造函数中只有base class subobjects以及member class objects会被初始化,而其他nonstatic data member(如整数、指针、数组)都不会初始化,因为他们是满足程序需要的。
有四种情况会使得编译器为未声明constructor的classes合成一个满足编译器需要的implicit nontrivial default constructor。对于其他情况又没有声明任何constructor的,实际上并不会合成。
1.1 带有Default Constructor的Member Class Object(一个类的成员类具有默认构造函数。)
若一个class没有constructor但有member object,而这些member object有default constructor,那么编译器就会为该class合成一个inline的default constructor,合成的constructor将会按照member class object声明顺序调用其member object。
class Foo {
public:
Foo(){ /* ...*/ };
Foo(int i){ /* ... */ };
}
class Bar{
public:
Foo foo;
char *str;
}
//Bar合成的default 构造函数很有可能是
inline Bar::Bar(){
//Bar::str的初始化是程序员的责任
foo.Foo::Foo();
}
由于合成的默认构造函数并不会初始化其他nonstatic data member(如整数、指针、数组),所以需要程序员来进行初始化操作,而编译器可以对程序员写的每个构造函数进行扩张,使其满足编译器需求。
//假如我们写一个默认构造函数
inline Bar::Bar(){
str = 0;
}
//它会被扩张成
inline Bar::Bar(){
foo.Foo::Foo();
str = 0;
}
假如我们不要调用Member Class Object的默认构造函数,我们可以这样:
inline Bar::Bar() : foo( 1024 ) {//这里并不会改变扩张后的调用顺序
str = 0;
}
//他会被扩张成
inline Bar::Bar(){
foo.Foo::Foo( 1024 );
str = 0;
}
1.2 带有Default Constructor的Base Class(一个派生类的基类具有默认构造函数。)
与1.1类似,若一个没有默认构造函数的class派生自一个有默认构造函数的base class,那么它将按照base classes的声明顺序调用上一层base classes的default constructor,也与1.1一样可以对每个constructor进行扩张操作。
1.2的操作将优先于1.1的操作进行。
1.3 带有一个Virtual Function的class(带有虚函数的类。 )
若class声明或继承一个virtual function,编译器会在编译期间合成default constructor或扩张所有的构造函数进行以下操作:
- 产生一个virtual function table (即vtbl),内放class的virtual functions地址。
- 在每个class object中合成一个额外的pointer member (即vptr),指向相关class vtbl地址。
同时虚函数的虚拟调用操作将会重新改写为使用vptr和vtbl的条目。
class Widget {
public:
virtual void flip() = 0; //pure virtual
};
class Bell : public Widget{};
class Whistle : public Widget{};
void flip(const Widget& widget) {
widget.flip(); //将被改写为
//(*widget.vptr[ 1 ] )( &widget);
}
void foo(){
Bell b;
Whistle w;
flip(b);
filp(w);
}
1.4 带有Virtual Base Class 的 class(一个派生类,该派生类的继承体系中含有虚基类(虚继承)。)
必须使virtual base class在其每一个derived class object中的位置,可以在执行期准备妥当。
可以使编译器在class object构造使其安插一个指向虚基类的指针_vbcX,然后所有仅有reference或pointer来存取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; }
//可能会被编译器转变成
void foo(const A* pa) { pa->_vbcX->i = 1024; }
main{
foo(new A);
foo(new C);
}
1.5 一些误区的实际情况
- 并不是任何class没有定义default constructor就一定会合成一个出来。
- 合成出的default constructor也并不会设定class内每一个data member的默认值。
2 Copy Constructor的构造操作
有三种情况会使用copy constructor:
- 显式的对一个object做一个初始化操作.
- object通过传值交给函数。
- 函数返回一个非引用class object。
2.1 Default Memberwise Initialization(默认成员逐一初始化)
如果一个class没有提供任何的copy construct,那么在进行拷贝构造是,class内部是以default memberwise initialization的手法完成的。实际上就是bitwise copies(位逐次拷贝),即把class object中的所有data members按顺序一个一个拷贝到另一个object身上,如果有data members是类类型,那么就会递归地施行bitwise copies。
class String{
public:
//.......无copy constructor
private:
char *str;
int len;
};
class Word {
public:
Word(int i, String s) : _occurs(i), _word(s) {}
private:
int _occurs;
String _word; //String object称为一个data member
};
Word word1(2, "word");
Word word2 = word1;
那么最后一行很有可能会是这样的,这不是copy constructor,而是default memberwise initialization!
word2._occurs = word1._occurs;
//word2._word = word1._word;
//递归展开
word2._word.str = word1._word.str;
word2._word.len = word1._word.len;
2.2 不要 Bitwise Copy Semantics
C++ Standard说若class没有声明一个copy constructor就会有隐式的声明或定义。实际上只有nontrivial的实力才会被合成在程序中,即只有class不展示出bitwise copy semantics的时候.例如
class String{
public:
String( const char* );
String( const String& );
private:
char *str;
int len;
};
class Word {
public:
Word(int i, String s) : cnt(i), str(s) {}
private:
int cnt;
String str; //String object称为一个data member
};
这种情况中Word没有展示出bitwise copy semantics,故会生成copy constructor:
inline Word::Word( const Word& wd){
str.String::String( wd.str );
cnt = wd.cnt;
}
因此,一个类的copy语意有三种情况:
- 存在copy constructor,使用copy constructor;
- 下面上面的四种情况,编译器帮助合成copy constructor;
- bitwise copy;
有四种情况表示class不展示出bitwise copy semantics:
- 当class中含有一个声明有copy constructor的member object时。(无论是显式还是被合成)
- 当class继承自有一个声明有copy constructor的base object时。(无论是显式还是被合成)
- 当class声明了virtual functions。
- 当class派生自一个继承链串,其中有virtual base classes时。
第一种和第二种情况不必多说,下面主要说第三第四种情况,当编译器导入一个vptr到class的时候,该class就不再展现bitwise semantics了。
2.2.1 重新设定Virtual Table的指针
现在,编译器需要合成一个copy constructor以求将vptr适当的初始化。假设有这样的继承关系:
class ZooAnimal {
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void draw();
virtual void animate();
};
class Bear : public ZooAnimal {
public:
Bear();
virtual ~Bear();
void draw(); //virtual function
void animate();
virtual void dance();
};
当一个class object以其class的另一个class为初始值时,种情况都可以直接靠“bitwise copy semantics”来完成(除了pointer member)。、例如一个ZooAnimal class object以另一个ZooAnimal class object为初值或者Bear class object以另一个Bear class object为初值:
Bear yogi;
Bear winnie = yogi;
相同 class 的 objects 的 vtbl 相同。在constructor中,yogi的vptr被设定指向Bear class的virtual table。
当一个base class object以其derived class的object为初值时,合成的显式构造函数会设定object的vptr指向base class的virtual table,而不是之前从右手边的object拷贝。
void draw( const ZooAnimal& zoey ) { zoey.draw(); }
void foo(){
ZooAnimal franny = yogi;//发生切割,franny的vptr指向ZooAnimate的vtbl而非Bear的
draw(yogi); //调用Bear::draw()
draw(franny); //调用ZooAnimate::draw()
2.2.2 处理Virtual Base Class Subobject
virtual base class需要特别处理,编译期必须让处于derived class object中的virtual base class subobject的位置在执行期就准备妥当。
假如有这样的继承关系:
class Raccoon : public virtual ZooAnimal {
public:
Raccoon() {}
Raccoon(int val) {}
};
class RedPanda : public Raccoon {
public:
RedPanda() {}
RedPanda(int val) {}
};
如果一个class object以其derived classes的某个object为初值(例如一个Raccoon object作为另一个Raccoon object的初值),“bitwise copy”就足够了。
Raccoon rocky;
Raccoon little_critter = rocky;
但是如果以derived object作为base object的初值,如以RedPanda object作为Raccoon object的初值,编译器必须判断**“能否正常执行存取ZooAnimal的subobject的动作”(进行切割),这种情况下编译器必须合成一个copy constructor,安插一些代码以设定virtual base class pointer/offset的初值**,对每个members执行必要的memberwise初始化以及执行其他的内存相关工作。
RedPanda little_red;
Raccoon little_critter = little_red;
对指针而言,“bitwise copy”可能够用,也可能不够用,因为编译器无法知道一个base class指针是否指向一个真正的base class object,或是指向一个derived class object。
3.程序转化语意学
#include <iostream>
using namespace std;
//加载头文件
#include "X.h"
X foo(){
X x_1;
//对对象x_1进行处理的相关操作。
return x_1;
}
两种正常假设:
- 每调用一次foo()函数,会返回一个对象x_1的值。
- .应该会调用类中的拷贝构造函数。
两个假设的正确性需要参看类X中的定义。
3.1 显式的初始化操作(Explicit Initialization)
若有下面程序
void foo_bar(){
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}
上面三种初始化操作显式的用x0初始化三个对象。但是在实际的编译器中可能会发生如下的转换。
- 重写定义,其中的初始化操作会被剥离 。
- 调用相关的拷贝构造函数。
转化后的代码有可能如下
void foo_bar(){
X x1;
X x2;
X x3;
x1.X::X(x0); //调用拷贝构造函数。
x2.X::X(x0);
x3.X::X(x0);
//可能在类X中会有类似的声明:
//X::X(const X&);
}
3.2 参数的初始化
当将一个class object作为参数以传值方式给一个函数或作为一个非引用返回值,将会导入<mark>临时对象</mark>策略,调用copy constructor将它初始化,然后将临时对象交给对象(或返回),同时根据需要将参数或返回值改为引用。假如有代码
void foo( X x0 );
X xx;
foo(xx);
可能会转换如下:
//第一步
void foo( X& x0 );
X xx;
//第二步
X __temp0;
__temp0.X::X( xx );
foo( __temp0 );
//第三步
__temp0.X::~X();
3.3返回值的初始化
对于返回值做两阶段的转化:
- 加入一个类型为class object的引用的额外参数作为返回值。
- 在return前安插一个copy constructor。将要返回的结果拷贝给引用的额外参数。<mark>(故要求一定要有拷贝构造函数才启动)</mark>
假如有代码:
X bar(){
X xx;
return xx;
}
//1
X xx = bar();
//2
bar().mem_func();
//3
X (*pf)();
pf = bar();
很有可能改写如下:
void bar(X& __result){
X xx;
xx.X::X();
__result.X::X(xx);
return;
}
//1
X xx;
bar( xx );
//2
X __temp0;
(bar( __temp0 ), __temp0 ).mem_func();
//3
X (*pf)( X& );
pf = bar();
3.4 在编译器层次做优化(NVR优化)
对返回值进一步优化,并不是在最后将要返回的结果拷贝给引用的额外参数,而是直接使用额外参数作为要返回的对象进行操作。
X bar(){
X xx;
//对xx操作
return xx;
}
很有可能改写如下:
void bar(X& __result){
__result.X::X();
//直接处理__result
return;
}
3.5 是否需要copy constructor?
假如class需要大量的memberwise(深拷贝)初始化操作,例如以传值(by value)的方式传回object,那么提供一个copy constructor的explicit inline函数实例就非常合理了。
3.6成员们的初始化队伍(Member Initialization List)
下列四种情况必须使用成员初始化列表:
- 当初始化一个reference member时;
- 当初始化一个const member时;
- 当调用一个base class的constructor,而它拥有一组参数时;
- 当调用一个member class的constructor,而它拥有一组参数时;
实际上在四种情况下不用Initialization List仍然可以正确编译执行,但是效率低。
class Word {
String _name;
int _cnt;
public:
Word() {
_name = 0;
_cnt = 0;
}
};
在以上程序中Word constructor会产生临时String对象,初始化后再给_name,最后再摧毁临时对象:
Word::Word {
_name.String::String();
String temp = String(0);
_name.String::operator=(temp);
temp.String::~String();
_cnt = 0;
}
正确高效的方法如下是
Word::Word() : _name(0), _cnt(0) {}
扩张后的结果为
Word::Word() {
_name::String::String(0);
_cnt = 0;
}
成员初始化列表中的初始化顺序是按照声明顺序来的,与initialization list顺序无关,并且initialization list的项目要先于explicit user code。