編譯器角度看C 複製建構函式

2022-01-21 21:41:29 字數 3372 閱讀 4241

關於複製建構函式的簡單介紹,可以看我以前寫過的一篇文章c++複製控制之複製建構函式該文章中介紹了複製建構函式的定義、呼叫時機、也對編譯器合成的複製建構函式行為做了簡單說明。本文因需要會涉及到上文的一些知識點,但還是推薦先閱讀上文。

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

我們首先來看複製建構函式涉及的兩個概念:淺拷貝與深拷貝。假設有兩個物件:a與b,它們是同型別的,下面分析b=a時淺拷貝與深拷貝行為。

淺拷貝簡單地把b複製為a的引用或指標,可以認為b複製了a的位址,複製的結果是b與a擁有相同的位址,它們將指向相同的記憶體區域的相同的資料。在這種情況下,如果物件a被銷毀,那麼對物件b的某些操作將是非法的。

深拷貝時使用乙個物件的內容來建立同乙個類的另乙個例項,b複製了a的所有成員,並在記憶體中不同於a的區域為b分配了儲存空間,也即是說b擁有自己的資源。在這種方式下,如果a被銷毀時,b依舊有效,因為a與b並沒有共享儲存空間,過載複製操作符時要採用這種深拷貝方式。

當你明確知道你中程式中使用的是淺拷貝並且明白它帶來的後果時你才去使用淺拷貝。而當你有大量的指標要處理時,對指標做淺拷貝是乙個糟糕的做法。如果我們類的資料成員都是內建型別而沒有指標,那麼簡單的淺拷貝是可以接受的,反之如果類中有需要深層複製的內容,則我們的複製建構函式必須以深拷貝的方式進行物件的複製。

逐個成員:我們把merberwise copy當成deep copy來理解就行了,這種複製會根據每個成員的型別來進行複製,對於指標型別會複製指標所指的值(重新分配儲存區域)。

bitwise copy 字面上的意思是逐位拷貝。舉個例子,對於兩個同型別的物件a與b,物件a在記憶體中佔據儲存區為0x0-0x9,執行b=a時,使用bitwise copy拷貝語義,那麼將會拷貝0x0到0x9的資料到b的記憶體位址,也就是說bitwise是位元組到位元組的拷貝。這樣子理解起來,實際上bitwise copy = shallow copy。

《effective c++》中說到:

如果你自己沒宣告,編譯器就會為它宣告乙個copy建構函式、乙個copy assignment操作符和乙個析構函式。

實際上在《深度探索c++物件模型》中對編譯器的行為並不是這樣描述的。對於預設建構函式與複製建構函式,都需要類滿足一定的條件時編譯器才會幫你合成。那麼需要滿足些什麼條件呢?這條件就是:類不展現bitwise copy 語意的時候。

當我們的類中只含有內建型別或復合型別時,類展現了bitwise copy 語意。這種情況下並不需要合成乙個預設複製建構函式,也即編譯器不會幫我們合成複製建構函式。如:

//以下宣告展現了bitwise copy 語意

class word

; //...

int cnt;

char *str;

};

這時候如果我們有兩個word物件的賦值操作如下:

int main()

執行程式,你會神奇地發現程式居然通得過編譯,而且b也得到了正確的賦值,就好像類中有了乙個複製建構函式一樣。不是說編譯器在bitwise copy語意下不會進行複製建構函式的合成嗎?

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

當類不展現出bitwise copy語意且類設計者沒有為類定義乙個複製建構函式,這時編譯器就會為合成乙個複製建構函式實體。那麼在什麼情況下乙個類才會不展現出bitwise copy 語意呢?

當類內含有乙個member object 而後者的類宣告中有個複製建構函式時(無論這個建構函式是設計者明確地宣告還是編譯器合成)。

當類繼承於乙個基類而後者有已給複製建構函式時(同樣的,無論基類的建構函式是設計者明確宣告的還是合成的)。

當類宣告了乙個或多個虛函式時。

當類派生自乙個繼承串鏈,其中有乙個或多個虛基類時。

前兩種情況中,編譯器必須將「類成員或基類的複製構造函式呼叫操作」安插到新合成的複製建構函式中去,如果類設計者已經明確宣告了乙個複製建構函式,則這些呼叫操作**將插入到已有的複製建構函式中去(在函式體的最前端插入)。

後兩種操作涉及到了虛表指標與虛基類指標的產生於初值設定。我們知道,當乙個類含有虛函式時(無論這虛函式是類本身定義還是繼承而來),在編譯期間會有以下兩個程式擴張操作:

顯然,如果編譯器對每個新定義的類物件不能正確地設定好初值,將導致嚴重的後果。所以編譯器需要合成出乙個複製建構函式來適當地初始化類物件的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與虛基類的問題,就交給編譯器吧!

編譯器合成複製建構函式

定義 只有單個形參,該形參是對本類型別物件的引用 常用const修飾 這樣的建構函式成為複製建構函式。使用方式 1 顯示使用 用乙個同型別的物件初始化該物件時 2 隱式使用 將該型別的物件傳遞給函式或從函式返回該型別物件時。三種型別的複製建構函式 bitwise copy constructor 逐...

從編譯器角度看 lambda表示式

lambda轉換為函式物件。現在,android已經全面轉向c 11 14標準了,看 的話,很多地方變化很大,新標準真的是有點顛覆性的,感覺已經不會c 了。今天有看到lambda表示式,突然想看一下,這貨是怎麼實現的,如下,寫了個例子,分別呼叫3個lambda表示式 include include ...

C 編譯器合成預設建構函式和複製控制成員的條件

參考自 深入理解c 物件模型 c 新手一般有兩個常見的誤解 任何class如果沒有定義default constructor,就會被合成乙個出來.編譯器合成出來的default constructor會明確設定class 內每乙個data member的預設值.現在主要解釋第一條為什麼是錯誤的,根據...