首先祝各位簡友雙節(jié)快樂酪呻!
找工作的同學(xué)逢考必過offer成堆~
已經(jīng)工作的童鞋工作順利獎金翻倍~
研究僧同學(xué)實(shí)驗(yàn)順利成果多多~O(∩_∩)O
上一篇中重溫了c++的兩個(gè)特殊的成員——默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù)哀峻,發(fā)現(xiàn)即使是這樣一個(gè)簡單的知識也有很多細(xì)節(jié)是我沒有把握到的鸯绿。關(guān)于上一篇文章月洛,有興趣的同學(xué)可以點(diǎn)擊傳送門日麸,大家一起交流交流唄~
本篇將繼續(xù)重溫c++類的特殊成員杭煎。
關(guān)鍵詞:拷貝、淺(深)拷貝泼疑、賦值...
回顧
復(fù)習(xí)一下6個(gè)特殊成員分別是什么
本篇我們將關(guān)注點(diǎn)放到拷貝構(gòu)造函數(shù)以及拷貝賦值初始化上德绿。
這兩個(gè)函數(shù)一旦我們沒有聲明,在被調(diào)用時(shí)編譯器會自動幫我們生成一個(gè)默認(rèn)實(shí)現(xiàn)的版本(同樣的函數(shù)還包括默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù))退渗,這些函數(shù)都是public和inline的
拷貝構(gòu)造函數(shù)(Copy constructor)
將對象一個(gè)實(shí)例作為參數(shù)傳給構(gòu)造函數(shù)時(shí),拷貝構(gòu)造函數(shù)就會被調(diào)用來初始化一個(gè)新的對象蕴纳』嵊停拷貝構(gòu)造函數(shù)的定義就是這樣。
以下是來自官網(wǎng)的定義:
When an object is passed a named object of its own type as argument, its?copy constructor?is invoked in order to construct a copy.
A?copy constructor?is a constructor whose first parameter is of type?reference to the class?itself (possibly const qualified) and which can be invoked with a single argument of this type.
相信大多數(shù)編程人員還是看到代碼更為親切吧古毛。OK翻翩,我們來看看代碼
上圖中main函數(shù)中包含了拷貝構(gòu)造函數(shù)的使用方式。我們這里沒有給出拷貝構(gòu)造函數(shù)的定義稻薇,因此它使用的應(yīng)該就是默認(rèn)的實(shí)現(xiàn)嫂冻。事實(shí)上對于變量b,我們還有另一種聲明方式塞椎,見下圖桨仿。
上圖的上半部分,我們發(fā)現(xiàn)很多STL中的容器也都支持了這種構(gòu)造函數(shù)案狠。這里就不重點(diǎn)關(guān)注STL了服傍,以后想專門研究下STL的源碼~
既然叫拷貝構(gòu)造函數(shù),那么問題來了骂铁,拷貝的到底是什么東西呢吹零?我們運(yùn)行這份代碼來尋找答案
我們發(fā)現(xiàn),copy constructor的默認(rèn)行為僅僅是復(fù)制了成員變量的值而已拉庵!也就是說對于我們這個(gè)MyClass類(唯一的成員變量是個(gè)指針)來說灿椅,copy constructor拷貝的僅僅是指針的地址,對于指針指向的地址包含的內(nèi)容,copy constructor并不關(guān)心茫蛹。這也就是我們常說的淺拷貝泣懊。淺拷貝可以解決大部分的問題,但是一旦遇到我們上面的成員變量是指針的情況麻惶,那么我們的copy constructor初始化便會出現(xiàn)問題馍刮,因?yàn)闇\拷貝會使一個(gè)類的兩個(gè)不同實(shí)例各自的成員變量成為命運(yùn)共同體——對于我們上面的這個(gè)例子,一旦a的resource發(fā)生變化窃蹋,b的也會相應(yīng)地發(fā)生變化卡啰。以下是來自官網(wǎng)的解釋。
An implicit?copy constructor?is automatically defined. The definition assumed for this function performs a?shallow copy
解決這個(gè)問題也很簡單警没,只要我們自己定義一個(gè)copy constructor即可匈辱。
這里注意到我們?yōu)榱诉M(jìn)行深拷貝,特意寫了一個(gè)getResource函數(shù)杀迹,因?yàn)榉駝t我們無法獲得被拷貝對象的指針指向的內(nèi)容亡脸。這個(gè)做法也是參考了cpp的官網(wǎng),【這里插一句getResource方法的兩個(gè)const分別是什么含義呢树酪?直接揭曉答案:前者代表返回的是一個(gè)const string&浅碾,后者代表的是這是個(gè)const 成員函數(shù)⌒铮】那么有沒有辦法不寫這個(gè)getResource方法呢垂谢?筆者也不知道,有知道的讀者不吝賜教哦~那么還是看看實(shí)驗(yàn)的結(jié)果
我們發(fā)現(xiàn)我們重寫的copy constructor有效果滥朱,實(shí)現(xiàn)了深拷貝的效果。
那么STL里面的拷貝賦值函數(shù)是不是使用的默認(rèn)的方法呢力试?細(xì)思極恐徙邻,如果STL實(shí)現(xiàn)的是淺拷貝的版本,筆者感覺自己的很多代碼都有問題吶畸裳!有興趣的讀者可以自己動手嘗試下——動手寫代碼是學(xué)習(xí)的重要環(huán)節(jié)缰犁!
下面我們來關(guān)注拷貝賦值函數(shù)~
拷貝賦值函數(shù)(Copy assignment)
首先還是看看拷貝賦值函數(shù)的定義
Objects are not only copied on construction, when they are initialized: They can also be copied on any assignment operation.
對象除了在構(gòu)造函數(shù)中被構(gòu)造,同樣也可以通過賦值的方式進(jìn)行創(chuàng)造躯畴。這個(gè)過程就是使用我們的拷貝賦值函數(shù)了民鼓。我們首先來看一段代碼。
代碼中我們實(shí)現(xiàn)了賦值拷貝函數(shù)蓬抄。copy assignment的本質(zhì)其實(shí)就是操作符的重載丰嘉,這里我們實(shí)現(xiàn)了一個(gè)淺拷貝的版本。同時(shí)我們也實(shí)現(xiàn)了一個(gè)拷貝構(gòu)造函數(shù)嚷缭,實(shí)現(xiàn)的是深拷貝的版本饮亏。我們很容易就認(rèn)為下面這種寫法一定會調(diào)用淺拷貝的版本耍贾,那么事實(shí)是什么樣的呢?
看到這個(gè)結(jié)果我是很詫異的路幸,因?yàn)榻Y(jié)果顯示調(diào)用的事實(shí)上是深拷貝荐开,難道這里面還有各種復(fù)雜的優(yōu)先級的問題在嗎?事實(shí)上并不是這樣<螂取(沒這么復(fù)雜啦)其實(shí)晃听,是我們對這個(gè)拷貝賦值的理解出了問題,我們使用的根本不是“賦值”砰识,而是“初始化”能扒!既然是初始化,那么構(gòu)造函數(shù)就自然而然的被調(diào)用了辫狼,正確的拷貝賦值的使用方法如下初斑。
我們發(fā)現(xiàn)此時(shí)a, b的resource的地址變得一樣了膨处,也就是說拷貝賦值函數(shù)被調(diào)用了见秤。這個(gè)程序運(yùn)行到最后會出問題,機(jī)智的讀者一定已經(jīng)發(fā)現(xiàn)了其中的問題所在——沒錯(cuò)真椿,就是淺拷貝惹的禍鹃答。a和b的析構(gòu)函數(shù)被調(diào)用了兩次,兩次嘗試釋放掉同一個(gè)指針瀑粥,一個(gè)指針是不可能被釋放兩次的挣跋!
將copy assignment改為deep copy——problem solved!
幾個(gè)需要注意的問題:
在我們使用copy assignment的時(shí)候狞换,如果賦值對象中含有const修飾的成員變量的時(shí)候,c++的編譯器會拒絕該賦值操作——因?yàn)樗恢廊绾蚊鎸ψ约鹤约寒a(chǎn)生的const成員
在我們的base class將copy assignment聲明為private舟肉,編譯器對拒絕為其生成一個(gè)copy assignment
在需要的時(shí)候拒絕拷貝修噪!
在閱讀《effective c++》的時(shí)候,Item 6中明確地指出
若不想使用編譯器自動生成的函數(shù)路媚,就該明確拒絕
Explicitly disallow the use of compiler-generated functions you do not want.
我們之前提到過黄琼,當(dāng)我們沒有聲明copy assignment與copy constructor的時(shí)候,編譯器會為我們默認(rèn)生成一個(gè)整慎。(同樣的還有destructor和construtor)脏款,但是《effective c++》中有一點(diǎn)提的很好——有些時(shí)候一些對象可能表示的是獨(dú)一無二的東西,那么對于這些對象進(jìn)行副本的生成顯然是沒有道理的裤园。沒有道理的事情我們理應(yīng)拒絕撤师,那么我們要如何拒絕一個(gè)編譯器自動幫我們生成的內(nèi)容呢?
其實(shí)答案很簡單拧揽,在上文中我們也提到了剃盾,copy函數(shù)全部都是public的腺占,而且如果將其聲明為private,編譯器會拒絕為其生成一個(gè)默認(rèn)版本的痒谴。那么我們只要將copy assignment和copy constructor申明為private就可以間接地阻止編譯器創(chuàng)建其專屬版本的copy函數(shù)了衰伯。
但是書中又提到了,這樣的做法也不是絕對安全的——我們通過繼承的知識积蔚,可以知道成員函數(shù)和友元函數(shù)是依然可以找到這兩個(gè)private函數(shù)的意鲸,一旦不慎調(diào)用了這兩個(gè)函數(shù)會觸發(fā)一個(gè)linkage error【”可見怎顾,拷貝確實(shí)會出現(xiàn)一系列的問題啊教翩!【‘==|
但是這個(gè)問題同樣也是可以解決的杆勇!我們可以將這個(gè)鏈接時(shí)錯(cuò)誤移至編譯期,方法就是設(shè)計(jì)一個(gè)專門為了阻止copying動作而設(shè)計(jì)的base class饱亿,并將copy assignment 和copy constructor聲明為private蚜退,再讓目標(biāo)class繼承這個(gè)類即可。
之前我們同樣提到彪笼,如果base class中的copy function被聲明為了public钻注,derived class中一旦調(diào)用,編譯期便會報(bào)錯(cuò)配猫,這樣我們就將錯(cuò)誤移至了編譯期——畢竟幅恋,錯(cuò)誤越早發(fā)現(xiàn)就越好!
參考文獻(xiàn):c++官網(wǎng)
《effective c++》
下一篇泵肄,將是這個(gè)系列的完結(jié)篇捆交,將會介紹move constructor和move assignment
下回見啦~