《深入探索C++对象模型》第二章 构造函数语义学(The Semantics of Constructors)

一、default constructor的构造操作

先看一个小例子:

class Foo { public: int val; Foo *pnext; };

此处正确的程序语意是要求Foo有一个默认构造函数,能将它的两个成员初始化为0,那么编译器会为我们合成这样的默认构造函数吗?答案是不会!(事实上,这里合成的默认构造函数是trival的构造函数,也就是它什么也不干,连初始化也不干。在概念上我们把这种构造函数叫做implicit trival default constructors,但实际上这种构造函数根本不会被合成出来)。这是因为初始化成员是程序的需要,而不是编译器的需要,本例中要承担成员初始化责任的是设计者。

在四种情况下,non-trival default constructor会被编译器合成出来,下面一一讨论这四种情况。

1. 带有Default constructor的Member Class Object

如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么编译器就需要为该class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时才会发生。

这就引出一个问题,在C++的各个不同的文件中,编译器如何避免合成出多个default constructor?解决办法是把合成的default constructor、copy constructor、deconstructor,copy assignment operator都以inline方式完成,一个inline函数具有静态链接,不会被文件以外者看到。如果函数太复杂不适合做成inline,就会合成出一个explicit non-inline static实例。(这一段没太明白)

下面是一个小例子:

class Foo { public: Foo(), Foo(int) ... };
class Bar { public: Foo foo; char *str; };

void foo_bar()
{
    Bar bar;   //Bar::foo在这里必须被初始化

    if( str ) { }...
}

在本例中,编译器将为class Bar合成一个default constructor,内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但是要注意,这个default constructor依然不会负责str的初始化!

但是如果class Bar已经有了构造函数,那么编译器会怎么做呢?由于Bar有构造函数,所以编译器不能再为class Bar合成一个默认构造函数。编译器的做法是:如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member class的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code被执行之前,先调用必要的default constructors。值得注意的是,调用的default constructor的顺序和member objects在class中的声明顺序相同。看下面这个例子:

class Dopey { public: Dopey(); ... };
class Sneezy { public: Sneezy( int ); Sneezy(); ... };
class Bashful { public: Bashful(); ... };

class Snow_White{
public:
    Dopey dopey;
    Sneezy sneezy;
    Bashful bashful;
private:
    int mumble;
};// Snow_White 包含dopey,sneezy,bashful三个类的对象成员

假设class Snow_white有这样的构造函数:

Snow_White::Snow_White() : sneezy( 1024 )
{
    mumble = 2048;
}

那么它会被编译器扩张为:

Snow_White::Snow_White() : sneezy( 1024 )
{
    //插入member class objects
    dopey.Dopey::Dopey();
    sneezy.Sneezy::Sneezy(1024);
    bashful.Bashful::Bashful();

    mumble = 2048;
}

值得注意的是,以上这个例子除了体现了上面的知识点外,还能看到sneezy既存在于初始化列表中,又存在于隐式构造函数的调用中,那么这两者之间有什么联系吗?这个问题将在后面的内容中回答。

2. “带有Default Constructor”的Base Class

这里默认构造函数合成的逻辑与1中很相似,因为class里面包含一个类的对象成员和class里面有一个base class的part这两种情况在内存布局上就很相似。

这里需要注意的是,如果一个class里面既有基类的部分也有类的对象成员,那么先调用基类的默认构造函数,再调用类的对象成员的默认构造函数。

3. “带有一个Virtual Function”的Class

假设一个class声明(或继承)了一个virtual function,那么以下两个扩张行动会在编译期间发生:(1) 一个virtual function table会被编译器产生出来,里面包含class的virtual functions地址。(2) 在每一个class object中,一个额外的vptr会被编译器合成出来,指向相关的class vtbl的地址。

所以编译器必须为继承体系中的每一个类的对象中的vptr设定初值。如果class定义了构造函数,那么编译器会在构造函数中安插代码做这些事情;如果class没有定义构造函数,那么编译器就会生成一个默认构造函数,用以正确地初始化每一个class object的vptr。

4. “带有一个Virtual Base Class”的Class(这一部分理解起来有点tricky)

