【c++工程实践】智能指针

一、智能指针简介

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?)。写出循环引用的原因是因为程序员自己没有理清楚资源的所有权问题。
 

posted on 2018-07-01 17:56  ym65536  阅读(297)  评论(0编辑  收藏  举报