一文詳解C++ 智能指針的原理、分類及使用
1. 智能指針介紹
為解決裸指針可能導致的內存泄漏問題。如:
a)忘記釋放內存;
b)程序提前退出導致資源釋放代碼未執(zhí)行到。
就出現(xiàn)了智能指針,能夠做到資源的自動釋放。
2. 智能指針的原理和簡單實現(xiàn)
2.1 智能指針的原理
將裸指針封裝為一個智能指針類,需要使用該裸指針時,就創(chuàng)建該類的對象;利用棧區(qū)對象出作用域會自動析構的特性,保證資源的自動釋放。
2.2 智能指針的簡單實現(xiàn)
代碼示例:
template<typename T>
class MySmartPtr {
public:
MySmartPtr(T* ptr = nullptr):mptr(ptr) { // 創(chuàng)建該對象時,裸指針會傳給對象
}
~MySmartPtr() { // 對象出作用域會自動析構,因此會釋放裸指針指向的資源
delete mptr;
}
// *運算符重載
T& operator*() { // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用
return *mptr;
}
// ->運算符重載
T* operator->() { // 即返回裸指針
return mptr;
}
private:
T* mptr;
};
class Obj {
public:
void func() {
cout << "Obj::func" << endl;
}
};
void test01() {
/*創(chuàng)建一個int型的裸指針,
使用MySmartPtr將其封裝為智能指針對象ptr,ptr對象除了作用域就會自動調用析構函數(shù)。
智能指針就是利用棧上對象出作用域自動析構這一特性。*/
MySmartPtr<int> ptr0(new int);
*ptr0 = 10;
MySmartPtr<Obj> ptr1(new Obj);
ptr1->func();
(ptr1.operator->())->func(); // 等價于上面
/* 中間異常退出,智能指針也會自動釋放資源。
if (xxx) {
throw "....";
}
if (yyy) {
return -1;
}
*/
}3. 智能指針分類
3.1 問題引入
接著使用上述自己實現(xiàn)的智能指針進行拷貝構造:
void test02() {
MySmartPtr<int> p1(new int); // p1指向一塊int型內存空間
MySmartPtr<int> p2(p1); // p2指向p1指向的內存空間
*p1 = 10; // 內存空間的值為10
*p2 = 20; // 內存空間的值被改為20
}但運行時出錯:

