借來的資源,如何還的瀟灑?
前言
本文的內(nèi)容將專門對(duì)付內(nèi)存管理,培養(yǎng)起有借有還的好習(xí)慣,方可消除資源管理的問題。
正文
所謂的資源就是,一旦用了它,將來必須還給系統(tǒng)。如果不是這樣,糟糕的事情就會(huì)發(fā)生。
C++ 程序內(nèi)常見的資源:
- 動(dòng)態(tài)分配內(nèi)存
- 文件描述符
- 互斥鎖
- 圖形頁面中的字型和筆刷
- 數(shù)據(jù)庫連接
- 網(wǎng)絡(luò) sockets
無論哪一種資源,重要的是,當(dāng)你不再使用它時(shí),必須將它還給系統(tǒng),有借有還是個(gè)好習(xí)慣。
細(xì)節(jié) 01 :以對(duì)象管理資源
把資源放在析構(gòu)函數(shù),交給析構(gòu)函數(shù)釋放資源
假設(shè)某個(gè) class 含有個(gè)工廠函數(shù),該函數(shù)獲取了對(duì)象的指針:
- A* createA(); // 返回指針,指向的是動(dòng)態(tài)分配對(duì)象。
- // 調(diào)用者有責(zé)任刪除它。
如上述注釋所言,createA 的調(diào)用端使用了函數(shù)返回的對(duì)象后,有責(zé)任刪除它。現(xiàn)在考慮有個(gè)f函數(shù)履行了這個(gè)責(zé)任:
- void f()
- {
- A *pa = createA(); // 調(diào)用工廠函數(shù)
- ... // 其他代碼
- delete pa; // 釋放資源
- }
這看起來穩(wěn)妥,但存在若干情況f函數(shù)可能無法執(zhí)行到delete pa語句,也就會(huì)造成資源泄漏,例如如下情況:
- 或許因?yàn)椤?hellip;」區(qū)域內(nèi)的一個(gè)過早的 return 語句;
- 或許因?yàn)椤?hellip;」區(qū)域內(nèi)的一個(gè)循環(huán)語句過早的continue 或 goto 語句退出;
- 或許因?yàn)椤?hellip;」區(qū)域內(nèi)的語句拋出異常,無法執(zhí)行到 delete。
當(dāng)然可以通過謹(jǐn)慎地編寫程序可以防止這一類錯(cuò)誤,但你必須想想,代碼可能會(huì)在時(shí)間漸漸過去后被修改,如果是一個(gè)新手沒有注意這一類情況,那必然又會(huì)再次有內(nèi)存泄漏的可能性。
為確保 A 返回的資源都是被回收,我們需要將資源放進(jìn)對(duì)象內(nèi),當(dāng)對(duì)象離開作用域時(shí),該對(duì)象的析構(gòu)函數(shù)會(huì)自動(dòng)釋放資源。
「智能指針」是個(gè)好幫手,交給它去管理指針對(duì)象。
對(duì)于是由動(dòng)態(tài)分配(new)于堆內(nèi)存的對(duì)象,指針對(duì)象離開了作用域并不會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)(需手動(dòng)delete),為了讓指針對(duì)象能像普通對(duì)象一樣,離開作用域自動(dòng)調(diào)用析構(gòu)函數(shù)回收資源,我們需要借助「智能指針」的特性。
常用的「智能指針」有如下三個(gè):
- std::auto_ptr( C++ 98 提供、C++ 11 建議摒棄不用 )
- std::unique_ptr( C++ 11 提供 )
- std::shared_ptr( C++ 11 提供 )
std::auto_ptr
下面示范如何使用 std::auto_ptr 以避免 f 函數(shù)潛在的資源泄漏可能性:
- void f()
- {
- std::auto_ptr<A> pa (createA()); // 調(diào)用工廠函數(shù)
- ... // 一如既往的使用pa
- } // 離開作用域后,經(jīng)由 auto_ptr 的析構(gòu)函數(shù)自動(dòng)刪除pa;
- std::auto_ptr<A> pa1(createA()); // pa1 指向 createA 返回物
- std::auto_ptr<A> pa2(pa1); // 現(xiàn)在 pa2 指向?qū)ο?,pa1將被設(shè)置為 null
- pa1 = pa2; // 現(xiàn)在 pa1 指向?qū)ο?,pa2 將被設(shè)置為 null
- std::unique_ptr<A> pa1(createA()); // pa1 指向 createA 返回物
- std::unique_ptr<A> pa2(pa1); // 編譯出錯(cuò)!
- pa1 = pa2; // 編譯出錯(cuò)!
- void f()
- {
- std::shared_ptr<A> pa1(createA()); // pa1 指向 createA 返回物
- std::shared_ptr<A> pa2(pa1); // 引用計(jì)數(shù)+1,pa2和pa1指向同一個(gè)內(nèi)存
- pa1 = pa2; // 引用計(jì)數(shù)+1,pa2和pa1指向同一個(gè)內(nèi)存
- }
- std::shared_ptr<A> pA(createA());
假設(shè)你希望以某個(gè)函數(shù)處理 A 對(duì)象,像這樣: 你想這么調(diào)用它:
- std::shared_ptr<A> pA(createA());
- getInfo(pA); // 錯(cuò)誤!!
會(huì)編譯錯(cuò)誤,因?yàn)?getInfo 需要的是 A 指針對(duì)象,而不是類型為std::shared_ptr 的對(duì)象。
這時(shí)候就需要用 std::shared_ptr 智能指針提供的 get 成員函數(shù)訪問原始的資源:
- std::shared_ptr<A> pA(createA());
- getInfo(pA.get()); // 很好,將 pA 內(nèi)的原始指針傳遞給 getInfo
智能指針「隱式」轉(zhuǎn)換的方式,是通過指針取值操作符。 智能指針都重載了指針取值操作符(operator->和operator*),它們?cè)试S隱式轉(zhuǎn)換至底部原始指針:
- class A
- {
- public:
- bool isExist() const;
- ...
- };
- A* createA(); // 工廠函數(shù),創(chuàng)建指針對(duì)象
- std::shared_ptr<A> pA(createA()); // 令 shared_ptr 管理對(duì)象資源
- bool exist = pA->isExist(); // 經(jīng)由 operator-> 訪問資源
- bool exist2 = (*pA).isExist(); // 經(jīng)由 operator* 訪問資源
- int getNum();
- void fun(std::shared_ptr<A> pA, int num);
- fun(std::shared_ptr<A>(new A), getNum());
令人想不到吧,上述調(diào)用卻可能泄露資源。接下來我們來一步一步的分析為什么存在內(nèi)存泄漏的可能性。
在進(jìn)入 fun 函數(shù)之前,肯定會(huì)先執(zhí)行各個(gè)實(shí)參。上述第二個(gè)實(shí)參只是單純的對(duì)getNum 函數(shù)的調(diào)用,但第一個(gè)實(shí)參 std::shared_ptr(new A) 由兩部分組成:
- std::shared_ptr<A> pA(new A); // 先構(gòu)造智能指針對(duì)象
- fun(pA, getNum()); // 這個(gè)調(diào)用動(dòng)作絕不至于造成泄漏。