深入探索C++对象模型之二 --- 构造函数语意学

深入探索C++对象模型之二 — 构造函数语意学

2.1 默认构造函数

当编译器需要的时候,default Constructor才会被合成出来,而且只执行编译器需要的任务。

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

对于上面的类对象是不会对两个成员进行初始化的,因为default Constructor只是做编译器需要的任务,而初始化成员变量很显然是程序员的事情。所以这个default Constructor是没啥用的(我们后面会看到对于这种情况的类,default Constructor都不会被合成出来!)

那么什么情况下才会是一个有用的default Constructor,才需要执行编译器的任务呢?
C++ standard一共叙述了四种情况:

  1. “带有Default Constructor” 的Member Class Object
    如果一个类还有一个member Object,并且该Object有default Constructor, 那么便一起就会自动合成一个default Constructor,当然这个合成过程只有在constructor真正被调用的时候才会发生。合成的default Constructor会在内部调用member Object的默认构造函数初始化该member Object。那么如果有多个memeber Object则会按照声明的顺序依次调用它们的default Constructor,而如果已经有定义好的constructor函数该怎么办呢?编译器会在这些构造函数中插入必要的代码来初始化member Object,之后再执行user code。
  2. “带有Default Constructor” 的Base Class
    如果一个类是派生自“带有default constructor”的base class,那么这时候编译器也会为该类合成一个default Constructor,它将调用base class 的constructor。同样的如果程序设计者已经声明了constructor,那么编译器就会插入必要的代码优先初始化base class。当第一种情况和第二种情况并存的时候,优先执行base class的constructor,然后再执行member Object的default Constructor,最后再是user code。
  3. “带有一个Virtual Function” 的Class
    如果一个类声明或者继承一个virtual Function,那么也编译器也需要合成default constructor。因为类存在虚函数,那么编译期间编译器会为该类产生一个virtual Function table(vtbl),里面存放virtual Functions的地址。而对于每个类对象,在初始化的时候就需要由编译器为它产生一个pointer member(vptr)指向vtbl。这样子就可以在执行期的时候根据该vptr完成多态的机制。

    另外,对于调用虚函数的地方也会被重写,都会被改写成vptr+函数索引的调用方式。诸如:

        (*widget.vptr[1])(&widget)
  4. “带有一个Virtual Base Class” 的Class
    如果类继承自一个或者多个virtual Base class,那么编译器必须确保使virtual base class在其每一个derived class object中的位置,能够于执行期准备妥当。如下case:

    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必须要等到执行期才能确定下来,因为pa的类型不固定,可能是A也可能是C,这时候就需要在class object中的每一个virtual base class中安插一个指针,所有经由reference或者pointer存取virtual base class的操作都可以通过相关指针来完成。

    void foo(const A *pa) { pa->_vbcX->i = 1024;}

    上面的_vbcX指针就是编译器所产生的指针,用来指向virtual base class X。

以上讲述的四种情况下编译器是需要合成一个default Constructor来完成它自己需要完成的任务的,并且合成的default constructor也只是完成上面四种情况需要完成的任务,对于其它一些member data的初始化是不操作的。

总结:

以下两种观点是错误的:
1. 任何class如果没有定义default constructor,就会被合成出一个来
2. 编译器合成出来的default constructor会明确设定“class内每一个data member的默认值”

根据上面的分析以上两种观点都是错误的。

2.2 拷贝构造函数 Copy Constructor

什么情况下我们会需要Copy Constructor函数呢?就是以一个object的内容作为另一个class object的初值
1. 对一个object做明确的初始化操作
2. 当object被当作参数交给某个函数时
3. 当函数传回一个class object时

假如class设计者明确定义了一个copy constructor,当遇到上面的情况时候就会调用该copy constructor,这可能会导致一个暂时性class object的产生或者程序代码的优化。那么如果class没有提供一个显式的copy constructor呢?

这里内部会以所谓的default memberwise initialization手法完成,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份倒另一个object中,不过并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。

