在包含指針的類中需要注意復制控制,復制指針時只復制指針中的地址,不會復制指針指向的對象。
大多數c++類采用三種方法管理指針成員:
1)指針成員采用常規指針型行為。
2)采用智能指針
3)采取值型行為
常規指針缺陷:可能會出現懸垂指針。當一個指針復制到另一個指針,兩個指針指向同一個對象,當一個指針刪除對象時,另一個指針不知道,所以出現懸垂指針。即使使用默認合成復制構造函數也會出現,類本身無法避免。
智能指針:加入了引用計數。引用計數跟蹤該類有多少對象共享同一指針。當引用計數為0 時,刪除對象。創建新類時,初始化指針並將引用計數置為1.進行復制時,增加相應引用計數值。賦值時,減少左操作數所指對象的引用計數的值(減至0,就刪除對象),增加右操作數所指對象的引用計數的值。最后,調用析構函數時,減少引用計數的值。如果減至0,就刪除對象。
值型類:復制時會new一個新的副本,指針所指向的對象是唯一的,每個類對象獨立管理。
為了管理具有指針成員的類,必須定義三個復制控制成員:復制構造函數,賦值操作符和析構函數。這些成員可以定義指針成員的指針型行為或者值型行為。
c++出現內存問題的地方一般:
1)緩沖區溢出
2)懸垂指針/野指針
3)重復釋放
4)內存泄漏
5)不配對的 new[]/delete
都可以通過智能指針很好的解決這些問題,比如:
1)->用vector/string或者自己寫的buffer類管理,自動增加緩沖區大小,用成員函數操作,不直接通過野指針操作
2),3),4)->可以通過智能指針解決,只在對象析構的時候釋放一次內存,引用計數為0的時候才刪除指針,自動釋放
5)->使用vector,自己釋放內存
智能指針的陷阱:
這樣的一個引用計數型智能指針目的是為了防止資源泄漏,但是只需要一個很小巧的代碼就可以讓這樣的初衷化為烏有……
class A { public: A() {cout<<"A CON"<<endl;} ~A() {cout<<"A DES"<<endl;} void hold(CountedPtr<A> ptr) { m_ptr = ptr; } private: CountedPtr<A> m_ptr; }; void self_cir_area() { CountedPtr<A> pA(new A()); pA->hold(pA); }
可以看見,一個對象A中有一個引用計數型智能指針,這樣的設計可能會很常見(指向自身類型的結構體——鏈表)
但是,當自身循環引用發生的時候會怎么樣呢? 下面就來看看這么兩句代碼
CountedPtr<A> pA(new A());
這里我們新建一個資源,並且把這個資源的管理權移交給pA這個引用計數型智能指針對象管理。如此,pA中的引用計數被初始化為1。
pA->hold(pA);
這里,我們把pA對象傳入給實例化的A對象中的引用計數型智能指針m_ptr,m_ptr執行這樣的一個成員函數:
//assignment (unshare old and share new value) CountedPtr<T>& operator= (const CountedPtr<T>& p) throw() { if (this != &p) { dispose(); ptr = p.ptr; count = p.count; ++*count; } return *this; }
因為這里很明顯不是自身賦值,A中的m_ptr和pA不是同一個對象,所以進入if結構中調用下面的內容。dispose是用作清理,因為m_ptr並沒有指向任何東西,所以第一個函數並沒有真正的意義。
然后
m_ptr.ptr = pA.ptr; m_ptr.count = pA.count; ++(*m_ptr.count); //(*pA.count)也被++
到此,pA的引用計數為2
嗯,下面就pA這個對象理所當然的離開了作用域,調用其析構函數:
~CountedPtr () throw() { dispose(); }
噢,是一個轉向,調用其private成員函數dispose():
很簡單,將引用計數-1,由2變成1,不為0,所以if結構內的語句不被執行。
由此,我們又制造了一個完美的太空垃圾……
void dispose() { if (--*count == 0) { delete count; delete ptr; } }
這樣的循環引用問題應該是在設計的過程中就應該避免的,如果用UML語言描述
A中持有一個 引用計數型智能指針 的語義就是 這個 持有關系 是需要在 A消失的時候所持有的對象也隨之消失(這正是智能指針的作用,在脫離作用域自動清除其持有的資源)。如此就構成了 組合 關系。如果要表示 聚合 關系,即有 部分-整體 關系但是部分不隨整體的消失而消失,這就不是 智能指針 所表達的語義。
還有可能遇見的循環引用就是 A1 持有 A2, A2 持有 A1 的情況……
這樣A1,A2中對雙方的引用計數都是2,當一方“銷毀”的時候,雙方的應用計數都變為1,實際上並沒有銷毀任何東西,制造了兩個完美無暇的太空垃圾~
這里又引發出一個問題,這樣的資源泄漏問題實際上還是由程序員自身引起的。
C++之所以是一個很容易出錯的語言,很大一部分在於其資源的管理權力全權交給了程序員。這樣的權力到底是造福了程序員還是迷惑了程序員呢?