【C++对象模型】第五章 构造、解构、拷贝 语意学

时间:2024-10-24 12:03:56

1、构造语义学

  C++的构造函数可能内带大量的隐藏码,因为编译器会扩充每一个构造函数,扩充程度视 class 的继承体系而定。一般而言编译器所做的扩充操作大约如下:

  1. 所有虚基类成员构造函数必须被调用,从左到右,从最深到最浅:
    • 如果该类被列于成员初始化列表中,那么如果有任何明确指定的参数,都应该传递过去。若没有列于列表之中,虚基类的一个默认构造函数被调用(有的话)。
    • 此外,保证的每一个虚基类子对象的偏移量(offset)必须在可执行期被存取。
    • 如果类对象代表最底层(most-derived)的类时,虚基类的构造函数才会被调用,这些代码才会被放进去。(直观说就是,虚基类的构造由最外层类控制)。
  2. 所有上一层的基类构造函数必须被调用,必须以基类的声明顺序为顺序(与成员初始化列表顺序没关联):
    • 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去。
    • 如果基类没有列入的话,就调用它的默认构造函数(如果有的话)。
    • 如果基类是多重继承下第二顺位或后继的基类,那么 this 指针必须有所调整。(对象一层一层向下构造,this 指针也从上到下指向不同的 subobject,构造终止时 this 要回到整个对象起始处(今天又看到一种说法,对象未构造完毕,其类型是base而不是derived,不止编译器这样解析,使用运行期类型信息(如dynamic_cast 和 typeid 也会解析为base类型,所以构造一个对象就相当于一个实物不断从基类对象进化,最终进化为deived对象,期间它是各种形态的,析构函数反之))。
  3. 如果类中含有 vptr,那么它们必须被设定初值,指向适当的 vtbl。
  4. 如果有一个成员对象没有出现在初始化列表之中,调用它的默认构造函数(如果有的话)。
  5. 记录在成员初始化列表中的数据成员的初始化操作会放进构造函数本身,并以成员的声明顺序为顺序。(普通数据成员是最后才构造的)。
  6. 在虚拟继承体系下,如果不是 most-derived 的类,那么其构造函数会被编译器加入一个bool __most_derived 参数,并赋值为 false,构造函数中判断该 bool 值,如果为 false 就不会构造虚基类,一直压抑虚基类的构造函数直到最外层也就是most-derived 类才构造,保证虚基类值构造一遍。
  7. 继承体系中,每一个构造函数中调用的函数会以静态方式决议之,因此即便某个基类和子类在构造函数中调用名字相同的函数,那么调用的也是基类的函数,子类此时还未构造出来。
  8. 拷贝赋值函数(copy assignment operator,即operator=)不具备 most-derived 特性!因为它没有像构造函数一样的执行列表,并且赋值函数是可以被取得地址的(就是说客户可见的意思)。所以在虚继承中,virtual base class subobject 会在不同层级的对象中多次被执行拷贝复制函数。

  如下图的继承关系:

  【C++对象模型】第五章 构造、解构、拷贝 语意学

  如果Vertex3d构造的时候,必然调用Point3d的构造函数,同时调用Vertex的构造函数,然而这两个类都要必须调用Point2d的构造函数,这是不合理的。取而代之的是应该在Vertex3d的构造函数中直接对Point2d初始化。这样就需要Vertex3d再调用Point3d或者Vertex的构造函数的时候传递一个bool参数__most_derived,即“是否是最后一层继承关系”,然后Point3d或者Vertex的构造函数根据这个bool变量决定是否构造Point2d。

  总结为一句话:virtual base class constructor,只有当一个完整的class object被定义出来时,它才会被调用。如果object只是某个完整的object的suboject,他就不会被调用。

  在base class constructor调用操作之后,但是在程序员提供的代码或是“member initialization list中所列的members初始化操作”之前编译器对vptr进行初始化。这个过程就像想象的那样:一个PVertex对象会先成为一个point2d对象。一个point3d对象、一个vertex对象和一个vertex3d对象,最后才成为一个PVertex对象。

  一个构造函数的真实步骤可能如下:

1.在derived class constructor 中,“所有virtual base classes”及“上一层base class”的constructor会被调用。

2.上述完成后,对象vptr(可能多个vptrs)被初始化,指向相关的virtual table(可能多个表)

3.如果有member initialization list 的话,将在constructor体内扩展开来。这必须在vptr被设定之后才进行,以免有一个virtual member function被调用。

  4.最后,执行程序员所提供的代码。

2、对象复制语意学

  2.1 类拷贝方式

  当设计一个class,并以一个class object 指定另一个class object时,有三种选择:

  1.什么都不做,实施默认行为。

  2.提供一个explicit copy assignment operator。

  3.明确拒绝一个class object指定给另一个class object。

  如果选择第三点,那么只要将copy assignment operator声明为private,并且不提供其定义即可。把它设定为private,我们就不在允许于任何地点(除了member function和该class的friend之中)做赋值操作。不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在链接是失败。

  对于第二点,只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy assignment operator。

  2.2 默认拷贝

  一个class对于默认的copy assignment operator,在以下情况,不会表现出bitwise copy语意: 

  1. 当class内含一个member object,而其class有一个copy assignment operator时。
  2. 当一个class的base class有一个copy assignment operator时。
  3. 当一个class声明了任何virtual function(我们不一定要拷贝右端class object的vptr,因为它可能是一个derived class object)时。
  4. 当class继承自一个virtual base class(不论此base class有没有copy operator)时。

  2.3 虚拟继承的拷贝

  在虚拟继承情况下,copy assignment opertator会遇到一个不可避免的问题, virtual base class subobject的复制行为会发生多次,与前面说到的在虚拟继承 情况下虚基类被构造多次是一个意思,不同的是在这里不能抑制非most-derived class 对virtual base class 的赋值行为。

  事实上,copy assignment operator 在虚拟继承情况下行为不佳,需要小心地设计和说明。因为 copy assignment operator 缺乏一个member assignment list(也就是平行与member initialization list 的东西),因此编译器没有办法压抑上一层的base class 的copy operators被调用,导致在虚拟继承情况下,derived class 将对virtual base class 进行多重拷贝。C++语言说:我们并灭有规定那些代表virtual base class 的subobjects 是否该被“隐喻定义(implicitly defined)的copy assignment operator”指派(赋值,assign)内容一次以上。因此建议尽可能不要允许一个virtual base class的拷贝操作,甚至可能的话,不要在任何virtual base class 中声明数据。

3、vptr语意学

  vptr在constructor何时被初始化?在base class constructors调用操作之后,但是在程序员供应的码或是初始化列表中所列的members初始化操作之前。

  C++ 语言规定:在一个class(base class) 的constructor(和destructor) 中,经由构造中的对象(derived class)来调用一个virtual function,其函数实体应该是在此class(base class) 中有作用的那个。也就是都静态决议,不用到虚拟机制。也就是说,虚拟机制本身必须知道是否这个调用源自一个constructor 之中。而根本的解决之道是,在执行一个constructor 时,必须限制一组virtual functions 候选名单。答案是通过vptr。而vptr 的适当初始化时间是在base class constructors 调用操作之后,但是在程序员供应的码或是“member initialization list 中所列的members初始化操作”之前。因此在class 的constructor 的member initialization list 中调用该class 的一个虚拟函数是安全的,但是在一个class的member initialization list *应参数一个base class constructor 时调用虚拟函数就是不安全的。

4、解构语意学

  4.1 destructor被扩展的方式

    1.destructor的函数本身首先被执行。
    2.如果class拥有member class objects,而后拥有destructor,那么它们会以声明顺序的相反顺序被调用。
    3.如果object内带一个vptr,则现在被重新设定,指向适当的base class virtual table。
    4.如果有任何直接的(上一层)nonvirtual base classes 拥有destructor ,它们会以声明顺序相反顺序调用。
    5.如果有任何virtual base classes 拥有destructor,而当前讨论的这个class 是最尾端的class,那么它们会以其原来顺序相反顺序被调用。

  4.2 总结

    1.纯虚基类尽量不要定义数据成员,如果定义了就需要在构造函数或其他成员函数设定初值,不过这通常是一种不好的设计。
    2.纯虚基类的纯虚函数可以在派生类中以静态方式调用。
    3.声明了纯虚析构函数就一定得定义它,为什么?因为每一个派生类析构函数都会被编译器扩展,以静态调用方式调用其每一个虚基类以及上一层基类的析构函数。因此,只要缺乏任何一个基类析构函数的定义,就会导致链接失败。
    4.如果某个函数其函数定义内容不与类型有关,不会被后继派生类改写,不要定义为虚函数。因为它的非虚函数实体是 inline,所以不使用 virtual 更能提高效率。
    5.虚函数中尽量不要使用 const,因为它可能被很多次调用,可能需要修改数据成员。
    6.对于 POD 类型的类,编译器不会为它生成什么函数,因为那是无关痛痒的。包括 delete 该类型的指针,也不会触发 constructor。(除开你自己定义的情况)。