C++11中智能指針的原理、使用、實現

目錄

  • 理解智能指針的原理
  • 智能指針的使用
  • 智能指針的設計和實現

1.智能指針的作用

C++程序設計中使用堆內存是非常頻繁的操作,堆內存的申請和釋放都由程序員自己管理。程序員自己管理堆內存可以提高了程序的效率,但是整體來說堆內存的管理是麻煩的,C++11中引入了智能指針的概念,方便管理堆內存。使用普通指針,容易造成堆內存洩露(忘記釋放),二次釋放,程序發生異常時內存洩露等問題等,使用智能指針能更好的管理堆內存。

理解智能指針需要從下面三個層次:

  1. 從較淺的層面看,智能指針是利用了一種叫做RAII(資源獲取即初始化)的技術對普通的指針進行封裝,這使得智能指針實質是一個對象,行為表現的卻像一個指針。
  2. 智能指針的作用是防止忘記調用delete釋放內存和程序異常的進入catch塊忘記釋放內存。另外指針的釋放時機也是非常有考究的,多次釋放同一個指針會造成程序崩潰,這些都可以通過智能指針來解決。
  3. 智能指針還有一個作用是把值語義轉換成引用語義。C++和Java有一處最大的區別在於語義不同,在Java裡面下列代碼:

Animal a = new Animal();

Animal b = a;

你當然知道,這裡其實只生成了一個對象,a和b僅僅是把持對象的引用而已。但在C++中不是這樣,

Animal a;

Animal b = a;

這裡卻是就是生成了兩個對象。

關於值語言參考這篇文章http://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html

2.智能指針的使用

智能指針在C++11版本之後提供,包含在頭文件中,shared_ptr、unique_ptr、weak_ptr

2.1 shared_ptr的使用

shared_ptr多個指針指向相同的對象。shared_ptr使用引用計數,每一個shared_ptr的拷貝都指向相同的內存。每使用他一次,內部的引用計數加1,每析構一次,內部的引用計數減1,減為0時,自動刪除所指向的堆內存。shared_ptr內部的引用計數是線程安全的,但是對象的讀取需要加鎖。

  • 初始化。智能指針是個模板類,可以指定類型,傳入指針通過構造函數初始化。也可以使用make_shared函數初始化。不能將指針直接賦值給一個智能指針,一個是類,一個是指針。例如std::shared_ptr p4 = new int(1);的寫法是錯誤的
  • 拷貝和賦值。拷貝使得對象的引用計數增加1,賦值使得原對象引用計數減1,當計數為0時,自動釋放內存。後來指向的對象引用計數加1,指向後來的對象。
  • get函數獲取原始指針
  • 注意不要用一個原始指針初始化多個shared_ptr,否則會造成二次釋放同一內存
  • 注意避免循環引用,shared_ptr的一個最大的陷阱是循環引用,循環,循環引用會導致堆內存無法正確釋放,導致內存洩漏。循環引用在weak_ptr中介紹。
#include 
#include
int main() {
{
int a = 10;
std::shared_ptr ptra = std::make_shared(a);
std::shared_ptr
ptra2(ptra); //copy
std::cout << ptra.use_count() << std::endl;
int b = 20;
int *pb = &a;
//std::shared_ptr ptrb = pb; //error
std::shared_ptr ptrb = std::make_shared(b);
ptra2 = ptrb; //assign
pb = ptrb.get(); //獲取原始指針
std::cout << ptra.use_count() << std::endl;
std::cout << ptrb.use_count() << std::endl;
}
}

2.2 unique_ptr的使用

unique_ptr“唯一”擁有其所指對象,同一時刻只能有一個unique_ptr指向給定對象(通過禁止拷貝語義、只有移動語義來實現)。相比與原始指針unique_ptr用於其RAII的特性,使得在出現異常的情況下,動態資源能得到釋放。unique_ptr指針本身的生命週期:從unique_ptr指針創建時開始,直到離開作用域。離開作用域時,若其指向對象,則將其所指對象銷燬(默認使用delete操作符,用戶可指定其他操作)。unique_ptr指針與其所指對象的關係:在智能指針生命週期內,可以改變智能指針所指對象,如創建智能指針時通過構造函數指定、通過reset方法重新指定、通過release方法釋放所有權、通過移動語義轉移所有權。

