問題引出:c++繼承了c那高效而又靈活的指針,使用起來稍微不小心就會導緻記憶體洩漏,懸挂指針,越界通路等問題,比如以下代碼:
int *p = new int;
...
if(xxx)
goto loop;
delete p;
以上代碼,很明顯很容易導緻記憶體洩漏問題,比如當滿足條件進行跳轉,或者當delete p;前的操作出現異常導緻程式退出,這些都會導緻動态記憶體沒有正确釋放掉,另外的當一個複雜程式,如果由于不小心delete/free了多次同一指針的話,這種錯誤編譯能夠通過,但在運作時會出現程式崩潰錯誤,另外還有可能忘了對指針進行delete導緻記憶體洩漏,而智能指針可以在哪些變量或對象在退出作用域時–不管是正常退出或異常退出–也能夠進行相應的資源釋放工作;
注:delete/free一個空指針總是沒有錯誤的;
以下就總結一下标準庫中的幾種智能指針包括:auto_ptr , shared_ptr , unique_ptr , weak_ptr ;都包含在頭檔案memory中;
1: auto_ptr
存在多種的智能指針,其中最有名的應該是c++98标準中改的自動指針“auto_ptr”,【注:c++11标準中聲明廢棄該智能指針,而應選用新的智能指針實std::unique_ptr,目前c++11中新增的智能指針有unique_ptr, shared_ptr, weak_ptr】,auto_ptr其實是一種類模闆,但其重載了operator*和operator->,auto_ptr對象的行為類似指針。其構造函數接受new操作符或者對象工廠建立出的對象指針作為參數,進而代理了原始指針,建立auto_ptr對象的示例方法如下代碼。c++保證當那些動态配置設定記憶體的指針變量離開作用域時,會調用auto_ptr的析構函數,進而使用delete操作符釋放動态申請的指針所指向的記憶體資源;
/* 不能使用指派的方法進行初始化一個auto_ptr,因為根據一般指針建立auto_ptr的構造函數被聲明為explict,
必須進行顯式的調用構造函數進行對象的建立,注意不是不可以指派 */
auto_ptr<int> p(new int());
auto_ptr<ClassA> pA(factory.create()); // 本質上還是傳入一個普通指針
auto_ptr的注意事項與常見操作:
1: auto_ptr所有權的轉移
即auto_ptr的對象所有權是獨占的,不可能有兩個auto_ptr對象同時擁有同一個動态對象的所有權【這個要求也是在提醒編碼時要注意防範以同一個對象為初值,将兩個auto_ptr初始化】;因為auto_ptr所有權獨占這個條件,導緻auto_ptr的copy構造函數和指派運算符函數的工作也有所不同;示例代碼:
auto_ptr<ClassA> ptr1(new ClassA);
auto_ptr<ClassA> ptr2(ptr1); // 調用拷貝構造函數進行對象的建立
auto_ptr<ClassA> ptr3(new ClassA);
auto_ptr<ClassA> ptr4;
...
ptr4 = ptr3; // 調用指派運算符
解釋:第一個語句中ptr1擁有new出來的那個對象的所有權,在第二個語句中,擁有權由ptr1轉交給ptr2。此後,ptr2就擁有了那個new出來的對象的所有權,ptr1不再擁有它,隻剩下一個null指針在手即“是去了所有權,隻剩一個null指針”;上面的指派操作也是相同的道理,ptr4擁有了原先ptr3擁有的對象,ptr3後來隻生一個null指針;
注:如果ptr3被指派之前正擁有另一個對象,指派動作發生時會調用delete,将該對象删除;
可能發生擁有權的轉移有函數轉交到另一個函數的情況:
1:某函數是資料的終點: auto_ptr以傳值得方式被當作一個參數傳遞給某函數,然後調用端獲得了這個auto_ptr的擁有權,如果函數不再将它傳遞出去,它所指的對象就會在函數退出時被删除;
2:某函數是資料的起點,當一個auto_ptr被傳回,其擁有權便被轉交給調用端了;
auto_ptr<ClassA> f()
{
auto_ptr<ClassA> ptr(new ClassA);
...
return ptr; // transfet ownership to calling function
}
void g()
{
auto_ptr<ClassA> p;
for(int i=; i<; i++)
{
p = f(); // p gets ownership of the returned object
// previous rerurned object of f() gets deleted
...
}
} // lasr-owned object of p gets deleted
由于函數調用導緻的所有權轉交問題,是以函數中的用法一定要注意:當五一轉交所有權時,不要再參數列中使用auto_ptr,也不要以它作為傳回值;
可以建立const型auto_ptr對象來終結所有權的轉交;
如:
const auto_ptr<int> p(new int());
auto_ptr<int> q(new int);
*p = ; // OK,關鍵詞const并非意味不能更改對象,而是意味不能更改auto_ptr的擁有權而已;
// 這個時候const型的auto_ptr更像是T* const p,而不是const T* P,盡管文法上看上去比較像後者;
p = q; // Error,想要轉交所有權,出現compile-time error
當auto_ptr作為類的成員變量之一時,别忘了要重寫拷貝構造函數和指派運算符函數,這兩個如果是預設的都會有所有權轉交的發生;
auto_ptr使用注意小結:
1: auto_ptr之間不能共享所有權;
2:auto_ptr不能指向數組;
3:auto_ptr不要作為容器的成員;
4:根據一般指針生成一個auto_ptr的構造函數被聲明為explict,是以不要一下方式進行對象的建立;
auto_ptr<int> p = new int(); // ERROR
auto_ptr<int> p(new int()); // OK
注:auto_ptr的接口與一般指針非常相似:operator*用來提領其所指對象,operator->用來指向對象中的成員,然而,*所有的指針算術(包括++)都沒有定義;
2: shared_ptr
是一種引用計數型的智能指針,允許多個指針指向同一個對象,包裝了new操作符在對上配置設定的動态對象,與auto_ptr或unique_ptr不同的是,shared_ptr可以被自由的拷貝和指派,在任意的地方共享它,當沒有代碼使用(引用計數為0)它時,才删除被包裝的動态配置設定的對象;shared_ptr可以被安全的放在标準容器中,是STL容器中存儲指針的最标準解法;
shared_ptr代碼示例
shared_ptr<string> p1; // 可以指向string的shared_ptr,預設初始化時儲存着一個空指針nullptr
shared_ptr<list<int> > p2; // 可以隻想list<int>的shared_ptr
shared_ptr和unique_ptr都支援的操作:
shared_ptr<T> sp / unique_ptr<T> up; // 空智能指針,可以指向類型T的對象
p // 将p用作一個條件進行判斷,若p指向一個對象,則為true
*p // 解引用,獲得它所指向的對象
p->mem // 等價于(*p).mem
p.get() // 傳回p中儲存的指針,要小心使用,若智能指針釋放了其對象,傳回的指針所指向的對象也就消失了
swap(p,q) / p.swap(q) // 交換p和q的指針
shares_ptr獨有的操作:
make_shared<T>(args); // 傳回一個shared_ptr,指向一個動态配置設定的類型為T的對象,是用args初始化此對象;
shared_ptr<T>p(q); // p是qshared_ptr q的拷貝,此操作會遞增q中的計數器,q中國的指針必須能轉換為T*
p = q; // p和q都是shared_ptr,所儲存的指針必須能互相轉換,此操作會遞減p的引用計數,遞增q的引用計數;若p的引用計數變為,則将其管理的原記憶體釋放
p.unique(); // 若p.use_count();為,傳回true,否則傳回false
p.use_count(); // 傳回與p共享對象的智能指針數量,主要用于調試作用
示例代碼:
if(p) // p為shared_ptr,相當于if(p==nullptr)
shared_ptr<int> p1 = make_shared<int>();
// 零初始化
shared_ptr<int> p2 = make_shared<int>();
/*
p6指向一個動态配置設定的空vector<string>通常用auto定義一個對象來
儲存make_shared的結果,方式簡單,有編譯器去識别;
*/
auto p3 = make_shared<vector<string> >();
// 錯誤,對于接受指針參數的智能指針構造函數是explict,必須直接初始化形式,即等号改為括号
shared_ptr<int> p4 = new int();
shared_ptr<int> clone(int p)
{
// 錯誤,這裡要求的是普通指針隐式的轉換為shared_ptr<int>,但該構造函數是explict的
return new int(p);
// 正确的是 return shared_ptr<int>(new int(p));
}
每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數,拷貝一個shared_ptr,将它作為參數傳遞給一個函數,作為函數的傳回值,用一個shared_ptr去初始化另一個shared_ptr,這幾種情況都會遞增它所關聯的計數器;
當給shared_ptr賦予一個新值,或shared_ptr被銷毀,例如一個局部的shared_ptr離開其作用域是,計數器就會遞減;
一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的對象;
void use_factory(T args)
{
shared_ptr<Foo> p = factory(arg);
// 使用p
return p; // 傳回p時,引用計數進行了遞增操作
}
/*
p離開了作用域,但它指向的記憶體不會被釋放掉,因為上面return的時候計數加1,不為0,
不會進行相應的資源釋放回收
*/
預設情況下,一個用來初始化智能指針的普通指針必須指向動态記憶體,因為隻能指針預設使用delete釋放它所關聯的對象;
定義和改變shared_ptr的其它方法
shared_ptr<T> p(q); // p管理内置指針q所指向的對象,q必須指向new配置設定的記憶體,且能夠轉換為T*類型
shared_ptr<T> p(u); // p從unique_ptr u那裡接管了對象的所有權,将u置為空
shared_ptr<T> p(q, d); // p接管了内置指針q所指向的對象的所有權,q必須能轉換為T*類型,p将使用可調用對象d來代替delete
shared_ptr<T> p(p2, d); // p是shared_ptr p2的拷貝唯一的差別是p将用可調用兌現gd來代替delete
p.reset(); // 若p是唯一指向其對象的shared_ptr,reset會釋放此對象,若傳遞了可選的參數内置指針q,會令p指向q,否則會将p置為空,若還傳遞了參數d,将會調用d而不是delete來釋放q
p.reset(q);
p.reset(q, d);
使用我們自己的釋放操作
預設情況下,shared_ptr假定它們直線改的是動态記憶體,是以,當一個shared_ptr被銷毀時,它預設的對它管理的指針進行delete操作,對于一些資料庫或網絡的連接配接釋放,我們需要自己定義函數代替預設的delete操作,比如:
void end_connection(connection *p)
{
disconnect( *p );
}
void f(destination &d)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
}
智能指針使用建議:
1:不使用相同的内置指針值初始化(或reset)多個智能指針;
2:不delete get()傳回的指針;
3:不使用get()初始化或reset另一個智能指針;
4:如果使用get()傳回的指針,記住當最後一個對應的之恩那個指針銷毀後,對應的指針就變為無效了;
5:如果使用智能指針管理的資源不是new配置設定的記憶體,記住傳遞給它一個删除器;
3: unique_ptr
unique_str是在c++11标準中定義的用來取代auto_ptr的新的智能指針,不僅能夠代理new建立的單個對象,也能夠代理new[ ]建立出來的數組對象,這點是auto_ptr不能實作的;
一個unique_ptr“擁有”它所指向的對象,某個時刻隻能有一個unique_ptr指向一個給定對象,當unique_ptr被銷毀時,它所指向的對象也被銷毀;
unique_ptr<string> pStr(new string("Hello"));
unique_ptr<int> p1;
unique_ptr<int> p2(new int());
p1 = p2; // 錯誤,unique_ptr不支援普通指派
unique_ptr<int> p3(p2); // 錯誤,unique_ptr不支援普通拷貝
**// 由于一個unique_ptr擁有它所指向的對象,是以unique_ptr不支援普通的拷貝或指派操作,
// 但是有一個例外就是,unique_ptr可以拷貝或指派一個将要被銷毀的unique_ptr,最常見的就是從函數傳回一個unique_ptr**
unique_ptr<int> clone(int p)
{
return unique_ptr<int>(new int(p)); // 建立一個匿名對象用于傳回
}
unique_ptr的獨有操作:
unique_ptr<T> u1; // 空unique_ptr,可以指向類型為T的對象,u1會使用delete來釋放它的指針,u2會使用一個類型為D的可調用對象釋放它的指針
unique_ptr<T, D> u2;
unique_ptr<P, D> u(d); // 空unique_ptr,指向類型T的對象,用類型為D的對象d代替delete
u = nullptr; // 釋放u指向的對象,将u置為空
u.release(); // u放棄對指針的控制權,傳回指針,并将u置為空
u.reset(); // 釋放u指向的對象
u.reset(q); // 如果提供了内置指針q,令u指向這個對象,否則将u置為空
u.reset(nullptr);
// 以上方法,release或reset可以将指針的所有權從一個(非const)unique_ptr轉移到另一個unique; 比如:
unique_ptr<string> p2(p.release()); // 所有權從p1轉移給p2
unique_ptr<string> p3(new string("Hello Test"));
p2.reset(p3.release()); // reset釋放了p2原來指向的記憶體,将所有權從p3轉移到p2
*// 注意:如果不用另一個智能指針來儲存release傳回的指針,那我們的程式就要負責資源的釋放,比如:*
auto p = p2.release();
// ...
delete p; *// 對于傳回給普通的指針類型或auto,需要手工的添加delete操作*
類似shared_ptr,unique_ptr預設情況下用delete釋放它指向的對象,與shared_ptr一樣,我們可以重載一個unique_ptr中預設的删除器;
4: weak_ptr
weak_ptr是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的對象,将一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數,一旦最後一個指向對象的shared_ptr被銷毀,對象就會被釋放,即使用weak_ptr指向對象,對象也還是會被釋放,weak_ptr是一種“弱共享”對象;
注:weak_ptr是為配合shared_ptr而引入的一種智能指針,它更像是shared_ptr的助手,因為它不具有普通指針的行為,沒有重載operator*和operator->【注:這是故意不重載的,因為它不共享指針,不能操作資源,這就是稱之為weak_ptr的原因】,它的最大作用在于協助shared_ptr工作,weak_ptr的構造和析構都不會影響shared_ptr的引用計數,隻是一個靜靜的觀察者;
weak_ptr的操作:
weak_ptr<T> w; // 空weak_ptr可以指向類型為T的對象
weak_ptr<T> w(sp); // 與shared_ptr sp指向相同對象的weak_ptr,sp指向的類型必須能轉換為類型T
w = p; // p可以是一個shared_ptr或一個weak_ptr,指派後w與p共享對象
w.reset(); // 将w置為空
w.use_count(); // 與w共享對象的shared_ptr的數量
w.expired(); // 若w.use_count()為,傳回true,否則傳回false
w.lock(); // 如果expired為true,傳回一個空shared_ptr,否則傳回一個指向w的對象的shared_ptr
建立一個weak_ptr時,要用一個shared_ptr來初始化它:
auto p = make_shared<int>();
weak_ptr<int> wp(p);
建立wp不會改變p的引用計數,wp指向的對象可能被釋放掉,由于對象可能不存在,我們不能使用weak_ptr直接通路對象,而必須調用lock,該函數檢查weak_ptr指向的對象是否仍存在,如果存在,lock傳回一個指向共享對象的shared_ptr;
if(shared_ptr<int> np = wp.lock())
{
// 在if中,np與p共享對象
// ...
}
補充資料:weak_ptr用于解決循環引用和自引用對象問題 【review at 2015-10-03】
weak_ptr
1 、weak_ptr是用來解決循環引用和自引用對象
從前面的例子我們可以看出,引用計數是一種很便利的記憶體管理機制,但是有一個很大的缺點,那就是不能管理循環引用或自引用對象(例如連結清單或樹節點),為了解決這個限制,是以weak_ptr被引入到boost的智能指針庫中。
2、weak_ptr并不能單獨存在
它是與shared_ptr同時使用的,它更像是shared_ptr的助手而不是智能指針,因為它不具備普通指針的行為,沒有重載operator *和->操作符,這是特意的。這樣它就不能共享指針,不能操作資源,這正是它“弱”的原因。它最大的作用是協助shared_ptr工作,像旁觀者那樣觀察資源的使用情況。
3、 weak_ptr獲得資源的觀察權
weak_ptr可以從一個shared_ptr或另外一個weak_ptr構造,進而獲得資源的觀察權,但weak_ptr并沒有共享資源,它的構造并不會引起引用計數的增加,同時它的析構也不會引起引用計數的減少,它僅僅是觀察者。
4、 weak_ptr可以被用于标準容器庫中的元素
weak_ptr實作了拷貝構造函數和重載了指派操作符,是以weak_ptr可以被用于标準容器庫中的元素,例如:在一個樹節點中聲明子樹節點std::vector
weak_ptr常見用途:
結合
shared_ptr
使用的特例智能指針。
weak_ptr
提供對一個或多個
shared_ptr
執行個體所屬對象的通路,但是,不參與引用計數。 如果您想要觀察對象但不需要其保持活動狀态,請使用該執行個體。 在某些情況下需要斷開
shared_ptr
執行個體間的循環引用。 頭檔案:
<memory>
。
weak_ptr
的用法如下:
weak_ptr
用于配合
shared_ptr
使用,并不影響動态對象的生命周期,即其存在與否并不影響對象的引用計數器。
weak_ptr
并沒有重載
operator->
和
operator *
操作符,是以不可直接通過
weak_ptr
使用對象。提供了
expired()
與
lock()
成員函數,前者用于判斷
weak_ptr
指向的對象是否已被銷毀,後者傳回其所指對象的
shared_ptr
智能指針(對象銷毀時傳回”空“
shared_ptr
)。循環引用的場景:如二叉樹中父節點與子節點的循環引用,容器與元素之間的循環引用等。
智能指針的循環引用
循環引用:
“循環引用”簡單來說就是:兩個對象互相使用一個
shared_ptr
成員變量指向對方的會造成循環引用。導緻引用計數失效。下面給段代碼來說明循環引用:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:// 為了省去一些步驟這裡 資料成員也聲明為public
//weak_ptr<B> pb;
shared_ptr<B> pb;
void doSomthing()
{
// if(pb.lock())
// {
//
// }
}
~A()
{
cout << "kill A\n";
}
};
class B
{
public:
//weak_ptr<A> pa;
shared_ptr<A> pa;
~B()
{
cout <<"kill B\n";
}
};
int main(int argc, char** argv)
{
shared_ptr<A> sa(new A());
shared_ptr<B> sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
cout<<"sa use count:"<<sa.use_count()<<endl;
return ;
}
上面的代碼運作結果為:
sa use count:2
, 注意此時sa,sb都沒有釋放,産生了記憶體洩露問題!!!
即A内部有指向B,B内部有指向A,這樣對于A,B必定是在A析構後B才析構,對于B,A必定是在B析構後才析構A,這就是循環引用問題,違反正常,導緻記憶體洩露。
解決循環引用有下面有三種可行的方法:
一般來講,解除這種循環引用有下面有三種可行的方法( 參考 ):
1 . 當隻剩下最後一個引用的時候需要手動打破循環引用釋放對象。
2 . 當A的生存期超過B的生存期的時候,B改為使用一個普通指針指向A。
3 . 使用弱引用的智能指針打破這種循環引用。
雖然這三種方法都可行,但方法1和方法2都需要程式員手動控制,麻煩且容易出錯。我們一般使用第三種方法:弱引用的智能指針
weak_ptr
。
強引用和弱引用
一個強引用當被引用的對象活着的話,這個引用也存在(就是說,當至少有一個強引用,那麼這個對象就不能被釋放)。
share_ptr
就是強引用。相對而言,弱引用當引用的對象活着的時候不一定存在。僅僅是當它存在的時候的一個引用。弱引用并不修改該對象的引用計數,這意味這弱引用它并不對對象的記憶體進行管理,在功能上類似于普通指針,然而一個比較大的差別是,弱引用能檢測到所管理的對象是否已經被釋放,進而避免通路非法記憶體。
使用
weak_ptr
來打破循環引用
代碼如下:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:// 為了省去一些步驟這裡 資料成員也聲明為public
weak_ptr<B> pb;
//shared_ptr<B> pb;
void doSomthing()
{
if(pb.lock())
{
}
}
~A()
{
cout << "kill A\n";
}
};
class B
{
public:
//weak_ptr<A> pa;
shared_ptr<A> pa;
~B()
{
cout <<"kill B\n";
}
};
int main(int argc, char** argv)
{
shared_ptr<A> sa(new A());
shared_ptr<B> sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
cout<<"sb use count:"<<sb.use_count()<<endl;
return ;
}
參考資料:
http://www.tuicool.com/articles/6j2yy2z
《C++ 标準程式庫》 侯捷 孟岩 譯
《C++ Primer》(第5版) 王剛 楊巨峰 譯
《Boost程式庫完全開發指南》(第3版) 羅劍鋒 著