深度探索c++对象模型2

这一章主要是编译器对于“对象构造过程”的干涉以及对于“程序形式”和“程序效率”的冲击

Default Constructor的构造操作

  • 带有默认构造函数的Member Class Object

    总结而言就是,如果对象a中依次有对象b,c,d,如果程序员仅仅初始化了c(10),那么编译器会调用b,d的默认构造函数,顺序是b,c(10),d;另外,对于对象g,假设里面只有int c;string d;编译器的隐式默认构造函数是不会帮忙解决这两者的初始化问题的,也就是这个构造函数是trivial的

  • 带有默认构造函数的Base Class

    一个类的构造函数,需要调用一些基类的构造函数(必要之默认构造函数),你自定义的构造函数,编译器会帮你补上调用那些构造函数的部分;先调用基类的默认构造函数然后调用成员类的默认构造函数

  • 带有一个Virtual Function 的 Class

    以下两种情况,需要编译器合成出default constructor

    • class声明(或者继承)一个virtual constructor
    • class派生自一个继承串链,其中有一个或者更多的virtual base classes
      以上两种情况如果缺乏用户声明的constructor,那么编译器会详细记录合成一个default constructor 的必要信息。主要有以下两种扩张行动:
    • 一个虚函数表会被编译器产生出来,存放着类的虚函数地址
    • 一个vptr会被编译器合成出来,包含着虚函数表的地址
  • 带有一个Virtual Base Class 的 Class

    C继承A、B,A虚继承X,B虚继承X;根据不同编译器,在构造这些对象的时候会有类似指针的东西指向虚继承的类里面的成员,这些都是编译器在类对象构造期间完成的;对于这样的类所定义的每一个构造函数,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的代码。如果class没有声明任何的constructors,编译器必须为它合成一个default constructor。;

  • 总结:

    上面四种情况,会造成:编译器必须为没有声明构造函数的类合成一个默认构造函数“,这些合成物被称为隐式非平凡默认构造函数(implicit nontrivial default constructors)。除此之外都是隐式平凡构造函数,实际上不会被合成出来。

    在合成出来的这些构造函数里面,只有base class subobjects和member class objects会被初始化,其他的都不会。

Copy Constructor 的构造操作

  • 下面三种情况,会以一个object的内容作为另一个class object的初值

    • 显式地以一个对象的内容作为另一个类对象的初值

      1
      2
      X x;
      X xx=x;
    • 当对象被当做参数交给某一个函数的时候

    • 当函数传回一个类对象的时候

    如果类的设计者显式定义了一个拷贝构造函数,就会调用它

  • 默认的拷贝构造函数

    当类没有提供一个显示的拷贝构造函数时候使用,用递归的方式实行member initialization,比每一个内建的活着派生的data member的值从一个object拷贝到另一个上面。

    下面讨论的是隐式的拷贝构造函数编译器是否会合成一个default copy constructor的问题。根据C++标准,决定一个copy constructor 是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。(只有nontrivial的实例才会被合成于程序里面。)

  • bitwise copy semantics(位逐次拷贝)

    什么时候一个class不展现出所谓的位逐次拷贝呢?

    • 一个class中有一个成员变量的class里面声明了一个copy constructor
    • 这个class继承自一个基类,然后这个基类里面有一个copy constructor
    • 当class声明了一个或者多个virtual functions(考虑vptr的拷贝问题)
    • 当class派生自一个继承串链,其中有一个或者多个virtual base classes(发生在一个class object 以其derived classes的某一个对象作为初值的时候,编译器需要安插一些代码来设定virtual base class pointer/offset 的初值)

程序转化语义学(Program Transformation Semantics)

  • 主要从初始化,参数初始化,返回值初始化三个角度探讨了拷贝构造函数的应用及应用的伪码;

  • 显示初始化:

    1
    2
    3
    4
    void foo_bar(){
    X x1(x0);
    X x2=x0;
    X x3=X(x0);

转换成的可能的伪码:

Alt
Alt

其中:

1
x1.X::X(x0);

//表现为对一下copy constructor 的调用:

1
X::x(const X& xx);
  • 参数初始化:
    对于一下子调用方式:

    1
    2
    3
    4
    void foo(X x0);
    X xx;
    //...
    foo(xx)

    可能的伪码:

    1
    2
    3
    X __temp0;
    __temp0.X::X(xx);
    foo(__temp0);
  • 返回值的初始化:
    对于以下函数:

    1
    2
    3
    4
    5
    X bar(){
    x xx;
    //....
    return xx;
    }

    转化为如下伪码:
    Alt

  • NRV优化:

    NRV(name returned value)优化大致如下,我觉得是通过把返回的临时变量变成一个引用形参来实现的;

    Alt

    NRV优化需要一个copy constructor,(最好是内联的提高效率)

  • 最后探讨了copy constructor 要还是不要的问题,我觉得它的含义是,从速度角度来看,如果存在NRV优化的可能性,以及传值的要求,那么实现拷贝构造函数可以帮助实现这一点;

同时实现拷贝构造函数准备使用memcpy,memset的时候要注意是否有虚函数或者含有虚基类,防止错误的改变内部的vtpr;

比如下图:

Alt
Alt
Alt
Alt

成员初始化表

  • 必须使用成员初始化表的四种情况:

    1. 当初始化一个reference member
    2. 当初始化一个const member;
    3. 当调用一个base class 的constructor,而它拥有一组参数的时候;
    4. 当调用一个member class的constructor,而它拥有一组参数的时候;
  • 使用的注意点:

    1. 顺序问题,成员初始化表的初始化顺序是声明的顺序,因此如下代码会有bug:
    1
    2
    3
    4
    5
    6
    7
    8
    class X{
    int i;
    int j;
    public:
    X(int val):j(val),i(j){
    ;
    }
    }

    因为实际执行的时候是先i(j)然后j(val)的;

    另外成员初始化表在显式代码的前面,因此

1
2
3
4
5
6
7
8
class X{
int i;
int j;
public:
X(int val):j(val){
i=j;
}
}

是合法的;

  • 本书不太建议在成员初始化表里面调用一个member function进行初始化,主要因为不清楚具体的依赖关系的问题;当然如下图的伪代码,这是合法的:
Alt
Alt

-