原因在于p1和p2指向同一塊int型堆區(qū)內存空間,p2析構將該int型空間釋放,p1再析構時釋放同一塊內存,則出錯。
那可否使用如下深拷貝解決該問題?
MySmartPtr(cosnt MySmartPtr<T>& src) {
mptr = new T(*src.mptr);
}不可以。因為按照裸指針的使用方式,用戶本意是想將p1和p2都指向該int型堆區(qū)內存,使用指針p1、p2都可改變該內存空間的值,顯然深拷貝不符合此場景。
3.2 兩類智能指針
不帶引用計數(shù)的智能指針:只能有一個指針管理資源。
auto_ptr;
scoped_ptr;
unique_ptr;.
帶引用計數(shù)的智能指針:可以有多個指針同時管理資源。
shared_ptr;強智能指針。
weak_ptr: 弱智能指針。這是特例,不能控制資源的生命周期,不能控制資源的自動釋放!
3.3 不帶引用計數(shù)的智能指針
只能有一個指針管理資源。
3.3.1 auto_ptr (不推薦使用)
void test03() {
auto_ptr<int> ptr1(new int);
auto_ptr<int> ptr2(ptr1);
*ptr2 = 20;
// cout << *ptr2 << endl; // 可訪問*ptr2
cout << *ptr1 << endl; //訪問*ptr1卻報錯
}如上代碼,訪問*ptr1為何報錯?
因為調用auto_ptr的拷貝構造將ptr1的值賦值給ptr2后,底層會將ptr1指向nullptr;即將同一個指針拷貝構造多次時,只讓最后一次拷貝的指針管理資源,前面的指針全指向nullptr。
不推薦將auto_ptr存入容器。
3.3.2 scoped_ptr (使用較少)
scoped_ptr已將拷貝構造函數(shù)和賦值運算符重載delete了。
scoped_ptr(const scoped_ptr<T>&) = delete; // 刪除拷貝構造 scoped_ptr<T>& operator=(const scoped_ptr<T>&) = delete; // 刪除賦值重載
3.3.3 unique_ptr (推薦使用)
unique_ptr也已將拷貝構造函數(shù)和賦值運算符重載delete。
unique_ptr(const unique_ptr<T>&) = delete; // 刪除拷貝構造 unique_ptr<T>& operator=(const unique_ptr<T>&) = delete; // 刪除賦值重載
但unique_ptr提供了帶右值引用參數(shù)的拷貝構造函數(shù)和賦值運算符重載,如下:
void test04() {
unique_ptr<int> ptr1(new int);
// unique_ptr<int> ptr2(ptr1); 和scoped_ptr一樣無法通過編譯
unique_ptr<int> ptr2(std::move(ptr1)); // 但可使用move得到ptr1的右值類型
// *ptr1 也無法訪問
}3.4 帶引用計數(shù)的智能指針
可以有多個指針同時管理資源。
原理:給智能指針添加其指向資源的引用計數(shù)屬性,若引用計數(shù) > 0,則不會釋放資源,若引用計數(shù) = 0就釋放資源。
具體來說:額外創(chuàng)建資源引用計數(shù)類,在智能指針類中加入該資源引用計數(shù)類的指針作為其中的一個屬性;當使用裸指針創(chuàng)建智能指針對象時,創(chuàng)建智能指針中的資源引用計數(shù)對象,并將其中的引用計數(shù)屬性初始化為1,當后面對該智能指針對象進行拷貝(使用其他智能指針指向該資源時)或時,需要在其他智能指針對象類中將被拷貝的智能指針對象中的資源引用計數(shù)類的指針獲取過來,然后將引用計數(shù)+1;當用該智能指針給其他智能指針進行賦值時,因為其他智能指針被賦值后,它們就不指向原先的資源了,原先資源的引用計數(shù)就-1,直至引用計數(shù)為0時delete掉資源;當智能指針對象析構時,會使用其中的資源引用計數(shù)指針將共享的引用計數(shù)-1,直至引用計數(shù)為0時delete掉資源。
shared_ptr:強智能指針;可改變資源的引用計數(shù)。
weak_ptr:弱智能指針;不可改變資源的引用計數(shù)。
帶引用計數(shù)的智能指針的簡單實現(xiàn):
/*資源的引用計數(shù)類*/
template<typename T>
class RefCnt {
public:
RefCnt(T* ptr=nullptr):mptr(ptr) {
if (mptr != nullptr) {
mcount = 1; // 剛創(chuàng)建指針指針時,引用計數(shù)初始化為1
}
}
void addRef() { // 增加引用計數(shù)
mcount++;
}
int delRef() { // 減少引用計數(shù)
mcount--;
return mcount;
}
private:
T* mptr; // 資源地址
int mcount; // 資源的引用計數(shù)
};
/*智能指針類*/
template<typename T>
class MySmartPtr {
public:
MySmartPtr(T* ptr = nullptr) :mptr(ptr) { // 創(chuàng)建該對象時,裸指針會傳給對象
mpRefCnt = new RefCnt<T>(mptr);
}
~MySmartPtr() { // 對象出作用域會自動析構,因此會釋放裸指針指向的資源
if (0 == mpRefCnt->delRef()) {
delete mptr;
mptr = nullptr;
}
}
// *運算符重載
T& operator*() { // 提供智能指針的解引用操作,即返回它包裝的裸指針的解引用
return *mptr;
}
// ->運算符重載
T* operator->() { // 即返回裸指針
return mptr;
}
// 拷貝構造
MySmartPtr(const MySmartPtr<T>& src):mptr(src.mptr),mpRefCnt(src.mpRefCnt) {
if (mptr != nullptr) {
mpRefCnt->addRef();
}
}
// 賦值重載
MySmartPtr<T>& operator=(const MySmartPtr<T>& src) {
if (this == &src) // 防止自賦值
return *this;
/*若本指針改為指向src管理的資源,則本指針原先指向的資源的引用計數(shù)-1,
若原資源的引用計數(shù)為0,就釋放資源*/
if (0 == mpRefCnt->delRef()) {
delete mptr;
}
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
mpRefCnt->addRef();
return *this;
}
private:
T* mptr; // 指向資源的指針
RefCnt<T>* mpRefCnt; // 資源的引用計數(shù)
};強智能指針原理圖:
比如有如下創(chuàng)建強智能指針的語句:
shared_ptr<int> sp1(new int(10));
則如下所示:
(a)智能指針對象sp1中主要包括ptr指針指向其管理的資源,ref指針指向該資源的引用計數(shù),則顯然會開辟兩次內存。
(b)uses為該資源的強智能指針的引用計數(shù),weaks為該資源的弱智能指針的引用計數(shù)。

