C++中的Primer拷貝控制和資源管理詳解
拷貝控制和資源管理
通常,管理類外資源的類必須定義拷貝控制成員,這種類需要通過析構(gòu)函數(shù)來釋放對象所分配的資源。一旦一個(gè)類需要析構(gòu)函數(shù),那么它兒乎肯定也需要一個(gè)拷貝構(gòu)造函數(shù)和一個(gè)拷貝賦值運(yùn)算符。
為了定義這些成員,我們首先必須確定此類型對象的拷貝語義。一般來說,有兩種選擇,可以定義拷貝操作,使類的行為看起來像一個(gè)值或者像一個(gè)指針。
類的行為像一個(gè)值,意味著它應(yīng)該也有自己的狀態(tài)。當(dāng)我們拷貝一個(gè)類值的對象時(shí),副本和原對象是完全獨(dú)立的。改變副本不會對原對象有任何影響,反之亦然。
行為像指針的類則共享狀態(tài)。當(dāng)我們拷貝一個(gè)這種類的對象時(shí),副本和原對象使用相同的底層數(shù)據(jù)。改變副本也會改變原對象,反之亦然。
在我們使用過的標(biāo)準(zhǔn)庫類中,標(biāo)準(zhǔn)庫容器和string類的行為像一個(gè)值。而不出意外的,shared_ptr類提供類似指針的行為,就像我們的StrBlob類一樣,IO類型和unique_ptr不允許拷貝或賦值,因此它們的行為既不像值也不像指針。
為了說明這兩種方式,我們會為練習(xí)中的HasPtr類定義拷貝控制成員。首先,我們將令類的行為像一個(gè)值;然后重新實(shí)現(xiàn)類,使它的行為像一個(gè)指針。
我們的HasPtr類有兩個(gè)成員,一個(gè)int和一個(gè)string指針。通常,類直接拷貝內(nèi)置類型(不包括指針)成員;這些成員本身就是值,因此通常應(yīng)該讓它們的行為像值一樣。我們?nèi)绾慰截愔羔槼蓡T決定了像HasPtr這樣的類是具有類值行為還是類指針行為。
行為像值的類
為了提供類值的行為,對于類管理的資源,每個(gè)對象都應(yīng)該擁有一份自己的拷貝。這意味著對于ps指向的string,每個(gè)HasPtr對象都必須有自己的拷貝。為了實(shí)現(xiàn)類值行為,HasPtr需要
- 定義一個(gè)拷貝構(gòu)造函數(shù),完成string的拷貝,而不是拷貝指針
- 定義一個(gè)析構(gòu)函數(shù)來釋放string
- 定義一個(gè)拷貝賦值運(yùn)算符來釋放對象當(dāng)前的string,并從右側(cè)運(yùn)算對象拷貝string
類值版本的HasPtr如下所示
class HasPtr{ public: HasPtr(const std::string &s=std::string()): ps(new std::string(s)1),i(0){} //對ps指向的string,每個(gè)BasPtr對象都有自己的拷貝 HasPt(const HasPtr&p): ps(new std::string(*p.ps)),i(p.i){} HasPtr & operator=(const HasPtr&); ~HasPtr(){delete ps;} private: std::string*ps; int i; }
我們的類足夠簡單,在類內(nèi)就已定義了除賦值運(yùn)算符之外的所有成員函數(shù)。第一個(gè)構(gòu)造函數(shù)接受一個(gè)(可選的)string參數(shù)。這個(gè)構(gòu)造函數(shù)動(dòng)態(tài)分配它自己的string副本,并將指向string的指針保存在ps中??截悩?gòu)造函數(shù)也分配它自己的string副本。析構(gòu)函數(shù)對指針成員ps執(zhí)行delete,釋放構(gòu)造函數(shù)中分配的內(nèi)存。
類值拷貝賦值運(yùn)算符
賦值運(yùn)算符通常組合了析構(gòu)函數(shù)和構(gòu)造函數(shù)的操作。類似析構(gòu)函數(shù),賦值操作會銷毀左側(cè)運(yùn)算對象的資源。類似拷貝構(gòu)造函數(shù),賦值操作會從右側(cè)運(yùn)算對象拷貝數(shù)據(jù)。但是,非常重要的一點(diǎn)是,這些操作是以正確的順序執(zhí)行的,即使將一個(gè)對象賦予它自身,也保證正確。而且,如果可能,我們編寫的賦值運(yùn)算符還應(yīng)該是異常安全的一一當(dāng)異常發(fā)生時(shí)能將左側(cè)運(yùn)算對象置于一個(gè)有意義的狀態(tài)。
在本例中,通過先拷貝右側(cè)運(yùn)算對象,我們可以處理自賦值情況,并能保證在異常發(fā)生時(shí)代碼也是安全的。在完成拷貝后,我們釋放左側(cè)運(yùn)算對象的資源,并更新指針指向新分配的string:
HasPtr&HasPtr::operator=(const HasPtr&rhs) { auto newp=new string(*rhs.ps);//指貝底層string delete ps;//釋放舊內(nèi)存 ps = newp;//從右側(cè)運(yùn)算對象指貝數(shù)據(jù)到本對象 i = rhs; return*this;//返回本對象 }
在這個(gè)賦值運(yùn)算符中,非常清楚,我們首先進(jìn)行了構(gòu)造函數(shù)的工作:newp的初始化器等價(jià)于HasPtr的拷貝構(gòu)造函數(shù)中ps的初始化器。接下來與析構(gòu)函數(shù)一樣,我們delete當(dāng)前ps指向的string。然后就只剩下拷貝指向新分配的string的指針,以及從rhs拷貝int值到本對象了。
關(guān)鍵概念:賦值運(yùn)算征
當(dāng)你編寫賦值運(yùn)算符時(shí):有兩點(diǎn)需要記住:
- 如果將一個(gè)對象賦予它自身,賊值運(yùn)算待必須能正確工作。
- 天多數(shù)賦值運(yùn)算符組各了析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù)的工作。
當(dāng)你編寫一個(gè)賦值運(yùn)算符時(shí),一個(gè)好的模式是先將右側(cè)運(yùn)算對象賦值到一個(gè)局部臨時(shí)對象中。當(dāng)拷貝完成后,銷毀左側(cè)運(yùn)算對象的現(xiàn)有成員就是安全的了。一旦左側(cè)運(yùn)算對象的資源被銷毀,就只剩下將數(shù)據(jù)從臨時(shí)對象拷貝到左側(cè)運(yùn)算對象的成員中了。
為了說明防范自賦值操作的重要性,考慮如果賦值運(yùn)算符如下編寫將會發(fā)生什么
//這樣編寫賦值運(yùn)算符是錯(cuò)誤的! HasPtr& HasPtr::operator=(const HasPtr&zhs) { delete ps;//釋放對象指向的string //如果rhs和*this是同一個(gè)對象,我們就將從已釋放的內(nèi)存中拷貝數(shù)據(jù)! ps=new string(*(rhs.ps)); i=rhs.i; return*this; }
如果zhs和本對象是同一個(gè)對象,delete ps會釋放this和rhs指向的string。接下來,當(dāng)我們在new表達(dá)式中試圖拷貝(rhs.ps)時(shí),就會訪問一個(gè)指向無效內(nèi)存的指針,其行為和結(jié)果是未定義的。
這樣我們的StrBlobPtr類就仍能使用指向vector的weak_ptr了。你修改后的類將需要一個(gè)拷貝構(gòu)造函數(shù)和一個(gè)拷貝賦值運(yùn)算符,但不需要析構(gòu)函數(shù)。解釋拷貝構(gòu)造出數(shù)和拷貝賦值運(yùn)算符必須要做什么。解釋為什么不需要析構(gòu)函數(shù)。
定義行為像指針的類
對于行為類似指針的類,我們需要為其定義拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,來拷貝指針成員本身而不是它指向的string。我們的類仍然需要自己的析構(gòu)函數(shù)來釋放接受string參數(shù)的構(gòu)造函數(shù)分配的內(nèi)存。但是,在本例中,析構(gòu)函數(shù)不能單方面地釋放關(guān)聯(lián)的string。只有當(dāng)最后一個(gè)指向string的HasPtr銷毀時(shí),它才可以釋放string。
令一個(gè)類展現(xiàn)類似指針的行為的最好方法是使用shared_ptr來管理類中的資源??截?或賦值)一個(gè)shared_ptr會拷貝(賦值)shared_ptr所指向的指針。
shared_ptr類自己記錄有多少用戶共享它所指向的對象。當(dāng)沒有用戶使用對象時(shí),shared_ptr類負(fù)責(zé)釋放資源。
但是,有時(shí)我們希望直接管理資源。在這種情況下,使用引用計(jì)數(shù)(reference count)就很有用了。為了說明引用計(jì)數(shù)如何工作,我們將重新定義HasPtr,令其行為像指針一樣,但我們不使用shared_ptr,而是設(shè)計(jì)自己的引用計(jì)數(shù)。
引用計(jì)數(shù)
引用計(jì)數(shù)的工作方式如下:
- 除了初始化對象外,每個(gè)構(gòu)造函數(shù)(拷貝構(gòu)造函數(shù)除外)還要?jiǎng)?chuàng)建一個(gè)引用計(jì)數(shù),用來記錄有多少對象與正在創(chuàng)建的對象共享狀態(tài)。當(dāng)我們創(chuàng)建一個(gè)對象時(shí),只有一個(gè)對象共享狀態(tài),因此將計(jì)數(shù)器初始化為1。
- 拷貝構(gòu)造函數(shù)不分配新的計(jì)數(shù)器,而是拷貝給定對象的數(shù)據(jù)成員,包括計(jì)數(shù)器??截悩?gòu)造函數(shù)遞增共享的計(jì)數(shù)器,指出給定對象的狀態(tài)又被一個(gè)新用戶所共享。“析構(gòu)函數(shù)遞減計(jì)數(shù)器,指出共享狀態(tài)的用戶少了一個(gè)。如果計(jì)數(shù)器變?yōu)?,則析構(gòu)函數(shù)釋放狀態(tài)。
- 拷貝賦值運(yùn)算符遞增右側(cè)運(yùn)算對象的計(jì)數(shù)器,遞減左側(cè)運(yùn)算對象的計(jì)數(shù)器。如果左側(cè)運(yùn)算對象的計(jì)數(shù)器變?yōu)?,意味著它的共享狀態(tài)沒有用戶了,拷貝賦值運(yùn)算符就必須銷毀狀態(tài)。
唯一的難題是確定在哪里存放引用計(jì)數(shù)。計(jì)數(shù)器不能直接作為HasPtr對象的成員。
下面的例子說明了原因:
HasPtr p1("HiYa!"); HasPtr p2(p1);//p1和p2指向相同的string HasPtr p3(p1);//p1、p2和p3都指向相同的string
如果引用計(jì)數(shù)保存在每個(gè)對象中,當(dāng)創(chuàng)建p3時(shí)我們應(yīng)該如何正確更新它呢?可以遞增p1中的計(jì)數(shù)器并將其拷貝到p3中,但如何更新p2中的計(jì)數(shù)器呢?
解決此問題的一種方法是將計(jì)數(shù)器保存在動(dòng)態(tài)內(nèi)存中。當(dāng)創(chuàng)建一個(gè)對象時(shí),我們也分配一個(gè)新的計(jì)數(shù)器。當(dāng)拷貝或賦值對象時(shí),我們拷貝指向計(jì)數(shù)器的指針。使用這種方法,副本和原對象都會指向相同的計(jì)數(shù)器。
定義一個(gè)使用引用計(jì)數(shù)的類
通過使用引用計(jì)數(shù),我們就可以編寫類指針的HasPtr版本了:
class HasPtr{ public: //構(gòu)造函數(shù)分配新的string和新的計(jì)數(shù)器,將計(jì)數(shù)器置為1 HasPtr(const std::string&s=std::string()): ps(newstd::string(s)),i(0),use(new std::size_t(1)){} //指貝構(gòu)造函數(shù)拷貝所有三個(gè)數(shù)據(jù)成員,并遞增計(jì)數(shù)器 HasPtr(const HasPtr &p): ps(p.ps),i(p.i),use(p.use){++*use;} HasPtr &operator=(const HasPtr&); ~HasPtr(); private: std::string* ps; std::size_t* use;//用來記錄有多少個(gè)對象共享*ps的成員 };
在此,我們添加了一個(gè)名為use的數(shù)據(jù)成員,它記錄有多少對象共享相同的string。接受string參數(shù)的構(gòu)造函數(shù)分配新的計(jì)數(shù)器,并將其初始化為1,指出當(dāng)前有一個(gè)用戶使用本對象的string成員。
類指針的拷貝成員“篡改“引用計(jì)數(shù)
當(dāng)拷貝或賦值一個(gè)HasPtr對象時(shí),我們希望副本和原對象都指向相同的string。即,當(dāng)拷貝一個(gè)HasPtr時(shí),我們將拷貝ps本身,而不是ps指向的string。當(dāng)我們進(jìn)行拷貝時(shí),會遞增該string關(guān)聯(lián)的計(jì)數(shù)器。
(我們在類內(nèi)定義的)拷貝構(gòu)造函數(shù)拷貝給定HasPtr的所有三個(gè)數(shù)據(jù)成員。這個(gè)構(gòu)造函數(shù)還遞增use成員,指出ps和p.ps指向的string又有了一個(gè)新的用戶。
析構(gòu)函數(shù)不能無條件地delete ps一一可能還有其他對象指向這塊內(nèi)存。析構(gòu)函數(shù)應(yīng)該遞減引用計(jì)數(shù),指出共享string的對象少了一個(gè)。如果計(jì)數(shù)器變?yōu)?,則析構(gòu)函數(shù)釋放ps和use指向的內(nèi)存:
HasPtr::~HasPtr() { if(--*use==0){//如果引用計(jì)數(shù)變?yōu)? delete ps;//釋放string內(nèi)存 delete use;//釋放計(jì)數(shù)器內(nèi)存 } }
拷貝賦值運(yùn)算符與往常一樣執(zhí)行類似拷貝構(gòu)造函數(shù)和析構(gòu)函數(shù)的工作。即,它必須遞增右側(cè)運(yùn)算對象的引用計(jì)數(shù)(即,拷貝構(gòu)造函數(shù)的工作),并遞減左側(cè)運(yùn)算對象的引用計(jì)數(shù),在必要時(shí)釋放使用的內(nèi)存(即,析構(gòu)函數(shù)的工作)。
而且與往常一樣,賦值運(yùn)算符必須處理自賦值。我們通過先遞增zhs中的計(jì)數(shù)然后再遞減左側(cè)運(yùn)算對象中的計(jì)數(shù)來實(shí)現(xiàn)這一點(diǎn)。通過這種方法,當(dāng)兩個(gè)對象相同時(shí),在我們檢查ps(及use)是否應(yīng)該釋放之前,計(jì)數(shù)器就已經(jīng)被遞增過了:
HasPtr& HasPtr::operator=(const HasPtr&rhs) { ++*rhs.use;//通增右側(cè)運(yùn)算對象的引用計(jì)數(shù) if(--*use==0){//然后遞減本對象的引用計(jì)數(shù) delete ps;//如果沒有其他用戶 delete use;//釋放本對象分配的成員 } ps=rhs.ps;//將數(shù)據(jù)從rhs拷貝到本對象 i = rhs.i; use=rhs.use; return*this;//返回本對象 }
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Qt利用QNetwork實(shí)現(xiàn)上傳數(shù)據(jù)的示例代碼
這篇文章主要為大家詳細(xì)介紹了Qt如何利用QNetwork實(shí)現(xiàn)上傳數(shù)據(jù)的 功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-02-02C++學(xué)習(xí)筆記std::vector底層原理及擴(kuò)容
這篇文章主要為大家介紹了C++學(xué)習(xí)之std::vector底層原理及擴(kuò)容詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10歸并排序的遞歸實(shí)現(xiàn)與非遞歸實(shí)現(xiàn)代碼
以下是對歸并排序的遞歸實(shí)現(xiàn)與非遞歸實(shí)現(xiàn)代碼進(jìn)行了詳細(xì)的介紹,需要的朋友可以過來參考下2013-08-08C/C++ Qt 自定義Dialog對話框組件應(yīng)用案例詳解
有時(shí)候我們需要一次性修改多個(gè)數(shù)據(jù),使用默認(rèn)的模態(tài)對話框似乎不太夠用,此時(shí)我們需要自己創(chuàng)建一個(gè)自定義對話框。這篇文章主要介紹了Qt自定義Dialog對話框組件的應(yīng)用,感興趣的同學(xué)可以學(xué)習(xí)一下2021-11-11一篇文章帶你用C語言玩轉(zhuǎn)結(jié)構(gòu)體
本文主要介紹C語言 結(jié)構(gòu)體的知識,學(xué)習(xí)C語言肯定需要學(xué)習(xí)結(jié)構(gòu)體,這里詳細(xì)說明了結(jié)構(gòu)體并附示例代碼,供大家參考學(xué)習(xí),有需要的小伙伴可以參考下2021-09-09