#include 
#include
int main() {
{
std::unique_ptr uptr(new int(10)); //綁定動態對象
//std::unique_ptr uptr2 = uptr; //不能賦值
//std::unique_ptr uptr2(uptr); //不能拷貝
std::unique_ptr uptr2 = std::move(uptr); //轉換所有權
uptr2.release(); //釋放所有權
}
//超過uptr的作用域,內存釋放
}

2.3 weak_ptr的使用

 weak_ptr是為了配合shared_ptr而引入的一種智能指針,因為它不具有普通指針的行為,沒有重載operator*和->,它的最大作用在於協助shared_ptr工作,像旁觀者那樣觀測資源的使用情況。weak_ptr可以從一個shared_ptr或者另一個weak_ptr對象構造,獲得資源的觀測權。但weak_ptr沒有共享資源,它的構造不會引起指針引用計數的增加。使用weak_ptr的成員函數use_count()可以觀測資源的引用計數,另一個成員函數expired()的功能等價於use_count()==0,但更快,表示被觀測的資源(也就是shared_ptr的管理的資源)已經不復存在。weak_ptr可以使用一個非常重要的成員函數lock()從被觀測的shared_ptr獲得一個可用的shared_ptr對象, 從而操作資源。但當expired()==true的時候,lock()函數將返回一個存儲空指針的shared_ptr。

#include 
#include
int main() {
{
std::shared_ptr sh_ptr = std::make_shared(10);
std::cout << sh_ptr.use_count() << std::endl;
std::weak_ptr wp(sh_ptr);
std::cout << wp.use_count() << std::endl;
if(!wp.expired()){
std::shared_ptr sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;
}
}
//delete memory
}

2.4 循環引用

考慮一個簡單的對象建模——家長與子女:a Parent has a Child, a Child knowshis/her Parent。在Java 裡邊很好寫,不用擔心內存洩漏,也不用擔心空懸指針,只要正確初始化myChild 和myParent,那麼Java 程序員就不用擔心出現訪問錯誤。一個handle 是否有效,只需要判斷其是否non null。

public class Parent

{

private Child myChild;

}

public class Child

{

private Parent myParent;

}

在C++ 裡邊就要為資源管理費一番腦筋。如果使用原始指針作為成員,Child和Parent由誰釋放?那麼如何保證指針的有效性?如何防止出現空懸指針?這些問題是C++面向對象編程麻煩的問題,現在可以藉助smart pointer把對象語義(pointer)轉變為值(value)語義,shared_ptr輕鬆解決生命週期的問題,不必擔心空懸指針。但是這個模型存在循環引用的問題,注意其中一個指針應該為weak_ptr。

原始指針的做法,容易出錯

#include 
#include
class Child;
class Parent;
class Parent {
private:
Child* myChild;
public:
void setChild(Child* ch) {
this->myChild = ch;
}
void doSomething() {
if (this->myChild) {
}
}
~Parent() {
delete myChild;

}
};
class Child {
private:
Parent* myParent;
public:
void setPartent(Parent* p) {
this->myParent = p;
}
void doSomething() {
if (this->myParent) {
}
}
~Child() {
delete myParent;
}
};
int main() {
{
Parent* p = new Parent;
Child* c = new Child;
p->setChild(c);
c->setPartent(p);
delete c; //only delete one
}
return 0;
}

循環引用內存洩露的問題

