0%

【C++对象模型】(五)对象复制和析构

《深度探索C++对象模型》第五章重点梳理。主要内容包括:

  • 关于对象复制操作(Copy Assignment Operator)
  • 关于对象析构(Destruction)
  • 几点类的设计原则

1 关于 Copy Assignment Operator

Copy Assignment Operator 是指类中对 operator= 的重载,用来将一个对象指定给另一个对象。如果我们希望不允许将该类的对象指定给另一个对象,只需要将 operator= 设定为 private,并且不提供函数体即可。

当然大部分情况下,类的设计者可以选择不显式提供 copy assignment operator,使用默认的逐成员复制(类似于拷贝构造函数),因为这样既方便,效率又高。copy assignment operator 和拷贝构造函数一样,只在有用(nontrivial)的时候会真正被编译器产生或合成出来,而其他情况下由于保持有 bitwise copy semantics,不需要以函数调用的形式进行复制,也就无须合成。在四种情况下 copy assignment operator 会被视为 nontrivial 而被合成出来:

  • 当 class 中包含一个对象成员而后者的类声明有一个 copy assignment operator 时,因为需要调用对象成员的 copy assignment operator
  • 当 class 继承自一个 base class 而后者存在有一个 copy assignment operator 时,因为需要调用基类的 copy assignment operator
  • 当 class 声明了一个或多个 virtual functions 时,因为我们一定不能直接拷贝等号右边对象的 vptr,因为等号左边可能是基类对象,而右边是派生类对象
  • 当 class 派生自一个继承串链,其中有一个或多个 virtual base classes 时(不论虚基类有没有 copy assignment operator)

2 关于析构(Destruction)

如果 class 没有显式定义析构函数,那么只有在 class 内包含的 member object(或是 class 自己的 base class)拥有析构函数时,编译器才会自动合成一个析构函数,否则,析构函数会被视为不需要,也就不需被合成(当然更不需要被调用)。析构函数的扩展和执行顺序类似于之前构造函数的执行顺序,并且与构造函数的顺序完全相反:

  1. 构造函数本身首先被执行
  2. 如果有对象成员,且其有析构函数,按照它们声明顺序的相反顺序调用它们的析构函数
  3. 如果有 vptr,则重设 vptr,指向适当的基类的虚函数表
  4. 如果有任何直接的(上一层) nonvirtual base classes 拥有 destructor,它们会以其声明顺序的相反顺序被调用
  5. 如果有任何 virtual base classes 拥有 destructor,而当前讨论的这个类是继承链最尾端 (most-derived) 的类,那么它们会以其原来的构造顺序的相反顺序被调用

对于如下类继承关系:

image-20220509161840825

那么一个 PVertex 对象的构造过程是:首先构造一个 Point 对象,然后变成一个 Vertex 和一个 Point3d 对象,然后变成一个 Vertex3d 对象,最后变成一个 PVertex 对象;而一个 PVertex 对象的析构过程正相反:依次变成一个 Vertex3d 对象、一个 Vertex 对象、 一个 Point3d 对象,最后成为一个 Point 对象。

所以对象的构造是一个逐步进化的过程,从构建一个最基础的对象开始,一步步构建成一个目标对象,而对象的析构则是相反的逐步退化的过程。

3 几点类的设计原则

  • 即使是一个抽象基类,如果它有非静态数据成员,也应该给它提供一个带参数的构造函数,来初始化它的数据成员。或许你可以通过其派生类来初始化它的数据成员(假如 non-static data member 为 publish 或 protected),但这样做的后果则是破坏了数据的封装性,使类的维护和修改更加困难。由此引申,类的 data member 应当被初始化,且只在其构造函数或其 member function 中初始化。

  • 只在有必要的时候才使用虚函数,不要滥用虚函数。虚函数意味着不小的成本,编译器很可能给你的类带来一连串的膨胀效应:

    • 每一个对象要多负担一个 vptr
    • 给每一个构造函数(不论是显式的还是编译器合成的),插入一些代码来初始化 vptr,这些代码必须被放在所有基类构造函数的调用之后,但需在任意用户代码之前
    • 生成或合成一个拷贝构造函数和一个复制操作符(如果没有的话),并插入对 vptr 的初始化代码
    • 如果类原本具有 bitwise copy 语意,将失去该语义,然后是上面所述,更大的对象、没有那么高效的构造函数、没有那么高效的复制操作
  • 不能决定一个虚函数是否需要 const ,那么就不要 const。

  • 不要将析构函数设计为纯虚的,这不是一个好的设计。将析构函数设计为纯虚函数意味着,即使纯虚函数在语法上允许我们只声明而不定义,但还是必须实现该纯虚析构函数,否则所有的派生类都将遇到链接错误。这是因为,每一个派生类的析构函数会被编译器加以扩展,以静态调用方式其每一个基类的析构函数(假如有的话,不论是显式的还是编译器合成的),所以只要任何一个基类的析构函数缺乏定义,就会导致链接失败。矛盾就在这里,纯虚函数的语法,允许只声明而不定义,所以当编译器看到一个其基类的析构函数声明,就去调用它的实体,而不管它有没有被定义。

  • 决不在构造函数或析构函数中使用虚函数机制。在构造函数中,每次调用虚函数会被决议为当前构造函数所对应类的虚函数实体,虚函数机制并不起作用。当一个基类的构造函数含有对虚函数的调用,当其派生类的构造函数调用基类的构造函数时,其中调用的虚函数是基类中的实体,而不是派生类中的实体。这是由 vptr 初始化的位置决定的——在所有基类构造函数调用之后,在程序员供应的代码或是成员初始化队列之前。因为构造函数的调用顺序是:有根源到末端,由内而外,所以对象的构造过程可以看成是,从构建一个最基础的对象开始,一步步构建成一个目标对象,析构函数则有着与构造相反的顺序,因此在构造或析构函数中使用虚函数机制,往往不是程序员的意图。若要在构造函数或析构函数中调用虚函数,应当直接以静态方式调用,而不要通过虚函数机制。

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。