构造函数语义学——Copy Constructor 篇

跟風遠走 提交于 2019-12-01 19:02:00

构造函数语义学——Copy Constructor 篇

本文主要介绍《深度探索 C++对象模型》之《构造函数语义学》中的 Copy Constructor

构造函数的调用时机

首先需要明确,构造函数何时会被调用呢?cppreference 中已经有了足够详细地说明:

凡在对象从同类型的另一对象(以直接初始化或复制初始化)初始化时,调用复制构造函数(除非重载决议选择了更好的匹配或其调用被消除),情况包括:
初始化:T a = b; 或 T a(b);,其中 b 类型为 T;
函数实参传递:f(a);,其中 a 类型为 T 而 f 为 Ret f(T t);
函数返回:在如 T f() 这样的函数内部的 return a;,其中 a 类型为 T,它没有移动构造函数。

编译器合成 copy constructor 的条件

在之前《构造函数语义学——Default Constructor 篇》一文中,我们分析了编译器产生 default constructor 的条件,以及编译器所产生的 default constructor 的类型(trivial & non-trivial);对于构造函数来说,其原理也是大致类似的,只是具体的细节条件不同,此文中就不再给出具体的证明,读过前一篇博文的读者也应该能够自己分析,此文只给出具体的条件

编译器隐式声明&定义 copy constructor 的条件

隐式声明的复制构造函数
若不对类类型(struct、class 或 union)提供任何用户定义的复制构造函数,则编译器始终会声明一个复制构造函数,作为其类的非 explicit 的 inline public 成员。

与 default constructor 类似,只要没有任何 user_declared 的 copy constructor,那么编译器就会为我们自动声明一个 copy constructor(这一点与《深度探索 C++对象模型》中所述不同)

隐式定义的复制构造函数
若隐式声明的复制构造函数未被弃置,则当其被 ODR 式使用时,它为编译器所定义(即生成并编译函数体)。对于 union 类型,隐式定义的复制构造函数(如同以 std::memmove)复制其对象表示。对于非联合类类型(class 与 struct),该构造函数用直接初始化,按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制。

trivial copy constructor 的条件

编译器自动合成的 copy constructor 也是分为 trivial 和 non-trivial 的

对于 trivial copy constructor 的条件,cppreference 中也给出了详细的说明:

当下列各项全部为真时,类 T 的复制构造函数为平凡的:
它不是用户提供的(即它是隐式定义或预置的),且若它被预置,则其签名与隐式定义的相同;
T 没有虚成员函数;
T 没有虚基类;
为 T 的每个直接基类选择的复制构造函数都是平凡的;
为 T 的每个类类型(或类类型数组)的非静态成员选择的复制构造函数都是平凡的;

而在《深度探索 C++对象模型》中有一句话“决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的bitwise copy semantics”;即如果一个 class 展现出了 bitwise copy semantics,那么编译器为其合成的 copy constructor 就是 trivial 的

换言之,如果不满足 bitwise copy semantics,那么编译器合成的 copy constructor 就是 non-trivial 的。何时一个 class 不表现出 bitwise copy semantics 呢?书中给了四个条件(略有修改):

  1. 当 class 内含一个 member object,而后者的 class 中的有一个 copy constructor(而后者 class 的 copy constructor 必须是 non-trivial 的)
  2. 当 class 继承自一个 base class,而这个 base class 存在一个 copy constructor(该 base class 的 copy constructor 必须是 non-trivial 的)
  3. class 声明了 virtual function
  4. class 派生自一个继承链,而该继承链中存在一个或多个 virtual base class

其实这个四个条件相当于 cppreference 中提到的成为 trivial copy constructor 的相反条件

编译器合成的 copy constructor 的行为

trivial copy constructor 的行为

关于 trivial copy constructor 的行为,cppreference 也有提到:

非联合类的平凡复制构造函数,效果为复制实参的每个标量子对象(递归地包含子对象的子对象,以此类推),且不进行其他动作。不过不需要复制填充字节,甚至只要其值相同,每个复制的子对象的对象表示也不必相同。

这句话的意思是说,如果编译器合成的出来 copy constructor 是 trivial 的,它展现出这种行为:逐个字节的拷贝所有内容

举个例子:

class A {
  private:
    int _a;
};

int main() {
  A a;
  A aa = a;
  return 0;
}

其中 A aa = a;这一句,会调用编译器产生的 trivial copy constructor,该 trivial copy constructor 会一个字节一个字节的把 a 中的成员变量的值拷贝到 aa 对应的成员变量中去

这似乎看起来挺好的呀,也正是我们所需要的结果,但是,如果 class A 中的成员变量是一根指针,那么问题就大了:

#include <iostream>
using namespace std;

class A {
  public:
  int *p;
};


int main() {
  A a;
  int val = 1;
  a.p = &val;
  A aa = a;
  cout << a.p << endl;
  cout << aa.p << endl;
  *(aa.p) = 2;
  cout << *(a.p) << endl;
  cout << *(aa.p) << endl;
}

// 上述程序的输出为
0x7ffc5d760414
0x7ffc5d760414
2
2

也就是说,在编译器自动为我们合成的 trivial copy constructor 的行为中,复制了 a 的指针给了 aa(浅拷贝),也就是说 a 和 aa 中的指针 p 指向了相同的地址!!!

在这种含有指针的情况下,编译器产生的 trivial copy constructor 的行为便不是我们所希望的,我们必须手动显示的定义一个符合我们需求的 copy constructor 来完成对指针的拷贝

non-trivial copy constructor 的行为

cppreference 中已经说了:

对于非联合类类型(class 与 struct),该构造函数用直接初始化,按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制。

non-trivial copy constructor 一个很重要的行为是:确保 vptr 的准确设定。(因为只要包含虚机制,那么编译器自动合成的 copy constructor 就不可能是 trivial 的)

上面一点,书中已经说的足够清楚,此文不再赘述

总结

  1. copy constructor 在特定条件下,编译器也会为我们自动合成
  2. 编译器合成的 copy constructor 也是分为 trivial 和 non-trivial 的
  3. 要时刻牢记 trivial copy constructor 的条件与行为
  4. 当成员变量涉及指针时,最好的做法就是显式提供自定义的 copy constructor 来满足需求
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!