3.4.1 shared_ptr
強智能指針??筛淖冑Y源的引用計數(shù)。
(1)強智能指針的交叉引用問題
class B;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
shared_ptr<B> _ptrb;
};
class B {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
shared_ptr<A> _ptra;
};
void test06() {
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->_ptrb = pb;
pb->_ptra = pa;
/*打印pa、pb指向資源的引用計數(shù)*/
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
}輸出結果:

可見pa、pb指向的資源的引用計數(shù)都為2,因此出了作用域導致pa、pb指向的資源都無法釋放,如下圖所示:

解決:
建議定義對象時使用強智能指針,引用對象時使用弱智能指針,防止出現(xiàn)交叉引用的問題。
什么是定義對象?什么是引用對象?
定義對象:
使用new創(chuàng)建對象,并創(chuàng)建一個新的智能指針管理它。
引用對象:
使用一個已存在的智能指針來創(chuàng)建一個新的智能指針。
定義對象和引用對象的示例如下:
shared_ptr<int> p1(new int()); // 定義智能指針對象p1 shared_ptr<int> p2 = make_shared<int>(10); // 定義智能指針對象p2 shared_ptr<int> p3 = p1; // 引用智能指針p1,并使用p3來共享它 weak_ptr<int> p4 = p2; // 引用智能指針p2,并使用p4來觀察它
如上述代碼,因為在test06函數(shù)中使用pa對象的_ptrb引用pb對象,使用pb對象的_ptra引用pa對象,因此需要將A類、B類中的_ptrb和_ptra的類型改為弱智能指針weak_ptr即可,這樣就不會改變資源的引用計數(shù),能夠正確釋放資源。
3.4.2 weak_ptr
弱智能指針。不能改變資源的引用計數(shù)、不能管理對象生命周期、不能做到資源自動釋放、不能創(chuàng)建對象,也不能訪問資源(因為weak_ptr未提供operator->和operator*運算符重載),即不能通過弱智能指針調用函數(shù)、不能將其解引用。只能從一個已有的shared_ptr或weak_ptr獲得資源的弱引用。
弱智能指針weak_ptr若想用訪問資源,則需要使用lock方法將其提升為一個強智能指針,提升失敗則返回nullptr。(提升的情形常使用于多線程環(huán)境,避免無效的訪問,提升程序安全性)
注意:弱智能指針weak_ptr只能觀察資源的狀態(tài),但不能管理資源的生命周期,不會改變資源的引用計數(shù),不能控制資源的釋放。
weak_ptr示例:
void test07() {
shared_ptr<Boy> boy_sptr(new Boy());
weak_ptr<Boy> boy_wptr(boy_sptr);
// boy_wptr->study(); 錯誤!無法使用弱智能指針訪問資源
cout << boy_sptr.use_count() << endl; // 引用計數(shù)為1,因為弱智能指針不改變引用計數(shù)
shared_ptr<int> i_sptr(new int(99));
weak_ptr<int> i_wptr(i_sptr);
// cout << *i_wptr << endl; 錯誤!無法使用弱智能指針訪問資源
cout << i_sptr.use_count() << endl; // 引用計數(shù)為1,因為弱智能指針不改變引用計數(shù)
/*弱智能指針提升為強智能指針*/
shared_ptr<Boy> boy_sptr1 = boy_wptr.lock();
if (boy_sptr1 != nullptr) {
cout << boy_sptr1.use_count() << endl; // 提升成功,引用計數(shù)為2
boy_sptr1->study(); // 可以調用
}
shared_ptr<int> i_sptr1 = i_wptr.lock();
if (i_sptr1 != nullptr) {
cout << i_sptr1.use_count() << endl; // 提升成功,引用計數(shù)為2
cout << *i_sptr1 << endl; // 可以輸出
}
}4. 智能指針與多線程訪問共享資源的安全問題
現(xiàn)要實現(xiàn)主線程創(chuàng)建子線程,讓子線程執(zhí)行打印Hello的函數(shù),有如下兩種方式:
方式1:主線程調用test08函數(shù),在test08函數(shù)中啟動子線程執(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;
}運行報錯:

