C++淺拷貝與深拷貝及引用計(jì)數(shù)分析
C++淺拷貝與深拷貝及引用計(jì)數(shù)分析
在C++開發(fā)中,經(jīng)常遇到的一個(gè)問題就是與指針相關(guān)的內(nèi)存管理問題,稍有不慎,就會(huì)造成內(nèi)存泄露、內(nèi)存破壞等嚴(yán)重的問題。不像Java一樣,沒有指針這個(gè)概念,所以也就不必?fù)?dān)心與指針相關(guān)的一系列問題,但C++不同,從C語言沿襲下來的指針是其一大特點(diǎn),我們常常要使用new/delete來動(dòng)態(tài)管理內(nèi)存,那么問題來了,特別是伴隨著C++的繼承機(jī)制,如野指針、無效指針使用、內(nèi)存泄露、double free、堆碎片等等,這些問題就像地雷一樣,一不小心就會(huì)踩那么幾顆。
先來談一下C++類中常見的淺拷貝問題,以及由此引發(fā)的double free。什么是淺拷貝?當(dāng)類中的成員變量包括指針時(shí),而又沒有定義自己的拷貝構(gòu)造函數(shù),那么在拷貝一個(gè)對象的情況下,就會(huì)調(diào)用其默認(rèn)拷貝構(gòu)造函數(shù),其實(shí)這個(gè)函數(shù)沒做什么事,只是對其成員變量作了個(gè)簡單的拷貝,也就是所謂的位拷貝,它們指向的還是同一個(gè)存儲(chǔ)空間,當(dāng)對象析構(gòu)時(shí),就會(huì)析構(gòu)多次,也就是double free,下面舉例說明。
class Common { public: Common() { std::cout << "Common::Common" << std::endl; } Common(const Common &r) { std::cout << "Common::Common copy-constructor" << std::endl; } ~Common() { std::cout << "Common::~Common" << std::endl; } };
類Common是個(gè)一般的類,定義了構(gòu)造、拷貝構(gòu)造和析構(gòu)函數(shù),在函數(shù)里輸出一些log,用以跟蹤函數(shù)調(diào)用情況。
class BitCopy { public: BitCopy() : m_p(new Common) { std::cout << "BitCopy::BitCopy" << std::endl; } ~BitCopy() { std::cout << "BitCopy::~BitCopy" << std::endl; if (m_p) { delete m_p; m_p = NULL; } } private: Common *m_p; };
類BitCopy就是一個(gè)淺拷貝類,成員變量是我們剛定義的類指針,構(gòu)造函數(shù)實(shí)例化成員變量,析構(gòu)函數(shù)delete成員變量,沒有定義拷貝構(gòu)造函數(shù)。
int main() { BitCopy a; BitCopy b(a); return 0; } log如下: Common::Common BitCopy::BitCopy BitCopy::~BitCopy Common::~Common BitCopy::~BitCopy Common::~Common *** Error in `./a.out': double free or corruption (fasttop): 0x0000000001f4e010 *** 已放棄 (核心已轉(zhuǎn)儲(chǔ))
從上面的log可以看出,對象a調(diào)用了構(gòu)造函數(shù),對象b調(diào)用的是默認(rèn)拷貝構(gòu)造函數(shù),最后析構(gòu)了兩次,從而造成double free,核心已轉(zhuǎn)儲(chǔ)即core dump。
針對以上問題,該怎么解決呢?有兩個(gè)辦法,一個(gè)是深拷貝,一個(gè)是引用計(jì)數(shù)。先來看一下深拷貝,深拷貝要定義自己的拷貝構(gòu)造函數(shù),在函數(shù)中給成員變量重新分配存儲(chǔ)空間,也就是所謂的值拷貝,這樣它們所指向的就是不同的存儲(chǔ)空間,析構(gòu)時(shí)不會(huì)有問題,但這種方法只適用于較小的數(shù)據(jù)結(jié)構(gòu),如果數(shù)據(jù)結(jié)構(gòu)過大,多次分配存儲(chǔ)空間之后,剩余的存儲(chǔ)空間將逐漸減小,
下面看個(gè)例子。
class ValueCopy { public: ValueCopy() : m_p(new Common) { std::cout << "ValueCopy::ValueCopy" << std::endl; } ValueCopy(const ValueCopy &r) : m_p(new Common(*r.m_p)) { std::cout << "ValueCopy::ValueCopy copy-constructor" << std::endl; } ~ValueCopy() { std::cout << "ValueCopy::~ValueCopy" << std::endl; if (m_p) { delete m_p; m_p = NULL; } } private: Common *m_p; };
類ValueCopy是個(gè)深拷貝類,與上面例子的淺拷貝類不同的是定義了拷貝構(gòu)造函數(shù),在函數(shù)中給成員變量重新分配存儲(chǔ)空間,下面是用法及l(fā)og。
int main() { ValueCopy c; ValueCopy d(c); return 0; } Common::Common ValueCopy::ValueCopy Common::Common copy-constructor ValueCopy::ValueCopy copy-constructor ValueCopy::~ValueCopy Common::~Common ValueCopy::~ValueCopy Common::~Common
從上面的log可以看出,對象c調(diào)用了構(gòu)造函數(shù),對象d調(diào)用的是自定義拷貝構(gòu)造函數(shù),最后析構(gòu)了兩次而沒有問題,可見深拷貝的用處所在。
引用計(jì)數(shù)與深拷貝不同,方法是共享同一塊存儲(chǔ)空間,這個(gè)對大的數(shù)據(jù)結(jié)構(gòu)比較有利。使用引用計(jì)數(shù),需要在類中定義一個(gè)成員變量專門用于計(jì)數(shù),初始值為1,后面引用了這個(gè)對象就加1,對象銷毀時(shí)引用減1,但并不真正的delete這個(gè)對象,只有當(dāng)這個(gè)成員變量的值為0時(shí)才進(jìn)行delete,例子如下。
class A { public: A() : m_refCount(1) { std::cout << "A::A" << std::endl; } A(const A &r) : m_refCount(1) { std::cout << "A::A copy-constructor" << std::endl; } ~A() { std::cout << "A::~A" << std::endl; } void attach() { std::cout << "A::attach" << std::endl; ++m_refCount; } void detach() { if (m_refCount != 0) { std::cout << "A::detach " << m_refCount << std::endl; if (--m_refCount == 0) { delete this; } } } private: int m_refCount; }; class B { public: B() : m_pA(new A) { std::cout << "B::B" << std::endl; } B(const B &r) : m_pA(r.m_pA) { std::cout << "B::B copy-constructor" << std::endl; m_pA->attach(); } ~B() { std::cout << "B::~B" << std::endl; m_pA->detach(); } private: A* m_pA; };
類A用到了引用計(jì)數(shù),構(gòu)造和拷貝構(gòu)造函數(shù)都初始化為1,attach()函數(shù)為引用加1,detach()函數(shù)為引用減1,當(dāng)引用計(jì)數(shù)值為0時(shí)delete對象。類B中的成員變量有個(gè)指針指向A,拷貝構(gòu)造函數(shù)中調(diào)用了attach(),析構(gòu)函數(shù)中調(diào)用了detach(),這樣也是一種保護(hù),不會(huì)有內(nèi)存泄露,也不會(huì)有double free,log如下。
int main() { B e; B f(e); return 0; } A::A B::B B::B copy-constructor A::attach B::~B A::detach 2 B::~B A::detach 1 A::~A
從log中可以看出,指針成員變量的引用計(jì)數(shù)為2,這是正確的,最后正確delete,沒有問題。
在類中只要有指針成員變量,就要注意以上問題,另外,operator=這個(gè)賦值操作符也要在適當(dāng)?shù)臅r(shí)候進(jìn)行重載。有時(shí)候,如果想規(guī)避以上問題,可以聲明拷貝構(gòu)造函數(shù)和operator=操作符為private而不去實(shí)現(xiàn)它們。
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
相關(guān)文章
用C語言來實(shí)現(xiàn)一個(gè)簡單的虛擬機(jī)
這篇文章主要介紹了用C語言來實(shí)現(xiàn)一個(gè)簡單的虛擬機(jī),其中棧數(shù)組的部分非常值得學(xué)習(xí),需要的朋友可以參考下2015-07-07深入講解C++數(shù)據(jù)類型轉(zhuǎn)換的相關(guān)函數(shù)的知識
這篇文章主要介紹了深入講解C++數(shù)據(jù)類型轉(zhuǎn)換的相關(guān)函數(shù)的知識,包括類型轉(zhuǎn)換運(yùn)算符函數(shù)等內(nèi)容,需要的朋友可以參考下2015-09-09C語言#define拼接宏定義實(shí)現(xiàn)方式
今天小編就為大家分享一篇C語言#define拼接宏定義實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12Qt數(shù)據(jù)庫相關(guān)應(yīng)用開發(fā)總結(jié)
這篇文章主要為大家介紹了在Qt數(shù)據(jù)庫應(yīng)用開發(fā)中的一些經(jīng)驗(yàn)總結(jié),以及一些組件的使用介紹。文中的示例代碼講解詳細(xì),需要的可以參考一下2022-02-02圖解AVL樹數(shù)據(jù)結(jié)構(gòu)輸入與輸出及實(shí)現(xiàn)示例
這篇文章主要為大家介紹了C++圖解AVL樹數(shù)據(jù)結(jié)構(gòu)輸入與輸出操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05VS2010+Opencv+MFC讀取圖像和視頻顯示在Picture控件
這篇文章主要為大家詳細(xì)介紹了VS2010+Opencv+MFC讀取圖像和視頻顯示在Picture控件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08