編譯器角度看C++複製構造函數

[C++對象模型]複製構造函數的建構操做

關於複製構造函數的簡單介紹,能夠看我之前寫過的一篇文章C++複製控制之複製構造函數該文章中介紹了複製構造函數的定義、調用時機、也對編譯器合成的複製構造函數行爲作了簡單說明。本文因須要會涉及到上文的一些知識點,但仍是推薦先閱讀上文。html

本文主要從編譯器角度對複製構造函數進行分析,糾正之前對複製構造函數的一些錯誤認識。前端

淺拷貝(deep copy)與深拷貝(shallow copy)

咱們首先來看複製構造函數涉及的兩個概念:淺拷貝與深拷貝。假設有兩個對象:A與B,它們是同類型的,下面分析B=A時淺拷貝與深拷貝行爲。git

淺拷貝:

淺拷貝簡單地把B複製爲A的引用或指針,能夠認爲B複製了A的地址,複製的結果是B與A擁有相同的地址,它們將指向相同的內存區域的相同的數據。在這種狀況下,若是對象A被銷燬,那麼對對象B的某些操做將是非法的。github

深拷貝:

深拷貝時使用一個對象的內容來建立同一個類的另外一個實例,B複製了A的全部成員,並在內存中不一樣於A的區域爲B分配了存儲空間,也便是說B擁有本身的資源。在這種方式下,若是A被銷燬時,B依舊有效,由於A與B並無共享存儲空間,重載複製操做符時要採用這種深拷貝方式。安全

當你明確知道你中程序中使用的是淺拷貝而且明白它帶來的後果時你纔去使用淺拷貝。而當你有大量的指針要處理時,對指針作淺拷貝是一個糟糕的作法。若是咱們類的數據成員都是內置類型而沒有指針,那麼簡單的淺拷貝是能夠接受的,反之若是類中有須要深層複製的內容,則咱們的複製構造函數必須以深拷貝的方式進行對象的複製。函數

Memberwise copy 與 Bitwise copy

Memberwise copy:

逐個成員:咱們把merberwise copy當成deep copy來理解就好了,這種複製會根據每一個成員的類型來進行復制,對於指針類型會複製指針所指的值(從新分配存儲區域)。設計

Bitwise copy:

Bitwise copy 字面上的意思是逐位拷貝。舉個例子,對於兩個同類型的對象A與B,對象A在內存中佔據存儲區爲0x0-0x9,執行B=A時,使用Bitwise copy拷貝語義,那麼將會拷貝0x0到0x9的數據到B的內存地址,也就是說Bitwise是字節到字節的拷貝。這樣子理解起來,實際上Bitwise copy = shallow copy。指針

類的Bitwise copy 語意

《Effective C++》中說到:code

若是你本身沒聲明,編譯器就會爲它聲明一個copy構造函數、一個copy assignment操做符和一個析構函數。htm

實際上在《深度探索C++對象模型》中對編譯器的行爲並非這樣描述的。對於默認構造函數與複製構造函數,都須要類知足必定的條件時編譯器纔會幫你合成。那麼須要知足些什麼條件呢?這條件就是:類不展示bitwise copy 語意的時候。

類展示Bitwise copy語意

當咱們的類中只含有內置類型或複合類型時,類展示了Bitwise copy 語意。這種狀況下並不須要合成一個默認複製構造函數,也即編譯器不會幫咱們合成複製構造函數。如:


//如下聲明展示了bitwise copy 語意
class Word
{
public:
Word(chartemp){ str = temp; };
//...
int cnt;
char
str;
};

這時候若是咱們有兩個Word對象的賦值操做以下:

int main()
{
char * temp = "hello";
Word A(temp);
Word B(A);
cout << B.str;
system("pause");
return 0;
}

運行程序,你會神奇地發現程序竟然通得過編譯,並且B也獲得了正確的賦值,就好像類中有了一個複製構造函數同樣。不是說編譯器在Bitwise copy語意下不會進行復制構造函數的合成嗎?

說實話這問題我也很疑惑,查看了許多資料,反覆看了《深度探索C++對象模型》後,我最終這樣認爲:展示了Bitwise copy語意的類編譯器不會爲它寫一個函數實體進行成員的複製。展示Bitwise copy語意的類,類的數據成員按照Memberwise Initialization(注意不一樣於Memberwise copy)進行初始化,具體是這樣的:當類對象以同類型的另外一個對象進行初始化時,把每個內建的或派生的date member(例如一個指針或一數目組)的值,從一個對象拷貝到另外一個對象,不過它並不會拷貝其中的member class object,而是以遞歸的方式施行以上的拷貝。實施這些步驟並不在函數實體內。

類不展示Bitwise copy語意

當類不展示出Bitwise copy語意且類設計者沒有爲類定義一個複製構造函數,這時編譯器就會爲合成一個複製構造函數實體。那麼在什麼狀況下一個類纔會不展示出Bitwise copy 語意呢?

  1. 當類內含有一個member object 然後者的類聲明中有個複製構造函數時(不管這個構造函數是設計者明確地聲明仍是編譯器合成)。
  2. 當類繼承於一個基類然後者有已給複製構造函數時(一樣的,不管基類的構造函數是設計者明確聲明的仍是合成的)。
  3. 當類聲明瞭一個或多個虛函數時。
  4. 當類派生自一個繼承串鏈,其中有一個或多個虛基類時。

前兩種狀況中,編譯器必須將「類成員或基類的複製構造函數調用操做」安插到新合成的複製構造函數中去,若是類設計者已經明確聲明瞭一個複製構造函數,則這些調用操做代碼將插入到已有的複製構造函數中去(在函數體的最前端插入)。

後兩種操做涉及到了虛表指針與虛基類指針的產生於初值設置。咱們知道,當一個類含有虛函數時(不管這虛函數是類自己定義仍是繼承而來),在編譯期間會有如下兩個程序擴張操做:

  • 爲類增長一個虛表(virtual function table),虛表內含有每個有做用的虛函數的地址。
  • 爲每個類對象增長一個虛表指針(vptr),虛表指針指向了該類的虛表。

顯然,若是編譯器對每一個新定義的類對象不能正確地設置好初值,將致使嚴重的後果。因此編譯器須要合成出一個複製構造函數來適當地初始化類對象的vptr。萬一類設計者明肯定義了本身的複製構造函數,則編譯器會把設置vptr的操做插入到已有的複製構造函數中。而vptr的複製又有兩種狀況:

  • 同類型對象間的vptr複製

同類類型的對象各自的vptr老是指向了同一個位置:該類的虛表指針。這時兩個對象的vptr的複製均可以直接考」bitwise copy「來完成(除了可能會有的其餘指針成員)。因此同類型對象間的vptr複製老是安全的。

-把子類對象vptr複製給父類對象

不用擔憂把子類對象複製給父類對象時,vptr也會採用bitwise copy來複制,這點編譯器給咱們作了保證:編譯器合成的默認構造函數(或者說在明確聲明的複製構造函數中安插的代碼)會明確設定父類的vptr指向父類的虛函數表,而不是採用傻瓜式直接複製子類對象vptr。

而對於第4點涉及到虛基類的狀況,能夠看C++合成默認構造函數的真相中有關虛基類的描述。虛基類的存在須要特殊處理,一個類對象若是以另外一個對象做爲初值,然後者派生於虛基類,那麼這種狀況下bitwise copy語意也會失效,編譯器會對派生自虛基類的類合成一個默認構造函數,在其中安插一些操做。對於虛繼承,編譯器有承偌:派生類對象中的虛基類位置在執行期就要準備穩當,維護」位置的完整性「是編譯器的責任,而顯然的,Bitwise copy 語意會破壞這個位置(這種傻瓜式的複製好像只適用內置類型的複製以及同類型對象間vptr的複製),因此編譯器必須在它本身合成出來的複製構造函數中作出仲裁。一樣的,若是類設計者明確聲明瞭複製構造函數,則這些衝裁代碼將安插在這個複製構造函數中。

總結

在類不知足"Bitwise copy"語意時編譯器會採起行動,若是類設計者沒有明肯定義複製構造函數,則編譯器將行動實施於合成構造函數中,不然將這些行動實施於已有的複製構造函數中。值得注意的是,編譯器除了對vptr與虛基類的處理能保證安全以外,對於內置類型或複合類型如指針的複製都是採用淺拷貝,因此,當咱們的類中含有指針的時候,咱們須要本身寫一個複製構造函數來對對象的指針進行深拷貝,而vptr與虛基類的問題,就交給編譯器吧!

相關文章
相關標籤/搜索