编译器角度看C++复制构造函数
[C++對象模型]復(fù)制構(gòu)造函數(shù)的建構(gòu)操作
關(guān)于復(fù)制構(gòu)造函數(shù)的簡單介紹,可以看我以前寫過的一篇文章C++復(fù)制控制之復(fù)制構(gòu)造函數(shù)該文章中介紹了復(fù)制構(gòu)造函數(shù)的定義、調(diào)用時(shí)機(jī)、也對編譯器合成的復(fù)制構(gòu)造函數(shù)行為做了簡單說明。本文因需要會(huì)涉及到上文的一些知識(shí)點(diǎn),但還是推薦先閱讀上文。
本文主要從編譯器角度對復(fù)制構(gòu)造函數(shù)進(jìn)行分析,糾正以前對復(fù)制構(gòu)造函數(shù)的一些錯(cuò)誤認(rèn)識(shí)。
淺拷貝(deep copy)與深拷貝(shallow copy)
我們首先來看復(fù)制構(gòu)造函數(shù)涉及的兩個(gè)概念:淺拷貝與深拷貝。假設(shè)有兩個(gè)對象:A與B,它們是同類型的,下面分析B=A時(shí)淺拷貝與深拷貝行為。
淺拷貝:
淺拷貝簡單地把B復(fù)制為A的引用或指針,可以認(rèn)為B復(fù)制了A的地址,復(fù)制的結(jié)果是B與A擁有相同的地址,它們將指向相同的內(nèi)存區(qū)域的相同的數(shù)據(jù)。在這種情況下,如果對象A被銷毀,那么對對象B的某些操作將是非法的。
深拷貝:
深拷貝時(shí)使用一個(gè)對象的內(nèi)容來創(chuàng)建同一個(gè)類的另一個(gè)實(shí)例,B復(fù)制了A的所有成員,并在內(nèi)存中不同于A的區(qū)域?yàn)锽分配了存儲(chǔ)空間,也即是說B擁有自己的資源。在這種方式下,如果A被銷毀時(shí),B依舊有效,因?yàn)锳與B并沒有共享存儲(chǔ)空間,重載復(fù)制操作符時(shí)要采用這種深拷貝方式。
當(dāng)你明確知道你中程序中使用的是淺拷貝并且明白它帶來的后果時(shí)你才去使用淺拷貝。而當(dāng)你有大量的指針要處理時(shí),對指針做淺拷貝是一個(gè)糟糕的做法。如果我們類的數(shù)據(jù)成員都是內(nèi)置類型而沒有指針,那么簡單的淺拷貝是可以接受的,反之如果類中有需要深層復(fù)制的內(nèi)容,則我們的復(fù)制構(gòu)造函數(shù)必須以深拷貝的方式進(jìn)行對象的復(fù)制。
Memberwise copy 與 Bitwise copy
Memberwise copy:
逐個(gè)成員:我們把merberwise copy當(dāng)成deep copy來理解就行了,這種復(fù)制會(huì)根據(jù)每個(gè)成員的類型來進(jìn)行復(fù)制,對于指針類型會(huì)復(fù)制指針?biāo)傅闹?#xff08;重新分配存儲(chǔ)區(qū)域)。
Bitwise copy:
Bitwise copy 字面上的意思是逐位拷貝。舉個(gè)例子,對于兩個(gè)同類型的對象A與B,對象A在內(nèi)存中占據(jù)存儲(chǔ)區(qū)為0x0-0x9,執(zhí)行B=A時(shí),使用Bitwise copy拷貝語義,那么將會(huì)拷貝0x0到0x9的數(shù)據(jù)到B的內(nèi)存地址,也就是說Bitwise是字節(jié)到字節(jié)的拷貝。這樣子理解起來,實(shí)際上Bitwise copy = shallow copy。
類的Bitwise copy 語意
《Effective C++》中說到:
如果你自己沒聲明,編譯器就會(huì)為它聲明一個(gè)copy構(gòu)造函數(shù)、一個(gè)copy assignment操作符和一個(gè)析構(gòu)函數(shù)。
實(shí)際上在《深度探索C++對象模型》中對編譯器的行為并不是這樣描述的。對于默認(rèn)構(gòu)造函數(shù)與復(fù)制構(gòu)造函數(shù),都需要類滿足一定的條件時(shí)編譯器才會(huì)幫你合成。那么需要滿足些什么條件呢?這條件就是:類不展現(xiàn)bitwise copy 語意的時(shí)候。
類展現(xiàn)Bitwise copy語意
當(dāng)我們的類中只含有內(nèi)置類型或復(fù)合類型時(shí),類展現(xiàn)了Bitwise copy 語意。這種情況下并不需要合成一個(gè)默認(rèn)復(fù)制構(gòu)造函數(shù),也即編譯器不會(huì)幫我們合成復(fù)制構(gòu)造函數(shù)。如:
//以下聲明展現(xiàn)了bitwise copy 語意
class Word
{
public:
Word(chartemp){ str = temp; };
//...
int cnt;
char str;
};
這時(shí)候如果我們有兩個(gè)Word對象的賦值操作如下:
int main()
{
char * temp = "hello";
Word A(temp);
Word B(A);
cout << B.str;
system("pause");
return 0;
}
運(yùn)行程序,你會(huì)神奇地發(fā)現(xiàn)程序居然通得過編譯,而且B也得到了正確的賦值,就好像類中有了一個(gè)復(fù)制構(gòu)造函數(shù)一樣。不是說編譯器在Bitwise copy語意下不會(huì)進(jìn)行復(fù)制構(gòu)造函數(shù)的合成嗎?
說實(shí)話這問題我也很疑惑,查看了許多資料,反復(fù)看了《深度探索C++對象模型》后,我最終這樣認(rèn)為:展現(xiàn)了Bitwise copy語意的類編譯器不會(huì)為它寫一個(gè)函數(shù)實(shí)體進(jìn)行成員的復(fù)制。展現(xiàn)Bitwise copy語意的類,類的數(shù)據(jù)成員按照Memberwise Initialization(注意不同于Memberwise copy)進(jìn)行初始化,具體是這樣的:當(dāng)類對象以同類型的另一個(gè)對象進(jìn)行初始化時(shí),把每一個(gè)內(nèi)建的或派生的date member(例如一個(gè)指針或一數(shù)目組)的值,從一個(gè)對象拷貝到另一個(gè)對象,不過它并不會(huì)拷貝其中的member class object,而是以遞歸的方式施行以上的拷貝。實(shí)施這些步驟并不在函數(shù)實(shí)體內(nèi)。
類不展現(xiàn)Bitwise copy語意
當(dāng)類不展現(xiàn)出Bitwise copy語意且類設(shè)計(jì)者沒有為類定義一個(gè)復(fù)制構(gòu)造函數(shù),這時(shí)編譯器就會(huì)為合成一個(gè)復(fù)制構(gòu)造函數(shù)實(shí)體。那么在什么情況下一個(gè)類才會(huì)不展現(xiàn)出Bitwise copy 語意呢?
前兩種情況中,編譯器必須將“類成員或基類的復(fù)制構(gòu)造函數(shù)調(diào)用操作”安插到新合成的復(fù)制構(gòu)造函數(shù)中去,如果類設(shè)計(jì)者已經(jīng)明確聲明了一個(gè)復(fù)制構(gòu)造函數(shù),則這些調(diào)用操作代碼將插入到已有的復(fù)制構(gòu)造函數(shù)中去(在函數(shù)體的最前端插入)。
后兩種操作涉及到了虛表指針與虛基類指針的產(chǎn)生于初值設(shè)置。我們知道,當(dāng)一個(gè)類含有虛函數(shù)時(shí)(無論這虛函數(shù)是類本身定義還是繼承而來),在編譯期間會(huì)有以下兩個(gè)程序擴(kuò)張操作:
- 為類增加一個(gè)虛表(virtual function table),虛表內(nèi)含有每一個(gè)有作用的虛函數(shù)的地址。
- 為每一個(gè)類對象增加一個(gè)虛表指針(vptr),虛表指針指向了該類的虛表。
顯然,如果編譯器對每個(gè)新定義的類對象不能正確地設(shè)置好初值,將導(dǎo)致嚴(yán)重的后果。所以編譯器需要合成出一個(gè)復(fù)制構(gòu)造函數(shù)來適當(dāng)?shù)爻跏蓟悓ο蟮膙ptr。萬一類設(shè)計(jì)者明確定義了自己的復(fù)制構(gòu)造函數(shù),則編譯器會(huì)把設(shè)置vptr的操作插入到已有的復(fù)制構(gòu)造函數(shù)中。而vptr的復(fù)制又有兩種情況:
- 同類型對象間的vptr復(fù)制
同類類型的對象各自的vptr總是指向了同一個(gè)位置:該類的虛表指針。這時(shí)兩個(gè)對象的vptr的復(fù)制都可以直接考”bitwise copy“來完成(除了可能會(huì)有的其他指針成員)。所以同類型對象間的vptr復(fù)制總是安全的。
-把子類對象vptr復(fù)制給父類對象
不用擔(dān)心把子類對象復(fù)制給父類對象時(shí),vptr也會(huì)采用bitwise copy來復(fù)制,這點(diǎn)編譯器給我們做了保證:編譯器合成的默認(rèn)構(gòu)造函數(shù)(或者說在明確聲明的復(fù)制構(gòu)造函數(shù)中安插的代碼)會(huì)明確設(shè)定父類的vptr指向父類的虛函數(shù)表,而不是采用傻瓜式直接復(fù)制子類對象vptr。
而對于第4點(diǎn)涉及到虛基類的情況,可以看C++合成默認(rèn)構(gòu)造函數(shù)的真相中有關(guān)虛基類的描述。虛基類的存在需要特殊處理,一個(gè)類對象如果以另一個(gè)對象作為初值,而后者派生于虛基類,那么這種情況下bitwise copy語意也會(huì)失效,編譯器會(huì)對派生自虛基類的類合成一個(gè)默認(rèn)構(gòu)造函數(shù),在其中安插一些操作。對于虛繼承,編譯器有承偌:派生類對象中的虛基類位置在執(zhí)行期就要準(zhǔn)備妥當(dāng),維護(hù)”位置的完整性“是編譯器的責(zé)任,而顯然的,Bitwise copy 語意會(huì)破壞這個(gè)位置(這種傻瓜式的復(fù)制好像只適用內(nèi)置類型的復(fù)制以及同類型對象間vptr的復(fù)制),所以編譯器必須在它自己合成出來的復(fù)制構(gòu)造函數(shù)中做出仲裁。同樣的,如果類設(shè)計(jì)者明確聲明了復(fù)制構(gòu)造函數(shù),則這些沖裁代碼將安插在這個(gè)復(fù)制構(gòu)造函數(shù)中。
總結(jié)
在類不滿足"Bitwise copy"語意時(shí)編譯器會(huì)采取行動(dòng),如果類設(shè)計(jì)者沒有明確定義復(fù)制構(gòu)造函數(shù),則編譯器將行動(dòng)實(shí)施于合成構(gòu)造函數(shù)中,否則將這些行動(dòng)實(shí)施于已有的復(fù)制構(gòu)造函數(shù)中。值得注意的是,編譯器除了對vptr與虛基類的處理能保證安全之外,對于內(nèi)置類型或復(fù)合類型如指針的復(fù)制都是采用淺拷貝,所以,當(dāng)我們的類中含有指針的時(shí)候,我們需要自己寫一個(gè)復(fù)制構(gòu)造函數(shù)來對對象的指針進(jìn)行深拷貝,而vptr與虛基類的問題,就交給編譯器吧!
轉(zhuǎn)載于:https://www.cnblogs.com/QG-whz/p/4716139.html
總結(jié)
以上是生活随笔為你收集整理的编译器角度看C++复制构造函数的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL性能调优与架构设计——第5章
- 下一篇: project template