这里我们要引入两个概念(位拷贝和值拷贝、浅拷贝和深拷贝)
位拷贝是指将一个对象的内存映像按位原封不动的复制给另一个对象,而值拷贝就是将原对象的值复制一份给新对象,值拷贝会复制对象持有的资源。

上面提到的default memberwise initialization就是位拷贝,它只是简单的将原先的内容拷贝一份到目标object中,如果类中存在指针之类的资源,那么使用位拷贝的话两个object的指针都是指向同一个内存区域,当其中一个object释放了资源的话另外一个object此时的指针就是野指针,这很明显是错误的,此时就需要深拷贝,就应该编写operator= 和copy constructor来实现值拷贝。

可以得到:如果类不表现为bitwise Copy Semantics的时候,此时default memberwise initialization的位拷贝行不通,那么编译器就有义务合成一个copy constructor来执行深拷贝。

那么什么情况下类才不表现为bitwise copy Semantics呢?
1. 当class内含一个member object而后者的class声明中有一个copy constructor时
2. 当class继承自一个base class而后者有一个copy constructor时
3. 当class声明了一个或者多个virtual functions时
之前说过对于有virtual function的class,对于每一个class object都会安插一个vptr指针指向虚函数表,那么编译器必须初始化这个指针指向正确的地址,因此当安插了vptr的时候,class就不再表现为bitwise Senmantics了。
4. 当class派生自一个继承单链,其中有一个或多个virtual base classes时
如果是相同class的object之间复制那么bitwise copy已经是绰绰有余了,但是问题的关键是以derived class object复制给base class object,这时候编译器就需要判断后续程序调用base class subobject的时候是否能够正确的执行。这时候就需要合成copy constructor来设定virtual base class pointer/offset的初值。

总结:
通过上面的分析,可以得到对于那些展现为bitwise copy Semantics的class,此时default memberwise initialization浅拷贝已经足够了,但是对于满足四种情况的不展现出bitwise Semantics的class,此时编译器必须要合成一个copy constructor来进行必要的工作。

2.3 程序转化语意学

这里考虑两种情况,第一种是object作为函数的参数传递,第二种情况是object作为函数的返回值

明确的初始化操作

c++
X x0;
void foo_bar() {
X x1(x0);
X x2 = x0;
X x3 = X(x0);
}
那么程序可能会被转化成:
void foo_bar() {
X x1;
X x2;
X x3;
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0); //编译器安插了copy constructor的调用
}

参数的初始化
X xx;
foo(xx);

会被改写成:
X _temp0;
_temp0.X::X(xx);
foo(_temp0);

这里就会产生一个临时性的对象_temp0

返回值的初始化
X bar() {
    X xx;
    //...
    return xx;
}
可能被转化成:
void bar(X &_result) {
    X xx;
    xx.X::X();
    //....
    _result.X::X(xx);
    return;
}

通过在函数中安插一个额外的参数来放置返回值

在使用者层面做优化
在编译器层面做优化

上面所说的返回值优化的方式成为Named Return Value(NRV)优化,但是这种优化必须有一个copy constructor。

以下三种初始化操作在语意上是相等的,但是第一个效率最高,因为第二个会先产生一个临时性的object,初始化该object之后再复制给目标object

X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;

通过上面的分析,当object作为函数的参数传入或者是函数的返回值return的时候,都可能会进行必要的优化,优化都调用到了copy constructor。那么如果class表现出bitwise Semantics的时候,默认的memberwise initialization已经足够了,但是如果我们预期该类会有大量的memberwise初始化操作的时候,那么我们可以提供一个explicit inline copy constructor,这样子可以让编译器开启NRV优化。

2.4 成员们的初始化队伍

当我们在constructor中设置class member的初值时,要么在member initialization list中,就是在函数体之内。一般情况下两种方式都差不多,只有以下四种情况必须使用member initialization list才可以:
1. 当初始化一个reference member时
2. 当初始化一个const member时
3. 当调用一个base class的constructor,而它拥有一组参数时
4. 当调用一个member class的constructor,而它拥有一组参数时

编译器会对member initialization list中的顺序重新排序,初始化是按照变量的声明顺序来的,这些都会在user code之前执行。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值