構造函數語義學——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 來知足需求
相關文章
相關標籤/搜索