1. 程式人生 > >【c++工程實踐】智能指針

【c++工程實踐】智能指針

現在 app apt cycle cpp adr RM 設計 HA

一、智能指針簡介

a smart pointer is an abstract data type that simulates a pointer while providing added features, such as automatic memory management or bounds checking.

智能指針和普通指針的區別在於智能指針實際上是對普通指針加了一層封裝機制,這樣的一層封裝機制的目的是為了使得智能指針可以方便的管理一個對象的生命期。

在C++中,我們知道,如果使用普通指針來創建一個指向某個對象的指針,那麽在使用完這個對象之後我們需要自己刪除它,例如:

ObjectType* temp_ptr = new ObjectType();
temp_ptr->foo();
delete temp_ptr;

很多材料上都會指出說如果程序員忘記在調用完temp_ptr之後刪除temp_ptr,那麽會造成一個懸掛指針(dangling pointer),也就是說這個指針現在指向的內存區域其內容程序員無法把握和控制,也可能非常容易造成內存泄漏。

智能指針設計思想:將基本類型指針封裝為類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),並在析構函數裏編寫delete語句刪除指針指向的內存空間。

STL一共給我們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暫不討論)。 模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。 二、四種智能指針簡介 1. auto_ptr 智能指針中很重要的一個概念是所有權問題:智能指針和祼指針都統稱為指針,它們共同的目標是通過地址去代表資源,智能指針是否有該資源的所有權,是取決於使用各種智能指針的關鍵。 The auto_ptr
has semantics of strict ownership, meaning that the auto_ptr instance is the sole entity responsible for the object‘s lifetime. If an auto_ptr is copied, the source loses the reference.
 1 int main(int argc, char **argv)
 2 {
 3     int *i = new int;
 4     auto_ptr<int> x(i);
 5     auto_ptr<int
> y; 6 7 y = x; 8 9 cout << x.get() << endl; // Print NULL 10 cout << y.get() << endl; // Print non-NULL address i 11 12 return 0; 13 }

x經過assignment copy以後,所有權發生轉移,資源的所有權轉移到y,x已經變成nullptr。這時再對x進行操作,很可能發生core。

auto_ptr采用可以采用copy語義來轉移指針資源的所有權的同時將原指針置為NULL,這跟通常理解的copy行為是不一致的,而這樣的行為要有些場合下不是我們希望看到的.

auto_ptr兩個缺陷:

o 復制和賦值會改變資源的所有權,不符合人的直覺。
o 在 STL 容器中無法使用auto_ptr ,因為容器內的元素必需支持可復制(copy constructable)和可賦值(assignable)。

2、unique_ptr

unique_ptr是c++11中用於替代auto_ptr的智能指針。
1 auto_ptr<string> p1(new string ("auto") ; //#1
2 auto_ptr<string> p2;                       //#2
3 p2 = p1;                                   //#3

在語句#3中,p2接管string對象的所有權後,p1的所有權將被剝奪。前面說過,這是好事,可防止p1和p2的析構函數試圖刪同—個對象
但如果程序隨後試圖使用p1,這將是件壞事,因為p1不再指向有效的數據。

下面來看使用unique_ptr的情況:

1 unique_ptr<string> p3 (new string ("auto");   //#4
2 unique_ptr<string> p4;                       //#5
3 p4 = p3;                                      //#6

編譯器認為語句#6非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全。

本質上來說,就是unique_ptr禁用了copy,而用move替代。unique_ptr禁止左值復制,不過可以通過std::move將左值轉換為右值屬性

1 unique_ptr<string> ps1, ps2;
2 ps1 = demo("hello");
3 ps2 = move(ps1);
4 ps1 = demo("alexia");
5 cout << *ps2 << *ps1 << endl;

3、shared_ptr

unique_ptr是獨占資源的,shared_ptr可以實現共享資源。

When using unique_ptr, there can be at most one unique_ptr pointing at any one resource. When that unique_ptr is destroyed, the resource is automatically reclaimed. Because there can only be one unique_ptr to any resource, any attempt to make a copy of a unique_ptr will cause a compile-time error. For example, this code is illegal:

1 unique_ptr<T> myPtr(new T);       // Okay
2 unique_ptr<T> myOtherPtr = myPtr; // Error: Can‘t copy unique_ptr

However, unique_ptr can be moved using the new move semantics:

unique_ptr<T> myPtr(new T);                  // Okay
unique_ptr<T> myOtherPtr = std::move(myPtr); // Okay, resource now stored in myOtherPtr

Similarly, you can do something like this:

1 unique_ptr<T> MyFunction() {
2     unique_ptr<T> myPtr(/* ... */);
3 
4     /* ... */
5 
6     return myPtr;
7 }

This idiom means "I‘m returning a managed resource to you. If you don‘t explicitly capture the return value, then the resource will be cleaned up. If you do, then you now have exclusive ownership of that resource." In this way, you can think of unique_ptr as a safer, better replacement for auto_ptr.

shared_ptr, on the other hand, allows for multiple pointers to point at a given resource. When the very last shared_ptr to a resource is destroyed, the resource will be deallocated. For example, this code is perfectly legal:

1 shared_ptr<T> myPtr(new T);       // Okay
2 shared_ptr<T> myOtherPtr = myPtr; // Sure!  Now have two pointers to the resource.

Internally, shared_ptr uses reference counting to track how many pointers refer to a resource, so you need to be careful not to introduce any reference cycles.

簡單來說,shared_ptr通過引用計數refCount保證有多個ptr共同擁有資源的所有權,當所有的資源都釋放所有權refCount=0,釋放該資源。

shared_ptr的簡單實現:

 1 template <typename V>
 2 class SmartPtr {
 3 private:
 4     int * refcnt;
 5     V * v;
 6 public:
 7     SmartPtr(V* ptr): v(ptr) {
 8         refcnt = new int(1);
 9     }   
10 
11     SmartPtr(const SmartPtr& ptr) {
12         this->v = ptr.v;
13         this->refcnt = ptr.refcnt;
14         *refcnt += 1;
15     }   
16 
17     ~SmartPtr() {
18         cout << "to delete a smart pointer" << endl;
19         *refcnt -= 1;
20 
21         if (*refcnt == 0) {
22             delete v;
23            delete refcnt;
24        }
25     }
26 };
27 
28 int main() {
29     A * ptrA = new A();
30     SmartPtr<A> sp1(ptrA);
31     SmartPtr<A> sp2 = sp1;
32 
33     return 0;
34 }

這個例子中中需要註意的點是引用計數是所有管理同一個指針的智能指針所共享的,所以在這個例子中,sp1和sp2的引用計數指向的是相同的一個整數。

我們看一下這個例子的輸出:

# g++ -o smart myShare.cpp 
# ./smart 
create object of A
to delete a smart pointer
to delete a smart pointer
destroy an object A

可以看到,這個和shared_ptr一樣可以正確地delete指針。

4、weak_ptr

weak_ptr和Raw Pointer很類似,只標記指向該資源,沒有該資源的所有權。同時weak_ptr擴展了Raw Pointer的功能。

std::weak_ptrs are typically created from std::shared_ptrs. They point to the same place as the std::shared_ptrs initializ‐
ing them, but they don’t affect the reference count of the object they point to:

auto spw = // after spw is constructed,
std::make_shared<Widget>(); // the pointed-to Widget‘s
// ref count (RC) is 1. (See
// Item 21 for info on
// std::make_shared.)
…
std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget
// as spw. RC remains 1
…
spw = nullptr; // RC goes to 0, and the
// Widget is destroyed.
// wpw now dangles

std::weak_ptrs that dangle are said to have expired. You can test for this directly,

if (wpw.expired()) … // if wpw doesn‘t point
// to an object…

有兩種方法從weak_ptr獲取shared_ptr:

One form is std::weak_ptr::lock, which returns a std::shared_ptr. The std::shared_ptr is null
if the std::weak_ptr has expired:

std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw‘s expired,
// spw1 is null
auto spw2 = wpw.lock(); // same as above,
// but uses auto

The other form is the std::shared_ptr constructor taking a std::weak_ptr as an
argument. In this case, if the std::weak_ptr has expired, an exception is thrown:

std::shared_ptr<Widget> spw3(wpw); // if wpw‘s expired,
// throw std::bad_weak_ptr

As a final example of std::weak_ptr’s utility, consider a data structure with objects
A, B, and C in it, where A and C share ownership of B and therefore hold
std::shared_ptrs to it:
技術分享圖片

There are three choices:
? A raw pointer. With this approach, if A is destroyed, but C continues to point to
B, B will contain a pointer to A that will dangle. B won’t be able to detect that, so B
may inadvertently dereference the dangling pointer. That would yield undefined
behavior.
? A std::shared_ptr. In this design, A and B contain std::shared_ptrs to each
other. The resulting std::shared_ptr cycle (A points to B and B points to A) will
prevent both A and B from being destroyed. Even if A and B are unreachable from
other program data structures (e.g., because C no longer points to B), each will
have a reference count of one. If that happens, A and B will have been leaked, for
all practical purposes: it will be impossible for the program to access them, yet
their resources will never be reclaimed.
? A std::weak_ptr. This avoids both problems above. If A is destroyed, B’s
pointer back to it will dangle, but B will be able to detect that. Furthermore,
though A and B will point to one another, B’s pointer won’t affect A’s reference
count, hence can’t keep A from being destroyed when std::shared_ptrs no
longer point to it.

(符號 &(reference),表示".....的地址"("address of"),因此成為地址操作符(adress operator),又稱引用操作符(reference operator) 符號 *(dereference),表示".....所指向的值"("value pointed to by")) 三、智能指針總結

在選擇具體指針類型的時候,通過問以下幾個問題就能知道使用哪種指針了。

  • 指針是否需要擁有資源的所有權?

如果指針變量需要綁定資源的所有權,那麽會選擇unique_ptr或shared_ptr。它們可以通過RAII完成對資源生命期的自動管理。如果不需要擁有資源的所有權,那麽會選擇weak_ptr和raw pointer,這兩種指針變量在離開作用域時不會對其所指向的資源產生任何影響。

  • 如果指針擁有資源的所有權(owning pointer),那麽該指針是否需要獨占所有權?

獨占則使用unique_ptr(人無我有,人有我丟),否則使用shared_ptr(你有我有全都有)。這一點很好理解。

  • 如果不擁有資源的所有權(non-owning pointer),那麽指針變量是否需要在適當的時候感知到資源的有效性?
如果需要則使用weak_ptr,它可以在適當的時候通過weak_ptr::lock()獲得所有權,當擁有所有權後便可以得知資源的有效性。如不需要,則使用祼指針。這通常是程序員知道在祼指針的作用域內它是有效的,並且使用祼指針效率更高,例如
1 auto p = make_shared<int>(1);
2 auto result = f(p.get());

這樣會衍生出另外一個問題,為何unique_ptr不能和weak_ptr配合?
這是因為unique_ptr是獨占所有權,也就是說資源的生命期等於指針變量的生命期,那麽程序員可以很容易通過指針變量的生命期來判斷資源是否有效,這樣weak_ptr就不再有必要了。而相對來說,shared_ptr則不好判斷,特別是多線程環境下。

另外,很多人說weak_ptr的作用是可以破除循環引用,這個說法是對的,但沒有抓住本質(祼指針也可以破除,那為何要用weak_ptr?)。寫出循環引用的原因是因為程序員自己沒有理清楚資源的所有權問題。

【c++工程實踐】智能指針