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

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

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

构造函数的调用时机

首先须要明确,构造函数什么时候会被调用呢?cppreference 中已经有了足够详细地说明:c++

凡在对象从同类型的另外一对象(以直接初始化或复制初始化)初始化时,调用复制构造函数(除非重载决议选择了更好的匹配或其调用被消除),状况包括:
初始化: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 成员。spa

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

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

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 来知足需求
相关文章
相关标签/搜索