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

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

构造函数的调用时机

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

编译器合成 copy constructor 的条件

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

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

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

trivial copy constructor 的条件

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

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

而在《深度探索 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 中已经说了:

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

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

总结

  1. copy constructor 在特定条件下,编译器也会为我们自动合成
  2. 编译器合成的 copy constructor 也是分为 trivial 和 non-trivial 的
  3. 要时刻牢记 trivial copy constructor 的条件与行为
  4. 当成员变量涉及指针时,最好的做法就是显式提供自定义的 copy constructor 来满足需求
01-23 20:06