#include 
#include
class Child;
class Parent;
class Parent {
private:
std::shared_ptr ChildPtr;
public:
void setChild(std::shared_ptr child) {
this->ChildPtr = child;
}
void doSomething() {

if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr ParentPtr;
public:
void setPartent(std::shared_ptr parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr wpp;
std::weak_ptr wpc;
{
std::shared_ptr p(new Parent);
std::shared_ptr c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 2
}
std::cout << wpp.use_count() << std::endl; // 1
std::cout << wpc.use_count() << std::endl; // 1
return 0;
}

正確的做法

#include 
#include
class Child;
class Parent;
class Parent {
private:
//std::shared_ptr ChildPtr;
std::weak_ptr ChildPtr;
public:
void setChild(std::shared_ptr child) {
this->ChildPtr = child;
}
void doSomething() {
//new shared_ptr
if (this->ChildPtr.lock()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr ParentPtr;
public:
void setPartent(std::shared_ptr parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr
wpp;
std::weak_ptr wpc;
{
std::shared_ptr p(new Parent);
std::shared_ptr c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 1
}
std::cout << wpp.use_count() << std::endl; // 0
std::cout << wpc.use_count() << std::endl; // 0
return 0;
}

3.智能指針的設計和實現

下面是一個簡單智能指針的demo。智能指針類將一個計數器與類指向的對象相關聯,引用計數跟蹤該類有多少個對象共享同一指針。每次創建類的新對象時,初始化指針並將引用計數置為1;當對象作為另一對象的副本而創建時,拷貝構造函數拷貝指針並增加與之相應的引用計數;對一個對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數為減至0,則刪除對象),並增加右操作數所指對象的引用計數;調用析構函數時,構造函數減少引用計數(如果引用計數減至0,則刪除基礎對象)。智能指針就是模擬指針動作的類。所有的智能指針都會重載 -> 和 * 操作符。智能指針還有許多其他功能,比較有用的是自動銷燬。這主要是利用棧對象的有限作用域以及臨時對象(有限作用域實現)析構函數釋放內存。

 1 #include 
2 #include
3
4 template
5 class SmartPointer {
6 private:
7 T* _ptr;
8 size_t* _count;
9 public:
10 SmartPointer(T* ptr = nullptr) :
11 _ptr(ptr) {
12 if (_ptr) {
13 _count = new size_t(1);
14 } else {
15 _count = new size_t(0);
16 }
17 }
18
19 SmartPointer(const SmartPointer& ptr) {
20 if (this != &ptr) {
21 this->_ptr = ptr._ptr;
22 this->_count = ptr._count;
23 (*this->_count)++;
24 }
25 }
26
27 SmartPointer& operator=(const SmartPointer& ptr) {
28 if (this->_ptr == ptr._ptr) {
29 return *this;
30 }
31
32 if (this->_ptr) {
33 (*this->_count)--;
34 if (this->_count == 0) {
35 delete this->_ptr;
36 delete this->_count;
37 }
38 }
39
40 this->_ptr = ptr._ptr;
41 this->_count = ptr._count;
42 (*this->_count)++;
43 return *this;
44 }
45
46 T& operator*() {

47 assert(this->_ptr == nullptr);
48 return *(this->_ptr);
49
50 }
51
52 T* operator->() {
53 assert(this->_ptr == nullptr);
54 return this->_ptr;
55 }
56
57 ~SmartPointer() {
58 (*this->_count)--;
59 if (*this->_count == 0) {
60 delete this->_ptr;
61 delete this->_count;
62 }
63 }
64
65 size_t use_count(){
66 return *this->_count;
67 }
68 };
69
70 int main() {
71 {
72 SmartPointer sp(new int(10));
73 SmartPointer sp2(sp);
74 SmartPointer sp3(new int(20));
75 sp2 = sp3;
76 std::cout << sp.use_count() << std::endl;
77 std::cout << sp3.use_count() << std::endl;
78 }
79 //delete operator
80 }

參考:

  1. 值語義:http://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html
  2. shared_ptr使用:http://www.cnblogs.com/jiayayao/archive/2016/12/03/6128877.html
  3. unique_ptr使用:http://blog.csdn.net/pi9nc/article/details/12227887
  4. weak_ptr的使用:http://blog.csdn.net/mmzsyx/article/details/8090849
  5. weak_ptr解決循環引用的問題:http://blog.csdn.net/shanno/article/details/7363480


分享到:


相關文章: