1. 智能指針介紹
為解決裸指針可能導(dǎo)致的內(nèi)存泄漏問題。如:
a)忘記釋放內(nèi)存;
b)程序提前退出導(dǎo)致資源釋放代碼未執(zhí)行到。
就出現(xiàn)了智能指針,能夠做到資源的自動(dòng)釋放。
2. 智能指針的原理和簡單實(shí)現(xiàn)
2.1 智能指針的原理
將裸指針封裝為一個(gè)智能指針類,需要使用該裸指針時(shí),就創(chuàng)建該類的對(duì)象;利用棧區(qū)對(duì)象出作用域會(huì)自動(dòng)析構(gòu)的特性,保證資源的自動(dòng)釋放。
2.2 智能指針的簡單實(shí)現(xiàn)
代碼示例:
template class mysmartptr { public: mysmartptr(t* ptr = nullptr):mptr(ptr) { // 創(chuàng)建該對(duì)象時(shí),裸指針會(huì)傳給對(duì)象 } ~mysmartptr() { // 對(duì)象出作用域會(huì)自動(dòng)析構(gòu),因此會(huì)釋放裸指針指向的資源 delete mptr; } // *運(yùn)算符重載 t& operator*() { // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用 return *mptr; } // ->運(yùn)算符重載 t* operator->() { // 即返回裸指針 return mptr; } private: t* mptr; }; class obj { public: void func() { cout << "obj::func" << endl; } }; void test01() { /*創(chuàng)建一個(gè)int型的裸指針, 使用mysmartptr將其封裝為智能指針對(duì)象ptr,ptr對(duì)象除了作用域就會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)。 智能指針就是利用棧上對(duì)象出作用域自動(dòng)析構(gòu)這一特性。*/ mysmartptr ptr0(new int); *ptr0 = 10; mysmartptr ptr1(new obj); ptr1->func(); (ptr1.operator->())->func(); // 等價(jià)于上面 /* 中間異常退出,智能指針也會(huì)自動(dòng)釋放資源。 if (xxx) { throw "...."; } if (yyy) { return -1; } */ }
3. 智能指針分類
3.1 問題引入
接著使用上述自己實(shí)現(xiàn)的智能指針進(jìn)行拷貝構(gòu)造:
void test02() { mysmartptr p1(new int); // p1指向一塊int型內(nèi)存空間 mysmartptr p2(p1); // p2指向p1指向的內(nèi)存空間 *p1 = 10; // 內(nèi)存空間的值為10 *p2 = 20; // 內(nèi)存空間的值被改為20 }
但運(yùn)行時(shí)出錯(cuò):
原因在于p1和p2指向同一塊int型堆區(qū)內(nèi)存空間,p2析構(gòu)將該int型空間釋放,p1再析構(gòu)時(shí)釋放同一塊內(nèi)存,則出錯(cuò)。
那可否使用如下深拷貝解決該問題?
mysmartptr(cosnt mysmartptr& src) { mptr = new t(*src.mptr); }
不可以。因?yàn)榘凑章阒羔樀氖褂梅绞?,用戶本意是想將p1和p2都指向該int型堆區(qū)內(nèi)存,使用指針p1、p2都可改變?cè)搩?nèi)存空間的值,顯然深拷貝不符合此場景。
3.2 兩類智能指針
不帶引用計(jì)數(shù)的智能指針:只能有一個(gè)指針管理資源。
auto_ptr;
scoped_ptr;
unique_ptr;.
帶引用計(jì)數(shù)的智能指針:可以有多個(gè)指針同時(shí)管理資源。
shared_ptr;強(qiáng)智能指針。
weak_ptr: 弱智能指針。這是特例,不能控制資源的生命周期,不能控制資源的自動(dòng)釋放!
3.3 不帶引用計(jì)數(shù)的智能指針
只能有一個(gè)指針管理資源。
3.3.1 auto_ptr (不推薦使用)
void test03() { auto_ptr ptr1(new int); auto_ptr ptr2(ptr1); *ptr2 = 20; // cout << *ptr2 << endl; // 可訪問*ptr2 cout << *ptr1 << endl; //訪問*ptr1卻報(bào)錯(cuò) }
如上代碼,訪問*ptr1為何報(bào)錯(cuò)?
因?yàn)檎{(diào)用auto_ptr的拷貝構(gòu)造將ptr1的值賦值給ptr2后,底層會(huì)將ptr1指向nullptr;即將同一個(gè)指針拷貝構(gòu)造多次時(shí),只讓最后一次拷貝的指針管理資源,前面的指針全指向nullptr。
不推薦將auto_ptr存入容器。
3.3.2 scoped_ptr (使用較少)
scoped_ptr已將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載delete了。
scoped_ptr(const scoped_ptr&) = delete; // 刪除拷貝構(gòu)造 scoped_ptr& operator=(const scoped_ptr&) = delete; // 刪除賦值重載
3.3.3 unique_ptr (推薦使用)
unique_ptr也已將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載delete。
unique_ptr(const unique_ptr&) = delete; // 刪除拷貝構(gòu)造 unique_ptr& operator=(const unique_ptr&) = delete; // 刪除賦值重載
但unique_ptr提供了帶右值引用參數(shù)的拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載,如下:
void test04() { unique_ptr ptr1(new int); // unique_ptr ptr2(ptr1); 和scoped_ptr一樣無法通過編譯 unique_ptr ptr2(std::move(ptr1)); // 但可使用move得到ptr1的右值類型 // *ptr1 也無法訪問 }
3.4 帶引用計(jì)數(shù)的智能指針
可以有多個(gè)指針同時(shí)管理資源。
原理:給智能指針添加其指向資源的引用計(jì)數(shù)屬性,若引用計(jì)數(shù) > 0,則不會(huì)釋放資源,若引用計(jì)數(shù) = 0就釋放資源。
具體來說:額外創(chuàng)建資源引用計(jì)數(shù)類,在智能指針類中加入該資源引用計(jì)數(shù)類的指針作為其中的一個(gè)屬性;當(dāng)使用裸指針創(chuàng)建智能指針對(duì)象時(shí),創(chuàng)建智能指針中的資源引用計(jì)數(shù)對(duì)象,并將其中的引用計(jì)數(shù)屬性初始化為1,當(dāng)后面對(duì)該智能指針對(duì)象進(jìn)行拷貝(使用其他智能指針指向該資源時(shí))或時(shí),需要在其他智能指針對(duì)象類中將被拷貝的智能指針對(duì)象中的資源引用計(jì)數(shù)類的指針獲取過來,然后將引用計(jì)數(shù)+1;當(dāng)用該智能指針給其他智能指針進(jìn)行賦值時(shí),因?yàn)槠渌悄苤羔槺毁x值后,它們就不指向原先的資源了,原先資源的引用計(jì)數(shù)就-1,直至引用計(jì)數(shù)為0時(shí)delete掉資源;當(dāng)智能指針對(duì)象析構(gòu)時(shí),會(huì)使用其中的資源引用計(jì)數(shù)指針將共享的引用計(jì)數(shù)-1,直至引用計(jì)數(shù)為0時(shí)delete掉資源。
shared_ptr:強(qiáng)智能指針;可改變資源的引用計(jì)數(shù)。
weak_ptr:弱智能指針;不可改變資源的引用計(jì)數(shù)。
帶引用計(jì)數(shù)的智能指針的簡單實(shí)現(xiàn):
/*資源的引用計(jì)數(shù)類*/ template class refcnt { public: refcnt(t* ptr=nullptr):mptr(ptr) { if (mptr != nullptr) { mcount = 1; // 剛創(chuàng)建指針指針時(shí),引用計(jì)數(shù)初始化為1 } } void addref() { // 增加引用計(jì)數(shù) mcount++; } int delref() { // 減少引用計(jì)數(shù) mcount--; return mcount; } private: t* mptr; // 資源地址 int mcount; // 資源的引用計(jì)數(shù) }; /*智能指針類*/ template class mysmartptr { public: mysmartptr(t* ptr = nullptr) :mptr(ptr) { // 創(chuàng)建該對(duì)象時(shí),裸指針會(huì)傳給對(duì)象 mprefcnt = new refcnt(mptr); } ~mysmartptr() { // 對(duì)象出作用域會(huì)自動(dòng)析構(gòu),因此會(huì)釋放裸指針指向的資源 if (0 == mprefcnt->delref()) { delete mptr; mptr = nullptr; } } // *運(yùn)算符重載 t& operator*() { // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用 return *mptr; } // ->運(yùn)算符重載 t* operator->() { // 即返回裸指針 return mptr; } // 拷貝構(gòu)造 mysmartptr(const mysmartptr& src):mptr(src.mptr),mprefcnt(src.mprefcnt) { if (mptr != nullptr) { mprefcnt->addref(); } } // 賦值重載 mysmartptr& operator=(const mysmartptr& src) { if (this == &src) // 防止自賦值 return *this; /*若本指針改為指向src管理的資源,則本指針原先指向的資源的引用計(jì)數(shù)-1, 若原資源的引用計(jì)數(shù)為0,就釋放資源*/ if (0 == mprefcnt->delref()) { delete mptr; } mptr = src.mptr; mprefcnt = src.mprefcnt; mprefcnt->addref(); return *this; } private: t* mptr; // 指向資源的指針 refcnt* mprefcnt; // 資源的引用計(jì)數(shù) };
強(qiáng)智能指針原理圖:
比如有如下創(chuàng)建強(qiáng)智能指針的語句:
shared_ptr sp1(new int(10));
則如下所示:
(a)智能指針對(duì)象sp1中主要包括ptr指針指向其管理的資源,ref指針指向該資源的引用計(jì)數(shù),則顯然會(huì)開辟兩次內(nèi)存。
(b)uses為該資源的強(qiáng)智能指針的引用計(jì)數(shù),weaks為該資源的弱智能指針的引用計(jì)數(shù)。
3.4.1 shared_ptr
強(qiáng)智能指針。可改變資源的引用計(jì)數(shù)。
(1)強(qiáng)智能指針的交叉引用問題
class b; class a { public: a() { cout << "a()" << endl; } ~a() { cout << "~a()" << endl; } shared_ptr _ptrb; }; class b { public: b() { cout << "b()" << endl; } ~b() { cout << "~b()" << endl; } shared_ptr _ptra; }; void test06() { shared_ptr pa(new a()); shared_ptr pb(new b()); pa->_ptrb = pb; pb->_ptra = pa; /*打印pa、pb指向資源的引用計(jì)數(shù)*/ cout << pa.use_count() << endl; cout << pb.use_count() << endl; }
輸出結(jié)果:
可見pa、pb指向的資源的引用計(jì)數(shù)都為2,因此出了作用域?qū)е聀a、pb指向的資源都無法釋放,如下圖所示:
解決:
建議定義對(duì)象時(shí)使用強(qiáng)智能指針,引用對(duì)象時(shí)使用弱智能指針,防止出現(xiàn)交叉引用的問題。
什么是定義對(duì)象?什么是引用對(duì)象?
定義對(duì)象:
使用new創(chuàng)建對(duì)象,并創(chuàng)建一個(gè)新的智能指針管理它。
引用對(duì)象:
使用一個(gè)已存在的智能指針來創(chuàng)建一個(gè)新的智能指針。
定義對(duì)象和引用對(duì)象的示例如下:
shared_ptr p1(new int()); // 定義智能指針對(duì)象p1 shared_ptr p2 = make_shared(10); // 定義智能指針對(duì)象p2 shared_ptr p3 = p1; // 引用智能指針p1,并使用p3來共享它 weak_ptr p4 = p2; // 引用智能指針p2,并使用p4來觀察它
如上述代碼,因?yàn)樵趖est06函數(shù)中使用pa對(duì)象的_ptrb引用pb對(duì)象,使用pb對(duì)象的_ptra引用pa對(duì)象,因此需要將a類、b類中的_ptrb和_ptra的類型改為弱智能指針weak_ptr即可,這樣就不會(huì)改變資源的引用計(jì)數(shù),能夠正確釋放資源。
3.4.2 weak_ptr
弱智能指針。不能改變資源的引用計(jì)數(shù)、不能管理對(duì)象生命周期、不能做到資源自動(dòng)釋放、不能創(chuàng)建對(duì)象,也不能訪問資源(因?yàn)閣eak_ptr未提供operator->和operator*運(yùn)算符重載),即不能通過弱智能指針調(diào)用函數(shù)、不能將其解引用。只能從一個(gè)已有的shared_ptr或weak_ptr獲得資源的弱引用。
弱智能指針weak_ptr若想用訪問資源,則需要使用lock方法將其提升為一個(gè)強(qiáng)智能指針,提升失敗則返回nullptr。(提升的情形常使用于多線程環(huán)境,避免無效的訪問,提升程序安全性)
注意:弱智能指針weak_ptr只能觀察資源的狀態(tài),但不能管理資源的生命周期,不會(huì)改變資源的引用計(jì)數(shù),不能控制資源的釋放。
weak_ptr示例:
void test07() { shared_ptr boy_sptr(new boy()); weak_ptr boy_wptr(boy_sptr); // boy_wptr->study(); 錯(cuò)誤!無法使用弱智能指針訪問資源 cout << boy_sptr.use_count() << endl; // 引用計(jì)數(shù)為1,因?yàn)槿踔悄苤羔槻桓淖円糜?jì)數(shù) shared_ptr i_sptr(new int(99)); weak_ptr i_wptr(i_sptr); // cout << *i_wptr << endl; 錯(cuò)誤!無法使用弱智能指針訪問資源 cout << i_sptr.use_count() << endl; // 引用計(jì)數(shù)為1,因?yàn)槿踔悄苤羔槻桓淖円糜?jì)數(shù) /*弱智能指針提升為強(qiáng)智能指針*/ shared_ptr boy_sptr1 = boy_wptr.lock(); if (boy_sptr1 != nullptr) { cout << boy_sptr1.use_count() << endl; // 提升成功,引用計(jì)數(shù)為2 boy_sptr1->study(); // 可以調(diào)用 } shared_ptr i_sptr1 = i_wptr.lock(); if (i_sptr1 != nullptr) { cout << i_sptr1.use_count() << endl; // 提升成功,引用計(jì)數(shù)為2 cout << *i_sptr1 << endl; // 可以輸出 } }
4. 智能指針與多線程訪問共享資源的安全問題
現(xiàn)要實(shí)現(xiàn)主線程創(chuàng)建子線程,讓子線程執(zhí)行打印hello的函數(shù),有如下兩種方式:
方式1:主線程調(diào)用test08函數(shù),在test08函數(shù)中啟動(dòng)子線程執(zhí)行線程函數(shù),如下:
void handler() { cout << "hello" << endl; } void func() { thread t1(handler); } int main(int argc, char** argv) { func(); this_thread::sleep_for(chrono::seconds(1)); system("pause"); return 0; }
運(yùn)行報(bào)錯(cuò):
方式2:主線程中直接創(chuàng)建子線程來執(zhí)行線程函數(shù),如下:
void handler() { cout << "hello" << endl; } int main(int argc, char** argv) { thread t1(handler); this_thread::sleep_for(chrono::seconds(1)); system("pause"); return 0; }
運(yùn)行結(jié)果:無報(bào)錯(cuò)
上面兩種方式都旨在通過子線程調(diào)用函數(shù)輸出hello,但為什么方式1報(bào)錯(cuò)?很簡單,不再贅述。
回歸本節(jié)標(biāo)題的正題,有如下程序:
class c { public: c() { cout << "c()" << endl; } ~c() { cout << "~c()" << endl; } void funcc() { cout << "c::funcc()" << endl; } private: }; /*子線程執(zhí)行函數(shù)*/ void threadhandler(c* c) { this_thread::sleep_for(chrono::seconds(1)); c->funcc(); } /* 主線程 */ int main(int argc, char** argv) { c* c = new c(); thread t1(threadhandler, c); delete c; t1.join(); return 0; }
運(yùn)行結(jié)果:
結(jié)果顯示c指向的對(duì)象被析構(gòu)了,但是仍然使用該被析構(gòu)的對(duì)象調(diào)用了其中的funcc函數(shù),顯然不合理。
因此在線程函數(shù)中,使用c指針訪問a對(duì)象時(shí),需要觀察a對(duì)象是否存活。
使用弱智能指針weak_ptr接收對(duì)象,訪問對(duì)象之前嘗試提升為強(qiáng)智能指針shared_ptr,提升成功則訪問,否則對(duì)象被析構(gòu)。
情形1:對(duì)象被訪問之前就被析構(gòu)了:
class c { public: c() { cout << "c()" << endl; } ~c() { cout << "~c()" << endl; } void funcc() { cout << "c::funcc()" << endl; } private: }; /*子線程執(zhí)行函數(shù)*/ void threadhandler(weak_ptr pw) { // 引用時(shí)使用弱智能指針 this_thread::sleep_for(chrono::seconds(1)); shared_ptr ps = pw.lock(); // 嘗試提升 if (ps != nullptr) { ps->funcc(); } else { cout << "對(duì)象已經(jīng)析構(gòu)!" << endl; } } /* 主線程 */ int main(int argc, char** argv) { { shared_ptr p(new c()); thread t1(threadhandler, weak_ptr(p)); t1.detach(); } this_thread::sleep_for(chrono::seconds(5)); return 0; }
運(yùn)行結(jié)果:
情形2: 對(duì)象訪問完才被析構(gòu):
class c { public: c() { cout << "c()" << endl; } ~c() { cout << "~c()" << endl; } void funcc() { cout << "c::funcc()" << endl; } private: }; /*子線程執(zhí)行函數(shù)*/ void threadhandler(weak_ptr pw) { // 引用時(shí)使用弱智能指針 this_thread::sleep_for(chrono::seconds(1)); shared_ptr ps = pw.lock(); // 嘗試提升 if (ps != nullptr) { ps->funcc(); } else { cout << "對(duì)象已經(jīng)析構(gòu)!" << endl; } } /* 主線程 */ int main(int argc, char** argv) { { shared_ptr p(new c()); thread t1(threadhandler, weak_ptr(p)); t1.detach(); this_thread::sleep_for(chrono::seconds(5)); } return 0; }
運(yùn)行結(jié)果:
可見shared_ptr與weak_ptr結(jié)合使用,能夠較好地保證多線程訪問共享資源的安全。
5.智能指針的刪除器deleter
刪除器是智能指針釋放資源的方式,默認(rèn)使用操作符delete來釋放資源。
但并非所有智能指針管理的資源都可通過delete釋放,如數(shù)組、文件資源、數(shù)據(jù)庫連接資源等。
有如下智能指針對(duì)象管理一個(gè)數(shù)組資源:
unique_ptr ptr1(new int[100]);
此時(shí)再用默認(rèn)的刪除器則會(huì)造成資源泄露,因此需要自定義刪除器。
一些為部分自定義刪除器的示例:
/* 方式1:類模板 */ template class mydeleter { public: void operator()(t* ptr) const { cout << "數(shù)組自定義刪除器1." << endl; delete[] ptr; } }; /* 方式2:函數(shù) */ void mydeleter(int* p) { cout << "數(shù)組自定義刪除器2." << endl; delete[] p; } void test09() { unique_ptr