方式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;
}運行結果:無報錯

上面兩種方式都旨在通過子線程調用函數(shù)輸出Hello,但為什么方式1報錯?很簡單,不再贅述。
回歸本節(jié)標題的正題,有如下程序:
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;
}運行結果:

結果顯示c指向的對象被析構了,但是仍然使用該被析構的對象調用了其中的funcC函數(shù),顯然不合理。
因此在線程函數(shù)中,使用c指針訪問A對象時,需要觀察A對象是否存活。
使用弱智能指針weak_ptr接收對象,訪問對象之前嘗試提升為強智能指針shared_ptr,提升成功則訪問,否則對象被析構。
情形1:對象被訪問之前就被析構了:
class C {
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
void funcC() {
cout << "C::funcC()" << endl;
}
private:
};
/*子線程執(zhí)行函數(shù)*/
void threadHandler(weak_ptr<C> pw) { // 引用時使用弱智能指針
this_thread::sleep_for(chrono::seconds(1));
shared_ptr<C> ps = pw.lock(); // 嘗試提升
if (ps != nullptr) {
ps->funcC();
} else {
cout << "對象已經析構!" << endl;
}
}
/* 主線程 */
int main(int argc, char** argv) {
{
shared_ptr<C> p(new C());
thread t1(threadHandler, weak_ptr<C>(p));
t1.detach();
}
this_thread::sleep_for(chrono::seconds(5));
return 0;
}運行結果:

情形2: 對象訪問完才被析構:
class C {
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
void funcC() {
cout << "C::funcC()" << endl;
}
private:
};
/*子線程執(zhí)行函數(shù)*/
void threadHandler(weak_ptr<C> pw) { // 引用時使用弱智能指針
this_thread::sleep_for(chrono::seconds(1));
shared_ptr<C> ps = pw.lock(); // 嘗試提升
if (ps != nullptr) {
ps->funcC();
} else {
cout << "對象已經析構!" << endl;
}
}
/* 主線程 */
int main(int argc, char** argv) {
{
shared_ptr<C> p(new C());
thread t1(threadHandler, weak_ptr<C>(p));
t1.detach();
this_thread::sleep_for(chrono::seconds(5));
}
return 0;
}運行結果:

