c++11總結12——四個智慧指標:shared_ptr、uinque_ptr、weak_ptr和auto_ptr
前言
智慧指標的作用是管理一個指標。申請的空間在函式結束時忘記釋放,就會造成記憶體洩漏。使用智慧指標可以很大程度上避免這個問題,因為智慧指標是一個類,當超出類的作用域時,類會自動呼叫解構函式來釋放資源。所以智慧指標的原理是在函式結束時自動釋放記憶體空間,而不需要手動釋放記憶體空間。
1. unique_ptr
1.1 獨享管理物件所有權
unique_ptr獨享被管理物件指標的所有權。
unique_ptr物件包裝一個原始指標,並負責其生命週期。當該物件被銷燬時,會在其解構函式中刪除關聯的原始指標。
一個錯誤的例子(不允許直接複製):
std::unique_ptr<string> p1(new string("c++")); std::unique_ptr<string> p2; p2 = p1; //error
一個正確的例子:
class A { public: A(int id) :mId(id) { cout << "A::A" << endl; } ~A() { cout << "~A::A" << endl; } int getId() const { return mId; } private: int mId; }; int main() { { std::unique_ptr<A> pA(new A(1)); cout << pA->getId() << endl; } system("pause"); return 0; }
列印如下:
std::unique<A>的物件pA接收原始指標作為引數。當出作用域(正常退出或異常退出)時,在pA的解構函式中,呼叫類A(原始指標)的解構函式,刪除關聯的原始指標,這樣就不用手動刪除原始指標了。
1.2 建立物件——make_unique
auto pA = std::make_unique<A>(1);
1.3 獲取被管理物件的原始指標
A* a1 = pA.get();
1.4 重置unique_ptr物件
呼叫reset()函式,將釋放相關聯的原始指標並使unique_ptr物件為空
std::unique_ptr<A> pA = std::make_unique<A>(1); //A* a1 = pA.get(); pA.reset(); if (nullptr == pA) { cout << "pA is NULL" << endl; }
1.5 轉移unique_ptr的控制權
std::unique_ptr<A> pA = std::make_unique<A>(1);
std::unique_ptr<A> pB = std::move(pA);
if (nullptr == pA)
cout << "pA is NULL" << endl;
if(nullptr != pB)
cout << "pB is not NULL" << endl;
cout << "pB->getId() is " << pB->getId() << endl;
利用std::move將pA的控制權轉移給pB,轉移後pA的指標為空。
1.6 釋放關聯的原始指標
在 unique_ptr 物件上呼叫release()
將釋放其關聯的原始指標的所有權,並返回原始指標。這裡是釋放所有權,並沒有delete原始指標,reset()
會delete原始指標。
std::unique_ptr<A> pA = std::make_unique<A>(1);
A* a1 = pA.release();
if (nullptr == pA)
{
cout << "pA is NULL" << endl;
}
2. shared_ptr
shared_ptr是一個標準的共享所有權的智慧指標,允許多個指標指向同一個物件。利用引用計數的方式實現了對所管理的物件的所有權的分享,即允許多個shared_ptr共同管理同一個物件。
每個shared_ptr物件在內部指向兩個記憶體位置:
1)指向物件的指標;
2)用於控制引用計數資料的指標;
共享所有權如何在參考計數的幫助下工作:
1)當新的 shared_ptr 物件與指標關聯時,則在其建構函式中,將與此指標關聯的引用計數增加1。
2)當任何 shared_ptr 物件超出作用域時,則在其解構函式中,它將關聯指標的引用計數減1。如果引用計數變為0,則表示沒有其他 shared_ptr 物件與此記憶體關聯,在這種情況下,它使用delete
函式刪除該記憶體。
2.1 使用原始指標建立shared_ptr物件
方法1:
std::shared_ptr<int> p1(new int());
cout << "p1.use_count() = " << p1.use_count() << endl; //1
方法2:
auto p1 = std::make_shared<int>();
cout << "p1.use_count() = " << p1.use_count() << endl; //1
std::make_shared 一次性為int物件和用於引用計數的資料都分配了記憶體,而new操作符只是為int分配了記憶體
2.2 分離原始指標
auto p1 = std::make_shared<int>();
cout << "p1.use_count() = " << p1.use_count() << endl; //1
p1.reset();
cout << "p1.use_count() = " << p1.use_count() << endl; //0
執行reset後引用計數會減一,如果引用計數為0,則刪除指標。
auto p1 = std::make_shared<int>();
cout << "p1.use_count() = " << p1.use_count() << endl; //1
p1.reset(new int(1));
cout << "p1.use_count() = " << p1.use_count() << endl; //1
p1 = nullptr;
cout << "p1.use_count() = " << p1.use_count() << endl; //0
這種情況下,它將在內部指向新指標,因此其引用計數將再次變為1。p1置為nullptr,引用計數變為0。
2.3 自定義刪除器Deleter
1)回撥函式
shared_ptr的解構函式中刪除內部原始指標,預設呼叫的delete()函式。當我們需要析構delete[]時,需要自己設計回撥函式刪除。
class A
{
public:
A() {
cout << "A" << endl;;
}
~A() {
cout << "~A" << endl;;
}
};
void deleter(A *p)
{
cout << "DELETER FUNCTION CALLED" << endl;
delete[] p;
}
int main(){
std::shared_ptr<A> pA(new A[3], deleter);
}
列印結果:
2)使用函式物件作為刪除器
class Deleter
{
public:
void operator() (A *p) {
std::cout << "DELETER FUNCTION CALLED\n";
delete[] p;
}
};
int main()
{
std::shared_ptr<A> p3(new A[3], Deleter());
}
3)使用lamba作為刪除器(該方式較簡潔)
std::shared_ptr<A> p4(new A[3], [](A * x) {
std::cout << "DELETER FUNCTION CALLED\n";
delete[] x;
});
2.4 NULL檢測
不分配值的情況下預設為nullptr。
std::shared_ptr<int> p1;
if (nullptr == p1)
{
cout << "p1 is NULL" << endl;
}
2.5 常見問題
2.5.1不要使用同一個原始指標構造 shared_ptr
int *num = new int(10);
std::shared_ptr<int> p1(num);
std::shared_ptr<int> p2(p1); // 正確使用方法
std::shared_ptr<int> p3(num); // 不推薦
std::cout << "p1 Reference = " << p1.use_count() << std::endl; // 輸出 2
std::cout << "p2 Reference = " << p2.use_count() << std::endl; // 輸出 2
std::cout << "p3 Reference = " << p3.use_count() << std::endl; // 輸出 1
上面的程式碼實際測試會崩潰。原因在於,使用原始指標num
建立了p1,又同樣方法建立了p3,當p1超出作用域時會呼叫delete
釋放num
記憶體,此時num成了懸空指標,當p3超出作用域再次delete
的時候就可能會出錯。
2.5.2不要用棧中的指標構造 shared_ptr 物件
int x = 12;
std::shared_ptr<int> ptr(&x);
上面的程式碼在vs環境下執行會崩潰。原因在於,shared_ptr 預設的建構函式中使用的是delete
來刪除關聯的指標,所以構造的時候也必須使用new
出來的堆空間的指標。
2.5.3 建議使用make_shared()
auto p1 = make_shared<int>();
std::shared_ptr<int> p2(p1);
3. weak_ptr
3.1 一個迴圈引用的例子
class ClassB;
class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
shared_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
shared_ptr<ClassA> pa; // 在B中引用A
};
int main()
{
{
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
}
system("pause");
return 0;
}
列印結果:
3.2 weak_ptr的概念
weak_ptr是為了配合shared_ptr而引入的一種智慧指標,它指向一個由shared_ptr管理的物件而不影響所指物件的生命週期,也就是將一個weak_ptr繫結到一個shared_ptr不會改變shared_ptr的引用計數。不論是否有weak_ptr指向,一旦最後一個指向物件的shared_ptr被銷燬,物件就會被釋放
3.3 weak_ptr建立
std::shared_ptr<int> sp(new int(5));
cout << "建立前sp的引用計數:" << sp.use_count() << endl; // 1
std::weak_ptr<int> wp(sp);
cout << "建立後sp的引用計數:" << sp.use_count() << endl; // 1
3.4判斷weak_ptr指向物件是否存在
C++11中通過lock()函式判斷weak_ptr指向的物件是否存在。如果物件存在,lock()函式返回一個指向共享物件的shared_ptr,否則返回一個空shared_ptr。
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
{
std::shared_ptr<A> sp(new A());
std::weak_ptr<A> wp(sp);
sp.reset();
std::shared_ptr<A> p1 = wp.lock();
if (nullptr == p1)
{
cout << "wp指向的物件為空" << endl;
}
}
system("pause");
return 0;
}
3.5 weak_ptr的使用
class ClassB;
class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
std::weak_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
std::weak_ptr<ClassA> pa; // 在B中引用A
};
int main()
{
{
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
}
system("pause");
return 0;
}
列印結果: