淺談C++普通指針和智能指針管理動态記憶體的陷阱
前言:
C++中動态記憶體的管理主要是使用new/delete表達式和std::allcator類。為了管理動态記憶體更加安全,C++11新标準庫推出了智能指針。這裡隻讨論使用他們在使用過程常見的錯誤以及解決方法,不過多讨論文法。
一、使用new和delete管理動态記憶體三個常見的問題。
1、忘記釋放(delete)記憶體。忘記釋放動态記憶體會導緻人們常說的 “記憶體洩漏(memory leak)” 問題 ,因為這種記憶體永遠不可能歸還系統,除非程式退出。比如在某個作用域的代碼如下:向系統申請了一塊記憶體,離開作用域之前沒有接管使用者這塊記憶體,也沒有釋放這塊記憶體。
{
//....
int *p = new int(0);
//....
}
有兩個方法可以避免以上問題:
(1) 在p離開它new所在作用域之前,釋放這塊記憶體。如:delete p
{
//....
int *p = new int(0);
//....
delete p; //釋放p的向系統申請的記憶體
p = nullptr; //盡管在這個地方沒必要,這是一個好習慣,也是動态管理記憶體常見的出錯的地方。等下會說到。
}
(2) 接管p的向系統申請的記憶體。 比如通過指派,函數傳回值等。
int *pAnother;
{
//....
int *p = new int(0);
//....
pAnother = p; //pAnother接管p所指向的記憶體。
}
//pAnother do something
delete pAnother; //通關pAnother,将p所申請的記憶體歸還系統。
2、使用已經釋放記憶體的對象。這種行為是未定義的,通過在釋放記憶體後将指針設定位空指針(nullptr),有時可以避免這個問題(這是基于一個前提條件,使用動态配置設定記憶體對象前,需要檢查該對象是否指向空(nullptr))。假如不對已經釋放記憶體的對象指派空指針,他的值是未定義的,就好比其他變量,使用未初始化的對象,其行為大都是未定義。
note: nullptr(C++11剛引入)是一種特殊類型的字面值,它可以被轉換成任何其他指針類型。過去程式使用NULL的預處理變量來給指針指派。 他們的值都是0。
使用已經釋放記憶體的對象,如下代碼:
{
//....
int *p = new int(0);
// p do something
delete p;
//do other thing...
std::cout<<*p<<std::endl; //*p的值是未定義
//....
}
避免以上問題:(對已經釋放記憶體對象賦于一個空指針,使用前進行判斷是否為空指針)
{
//....
int *p = new int(0);
// p do something
delete p;
//下面三條語句等價
p = nullptr;
//p = NULL;
//p = 0;
//do other thing...
if(p!=nullptr) //等價if(p)
std::cout<<*p<<std::endl;
//....
}
note: 同樣當我們定義一個指針時,如果沒有立即為它配置設定記憶體,也需要将指針設定為空指針,防止不恰當使用。這裡也涉及一個問題,new出來的記憶體也應該初始化,稍後再講。
3、同一塊記憶體釋放兩次。 當有兩個指針指向相同的動态配置設定對象時,可能發生這種錯誤。如果對其中一個對象進行了delete操作,對象的記憶體就歸還給系統,如果我們随後有delete第二個指針,堆空間可能被破壞。
産生問題代碼:
int *pAnother;
{
//....
int *p = new int(0);
pAnother =p;
//p do something....
delete p;
}
delete pAnother; //未定義行為
避免這個問題:在delete p 之後, 将p置為一個空指針。
其次明白一個道理:delete p, p 必須指向一個空指針或者動态配置設定的記憶體,否則其行為未定義。
note: 這也很好就解釋了為什麼delete一個對象之後需要将該對象置為空指針,一是為了避免再次通路它出現未定義行為,二是為了避免再次delete它出現未定義行為。
小結:
1、定義一個指針需要初始化為空指針,(除非在定義的時候給它申請一塊記憶體)
2、通路一個指針需要先判斷該指針是否為空指針。
3、 釋放一個指針之後,應該将它置為空指針。
二、使用std::allocator類管理動态記憶體
在繼續了解标準庫std::allocator類管理動态記憶體之前,有必要先了解new和delete具體工作(機制)。
new完成的操作:
(1): 它配置設定足夠存儲一個特定類型對象的記憶體
(2):為它剛才配置設定的記憶體中的那個對象設定初始值。(對于内置類型對象,就是預設初始化該它,對應類類型,調用constructor初始化)
delete完成的操作:
(1):銷毀給定指針指向的對象
(2):釋放該對象的對應記憶體
這兒有詳細的講叙,new, delete背後在做什麼:http://blog.csdn.net/hazir/article/details/21413833
标準庫std::allocator類幫助我們将記憶體配置設定和對象初始化分離開來,也允許我們将對象的銷毀跟對象記憶體釋放分開來。std::allocator配置設定的記憶體是原始的、未構造的。這裡提供一個執行個體感受一下這個流程。然後注意事項跟new/delete類似。std::allocator在memory頭檔案中。
{
std::allocator<std::string> allocate_str; //定義一個可以配置設定記憶體的string的allocator對象allocate_str
std::string *p = allocate_str.allocate(1); //配置設定一個未初始化的string,p指向一塊大小為string的原始記憶體
//std::cout<<*p<<std::endl; eg:這種行為是未定義的
allocate_str.construct(p,"hello world"); //初始化p,*p="hello world";
std::cout<<*p<<std::endl; //列印出hello world
allocate_str.destroy(p);// 銷毀p構造的對象。對應的是調用p的析構函數,
//這時候指向一塊原始記憶體,其值是未定義的。
allocate_str.deallocate(p,1); //将指向的原始記憶體歸還給系統,也就是釋放p的記憶體
}
三、智能指針(smart pointer)
為了更加安全的管理動态記憶體,C++11新标準庫推出了智能指針。主要是std::shared_ptr 、 std::unique_ptr 、std::weak_ptr(作為一個伴随類)。他們都位于memory後檔案中。
智能指針的行為類似普通指針,一個重要差別是他負責自動釋放所指向對象的記憶體。智能指針可以提供對動态配置設定的記憶體安全而又友善的管理,但這是建立在正确使用的前提下,為了正确使用智能指針,我們必須堅持一些基本規範。
在管理new配置設定出來的資源,shared_ptr類大概可以這樣了解:(省略很多,最明顯沒有一個計數器,但有助加深對智能指針了解,我是這麼認為。)
template<class T>
class shared_ptr
{
public:
shared_ptr(T* p=0):ptr(p) {} //存儲對象
~shared_ptr(){ delete ptr; } //删除對象
T* get() { return ptr;}
private:
T *ptr;
};
1、不使用相同的普通指針初始化多個智能指針。因為當某個智能指針對象釋放其記憶體時,這個普通指針相應會被delete,此時其他智能指針管理的資源已經被釋放了,再對資源進行操作其行為是未定義。請看下面代碼。
{
int *p = new int(10);
std::cout<<*p<<std::endl;
std::shared_ptr<int> ptr1(p);
//...
{
//....
std::shared_ptr<int> ptr2(p);
//...
} //當ptr2離開其作用域,釋放ptr2對象,p所指向的資源也被delete,可以參考上面的hare_ptr類定義。
//..
//此時ptr1對象所管理的資源已經被釋放了。
std::cout<<*ptr1<<std::endl; //這種行為是未定義的
}
2、不delete get()傳回的指針。get()即傳回智能指針對象中儲存的指針,這個應該很容易了解,delete了get()傳回的指針,那麼相當于釋放了智能指針的資源。代碼如下:
{
std::shared_ptr<int> ptr(new int(10));
//...
int *p =ptr.get();
//..
std::cout<<*p<<std::endl; //可以通路
//...
delete p;
//此時ptr對象所管理的資源是被釋放了。
std::cout<<*ptr<<std::endl; //這個值是未定義的
}
3、如果你使用get()傳回的指針,記住當最後一個對應的智能指針銷毀後,你的指針就變為無效了。這個道理跟第2條類似,這兩條都是普通指針跟智能指針公用資源,那麼無論誰釋放了記憶體,另外一個都不能再使用該資源,其行為是未定義的。
int *p=nullptr;
{
std::shared_ptr<int> ptr(new int(0));
//ptr do something....
p = ptr.get();
//....
} //當ptr離開作用域,其引用次數減為0,是以釋放其所管理資源
std::cout<<*p<<std::endl; //此時p的值是未定義的
4、不使用get()初始化或reset()另一個智能指針。這個道理也是跟上面類似,reset()作用大概是釋放調用者所管理的資源,如果有參數,那麼該調用者轉去管理新的資源(參數)。
std::shared_ptr<int> ptr(new int(0));
{
//使用get()去初始化另一個智能指針。那麼當ptrAnother離開其作用域,
//他将會釋放ptr管理的資源(引用計數為0),
std::shared_ptr<int> ptrAnother(ptr.get());
std::cout<<*ptr<<std::endl;
//其分析跟上面一樣,
std::shared_ptr<int> ptrThird;
ptrThird.reset(ptr.get());
}
5、如果你使用的智能指針管理的資源不是new管理的記憶體,記住傳遞它一個删除器。
C++類動應以了析構函數,但是一些為了C和C++兩種語言而設計的類。通常都沒有定義析構函數。很容易發生記憶體洩漏。
struct destination; // 表示我們正在連接配接什麼
struct connection; // 打開連接配接所需的資訊
connection connect(destination*); // 打開連接配接
void disconnect(connection); // 關閉給定的連接配接
void f(destination &d /* other parameters */)
{
// 獲得一個連接配接,使用完記得關閉它。
connection c = connect(&d);
//.....使用連接配接
//如果再離開f前忘記調用disconnect,就無法關閉c了。
}
為了避免這種問題,可以使用std::shared_ptr,但是需要傳遞一個删除器給他。
#include <iostream>
#include <string>
#include <memory>
struct connection {
std::string ip;
int port;
connection(std::string ip_, int port_) : ip(ip_), port(port_) {}
};
struct destination {
std::string ip;
int port;
destination(std::string ip_, int port_) : ip(ip_), port(port_) {}
};
connection connect(destination* pDest)
{
std::shared_ptr<connection> pConn(new connection(pDest->ip, pDest->port));
std::cout << "creating connection(" << pConn.use_count() << ")"
<< std::endl;
return *pConn;
}
void disconnect(connection pConn)
{
std::cout << "connection close(" << pConn.ip << ":" << pConn.port << ")"
<< std::endl;
}
void end_connection(connection* pConn)
{
disconnect(*pConn);
}
void f(destination& d)
{
connection conn = connect(&d);
std::shared_ptr<connection> p(&conn, end_connection);
//p管理&conn的資源,當其引用計數為0,調用end_connection。 在這裡就相當于離開函數f,釋放conn的資源。
std::cout << "connecting now(" << p.use_count() << ")" << std::endl;
}
int main()
{
destination dest("202.118.176.67", 3316);
f(dest);
}
小結:智能指針跟普通指針混合使用應當特别注意,防止引用不存在的資源。另外不具備析構函數的類,使用智能指針的時候應該提供一個删除器。
原文:http://blog.csdn.net/qq_33850438/article/details/52994314
參考:
C++ Primer 5th
Effective C++