可見shared_ptr與weak_ptr結合使用,能夠較好地保證多線程訪問共享資源的安全。
5.智能指針的刪除器deleter
刪除器是智能指針釋放資源的方式,默認使用操作符delete來釋放資源。
但并非所有智能指針管理的資源都可通過delete釋放,如數(shù)組、文件資源、數(shù)據(jù)庫連接資源等。
有如下智能指針對象管理一個數(shù)組資源:
unique_ptr<int> ptr1(new int[100]);
此時再用默認的刪除器則會造成資源泄露,因此需要自定義刪除器。
一些為部分自定義刪除器的示例:
/* 方式1:類模板 */
template<typename T>
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<int, MyDeleter<int>> ptr1(new int[100]);
unique_ptr<int, void(*)(int*)> ptr2(new int[100], myDeleter);
/* 方式3:Lambda表達式 */
unique_ptr<int, void(*)(int*)> ptr3(new int[100], [](int* p) {
cout << "數(shù)組自定義刪除器3." << endl;
delete[] p;
});
}
void test10() {
unique_ptr<FILE, void(*)(FILE*)> ptr2(fopen("1.txt", "w"), [](FILE* f) {
cout << "文件自定義刪除器." << endl;
fclose(f);
});
}運行結果:

6. make_shared和make_unique
6.1 make_shared
下面這種方式創(chuàng)建強智能指針存在缺陷。
shared_ptr<int> sp1(new int(10));
如下圖,假設為資源開辟內存成功,但為引用計數(shù)結構開辟內存失敗,則shared_ptr創(chuàng)建sp1失敗,則不會去釋放new int(10)的資源,導致內存泄漏。

因此,建議使用make_shared的方式創(chuàng)建強智能指針:
shared_ptr<int> sp1 = make_shared<int>(10); // 或者 auto sp1 = make_shared<int>(10); // 或者 auto sp1(make_shared<int>(10)); /*若有Test類,其中有兩個int型成員變量,則make_shared創(chuàng)建該類的智能指針的方式如下*/ shared_ptr<Test> sp2 = make_shared<Test>(1, 2); // 或者 auto sp2 = make_shared<Test>(1, 2); // 或者 auto sp2(make_shared<Test>(1, 2));
如下圖,make_shared創(chuàng)建智能指針時,將資源的內存和引用計數(shù)的內存開辟在一起,因此只開辟一次內存,要么開辟成功,要么開辟失敗,不存在像上面開辟兩次時可能導致內存泄漏的問題。

因此make_shared的優(yōu)點有:
(a)內存分配效率高;
(b)降低內存泄漏風險。
但也存在缺點:
(a)目前無法自定義刪除器;
(b)管理的內存延遲釋放;
具體來說,原先的方式,無論弱引用計數(shù)weaks為多少,只要強引用計數(shù)uses為0,那塊int內存就被釋放;注意:當weaks和uses都為0時,引用計數(shù)內存才被釋放;
但由于make_shared的方式為資源內存和引用計數(shù)開辟一整塊內存,只要weaks和uses有不為0的,這一整塊內存就不會被釋放。
6.2 make_unique
同樣的,建議使用make_unique。
優(yōu)點:
(a)內存分配效率高;
(b)降低內存泄漏風險。
缺點:
(a)目前無法自定義刪除器;
以上就是一文詳解C++ 智能指針的原理、分類及使用的詳細內容,更多關于C++ 智能指針的資料請關注腳本之家其它相關文章!
相關文章
pcl1.8.0+vs2013環(huán)境配置超詳細教程
這篇文章主要介紹了pcl1.8.0+vs2013環(huán)境配置超詳細教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07
全面了解#pragma once與 #ifndef的區(qū)別
下面小編就為大家?guī)硪黄媪私?pragma once與 #ifndef的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08
C++ Log日志類輕量級支持格式化輸出變量實現(xiàn)代碼
這篇文章主要介紹了C++ Log日志類輕量級支持格式化輸出變量實現(xiàn)代碼,需要的朋友可以參考下2019-04-04