(这一部分对构造函数影响的逻辑和3有点类似,都是因为class具有某些特性,所以编译器必须为我们多做一些事情,要么是在已有的构造函数中安插代码,要么是为我们合成一个默认构造函数。至于这个特性到底是什么,编译器需要为我们做些什么,可以抽象出来考虑。以下就来聊一聊,对于有虚基类的class,编译器需要额外做点什么。)

virtual base class的实现法在不同编译器之间有着极大的差异。然而,每一种实现法的共同点在于必须使virtual base class在其每一个derived class object中的位置,能够于执行期准备妥当。看以下这个例子:

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( const A* pa ) { pa->i = 1024 };

编译器在编译期间无法确定经由pa存取的X::i的实际偏移位置,因为pa的实际类型可以改变,所以编译器必须改变执行存取操作的那些代码,从而使X::i延迟到执行期再确定下来。

二、Copy Constructor的构造操作

什么时候会调用一个类的拷贝构造函数呢?当使用一个类的对象对另一个类的对象进行初始化时。

1. Default Memberwise Initialization

当class object以相同class的另一个object作为初值,且class没有提供一个显式的拷贝构造函数时,其内部是以所谓的“逐成员初始化”来完成的,也就是把每一个内建的或派生的data member的值,从一个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的形式实行“逐成员初始化”。

合成默认拷贝构造函数的逻辑和合成默认构造函数的逻辑是类似的,也分为trivial和non-trivial两种,像只有逐成员初始化这种就是trivial的,只有non-trivial的实例才会被合成于程序之中。那么怎么判断是否trivial的情况呢?这取决于class是否展现出所谓的“bitwise copy semantics(位逐次拷贝)”

2. Bitwise Copy Semantics

在以下4中情况下,一个class不展现出“bitwise copy semantics”

(1) 当class内含一个member object而后者的class声明有一个copy constructor时,不论是被class设计者显式地声明还是被编译器合成(也就是说,递归地执行逐成员初始化这种情况也是non-trivial的?)

(2) 当class继承自一个base class而后者存在一个copy constructor(再次强调,无论是显式声明还是被编译器合成)

(3) 当class声明了一个或多个virtual function时

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

接下来重点讨论情况(3)和(4)

3. 重新设定virtual table的指针

如果编译器对于每一个新产生的class object的vptr不能成功且正确地设好初值,将导致可怕的后果,所以当编译器导入一个vptr到class之中时,class就不再呈现出bitwise semantics了。

所以在这种情况下,编译器会合成default copy constructor,指定object的vptr指向正确class的virtual table。因为copy constructor的参数是一个引用,所以也可能是派生类的类型。

4. 处理Virtual Base Class Subobject

一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使bitwise semantics失效。

当用相同class的object初始化另一个object时,bitwise copy是足够使用的,但是如果使用派生类的object初始化基类的object时,编译器就必须合成一个copy constructor,安插一些代码以设定virtual base class pointer/offset 的初值,对每一个member执行必要的memberwise初始化,以及执行其他的内存相关工作。

这里还存在一个有趣的问题:当一个初始化操作存在并保持着bitwise copy semantics时,编译器是否应该抑制copy constructor的调用?下一个部分会专门讨论一下这个问题。

三、程序转化语意学(Program Transformation Semantics)

(这部分内容不做总结)

四、成员们的初始化队伍(Member Initialization List)

在下列4中情况下,为了保证程序正确编译,必须使用member initialization list:

(1) 初始化一个reference member时

(2) 初始化一个const member时

(3) 当调用一个base class的constructor,而它拥有一组参数时

(4) 当调用一个member class的constructor,而它拥有一组参数时

除这4种情况之外,如果不使用member initialization list也可以正确编译,但是效率不高,比如:

class Word{
    String _name;
    int _cnt;

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

这里Word的构造函数会先产生一个临时的String对象,然后将它初始化,之后以一个赋值运算符将临时对象指定给_name,然后再销毁这个临时性对象。

成员初始化列表中到底发生了什么?编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何explicit user code之前。并且要注意的是:成员初始化的顺序是由class中members的声明顺序决定的,不是由Initialization list中的排列顺序决定的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值