std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题

在std::shared_ptr被引入之前,C++标准库中实现的用于管理资源的智能指针只有std::auto_ptr一个而已。std::auto_ptr的作用非常有限,因为它存在被管理资源的所有权转移问题。这导致多个std::auto_ptr类型的局部变量不能共享同一个资源,这个问题是非常严重的哦。因为,我个人觉得,智能指针内存管理要解决的根本问题是:一个堆对象(或则资源,比如文件句柄)在被多个对象引用的情况下,何时释放资源的问题。何时释放很简单,就是在最后一个引用它的对象被释放的时候释放它。关键的问题在于无法确定哪个引用它的对象是被最后释放的。std::shared_ptr确定最后一个引用它的对象何时被释放的基本想法是:对被管理的资源进行引用计数,当一个shared_ptr对象要共享这个资源的时候,该资源的引用计数加1,当这个对象生命期结束的时候,再把该引用技术减少1。这样当最后一个引用它的对象被释放的时候,资源的引用计数减少到0,此时释放该资源。下边是一个shared_ptr的用法例子:

  1. #include <iostream>
  2. #include <memory>
  3. class Woman;
  4. class Man{
  5. private:
  6. std::weak_ptr<Woman> _wife;
  7. //std::shared_ptr<Woman> _wife;
  8. public:
  9. void setWife(std::shared_ptr<Woman> woman){
  10. _wife = woman;
  11. }
  12. void doSomthing(){
  13. if(_wife.lock()){
  14. }
  15. }
  16. ~Man(){
  17. std::cout << "kill man\n";
  18. }
  19. };
  20. class Woman{
  21. private:
  22. //std::weak_ptr<Man> _husband;
  23. std::shared_ptr<Man> _husband;
  24. public:
  25. void setHusband(std::shared_ptr<Man> man){
  26. _husband = man;
  27. }
  28. ~Woman(){
  29. std::cout <<"kill woman\n";
  30. }
  31. };
  32. int main(int argc, char** argv){
  33. std::shared_ptr<Man> m(new Man());
  34. std::shared_ptr<Woman> w(new Woman());
  35. if(m && w) {
  36. m->setWife(w);
  37. w->setHusband(m);
  38. }
  39. return 0;
  40. }

在Man类内部会引用一个Woman,Woman类内部也引用一个Man。当一个man和一个woman是夫妻的时候,他们直接就存在了相互引用问题。man内部有个用于管理wife生命期的shared_ptr变量,也就是说wife必定是在husband去世之后才能去世。同样的,woman内部也有一个管理husband生命期的shared_ptr变量,也就是说husband必须在wife去世之后才能去世。这就是循环引用存在的问题:husband的生命期由wife的生命期决定,wife的生命期由husband的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。

解决std::shared_ptr循环引用问题的钥匙在weak_ptr手上。weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放。另外很自然地一个问题是:既然weak_ptr不增加资源的引用计数,那么在使用weak_ptr对象的时候,资源被突然释放了怎么办呢?呵呵,答案是你根本不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()方法。

细节:shared_ptr实现了operator bool() const方法来判断一个管理的资源是否被释放。

条款20:使用std::weak_ptr作为一个类似std::share_ptr但却能悬浮的指针

有一个矛盾,一个灵巧指针可以像std::shared_ptr (见条款 19)一样方便,但又不参与管理被指对象的所有权。换句话说,需要一个像std::shared_ptr但又不影响对象引用计数的指针。这类指针会有一个std::shared_ptr没有的问题:被指的对象有可能已经被销毁。一个良好的灵巧指针应该能处理这种情况,通过跟踪什么时候指针会悬浮,比如在被指对象不复存在的时候。这正是std::weak_ptr这类型灵巧指针所能做到的。

你可能疑惑std::weak_ptr能有什么用处,在你看了std::weak_ptr的API后可能更疑惑。它看上去根本不灵巧。std::weak_ptr不能解引用,也不能检查是否为空。这是因为std::weak_ptr不能作为一个独立的灵巧指针,它是作为std::shared_ptr的延伸。

指针生成的时刻就决定了这种关系。std::weak_ptr一般是通过std::shared_ptr来构造的。当std::shared_ptr初始化std::weak_ptr时,std::weak_ptr就指向了相同的地方,但它不改变所指对象的引用计数。

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_ptr成为悬浮指针也被称作过期。你可以直接检测,

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

但是经常期望的是检查一个std::weak_ptr是否已经过期,以及是否不能访问访问做指向的对象。这个比较难做到。因为std::weak_ptr缺乏解引用操作,没法写这样的代码。即使有,把检查和解引用分隔开来也会引起竞争冲突:在调用过期操作(expired)和解引用之间。另一个线程会重新分配或者删除指向对象的最后一个std::shared_ptr,这会引起的对象被销毁,于是你的解引用会产生未定义行为。

你所需要的是一个原子操作来检查std::weak_ptr是否过期,如果没过期则提供对所指对象的访问。可以通过从std::weak_ptr构造std::shared_ptr来实现上述操作。这个操作有两个形式,取决于假如你从std::weak_ptr来构造std::shared_ptr时std::weak_ptr已经失效你期望发生什么情况。一种形式是std::weak_ptr::lock,它返回一个std::shared_ptr。如果std::weak_ptr失效,则std::shared_ptr为空:

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

另一种形式是把std::weak_ptr作为参数来构造std::shared_ptr。这样,如果std::weak_ptr失效的话,则会抛异常:

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

可能你还是很疑惑std::weak_ptr怎样使用呢。设想一个工厂函数,基于唯一ID来创建一些指向只读对象的灵巧指针。根据条款18对工厂函数返回类型的建议,应该返回一个 std::unique_ptr:

std::unique_ptr<const Widget> loadWidget(WidgetID id);

假如loadWidget是一个昂贵的调用(比如因为涉及到文件或数据库io)而且经常会被相同的ID重复调用,一个合理的优化是写一个函数做loadWidget的工作,并且缓存结果。然而保持每一个请求过的Widget在缓存中可能会引起性能问题,所以另一个优化就是在Widget不再使用时删除之。

对这个工厂函数来说,返回一个std::unique_ptr并不是最合适的。调用者获得灵巧指针并缓存下来,调用者决定了这些对象的生存期,但是缓存也需要一个指向这些对象的指针。缓存的指针需要能够检测什么时候是悬浮的,因为工厂函数的使用者不在使用这些指针时,对象会被销毁,这样相关的cache项就会变成悬浮指针。于是缓存的指针应该是std::weak_ptr---这样可以检测到什么时候悬浮。那么这意味着工厂的返回值应该是std::shared_ptr,因为只有对象的生存期由std::shared_ptr管理时,std::weak_ptr才可以检测到何时悬浮。

这里有个快速但不好的loadWidget缓存实现方案:
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
                                          std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr
                                                // to cached object (or null
                                                // if object's not in cache)

if (!objPtr) {                              // if not in cache,
    objPtr = loadWidget(id);      // load it
    cache[id] = objPtr;               // cache it
}

return objPtr;
}

这个实现使用了C++11中的hash table容器(std::unorderer_map),尽管没有显示出WidgetID的hash计算和比较相等的函数。

fastLoadWidget 的实现忽略了缓存中的过期的std::weak_ptr会不断积累,因为相关联的Widget可能不再使用(因此会被销毁)。这个实现可以被改善,而不是花时间去深入到std::weak_ptr中去观察,让我们考虑第二种情况:观察者模式。改模式的主要部件是主题(Subjects,状态可以改变的对象)和观察者(Observers,状态改变发生后被通知的对象)。多数实现中,每个subject包含了一个数据成员,保持着指向observer的指针,这样很容易在subject发生状态改变时发通知。subject没有兴趣控制它们的observer的生命周期(不关心何时它们被销毁),但他要确认一个observer是否被销毁,这样避免去访问。一个合理的设计就是每个subject保存一个容器,容器里放着每个observer的std::weak_ptr,这样在subject在使用前就可以检查observer指针是否是悬浮的。

最后再举一个使用std::weak_ptr的例子,考虑一个数据结构,里面包含了A,B,C3个对象,A和C共享B的所有权,因此保持了一个B的std::shared_ptr:

std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题

假设有一个指针从B回指向A,那么这个指针应该用什么类型的指针呢?

std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题

有三中选择:

1.一个原始指针。 这种情况下,如果A被销毁,C依然指向B,B保存着指向A的指针,但是已经是悬浮指针了。B却检测不出来,所以B有可能去解引用这个悬浮指针,结果就是为定义的行为。

2.一个std::shared_ptr。这种情况下,A和B互相保存着一个std::shared_ptr,结果这个环路(A指向B,B指向A)组织了A和B被销毁。即使程序的其他数据已经不再访问A和B,它们两者都互相保存着对方一个引用计数。这样,A和B就内存泄漏了,实用中,程序将不可能访问到A和B,而它们的资源也将不会被重新使用。

3.一个std::weak_ptr。这将避免上述两个问题。假如A被销毁,那么B的回指指针将会悬浮,但是B可以检测到。进一步说,A和B虽然都互相指想彼此,但是B的指针不影响A的引用计数,所以当std::shared_ptr不再指向A时,并不能阻止A被销毁。

使用std::weak_ptr无疑是最好的选择。然而用std::weak_ptr来打破std::shared_ptr引起的循环并不那么常见,所以这个方法也不值一提。严格来讲层级数据结构,比如tree,孩子结点一般都只被父节点拥有,当父节点被销毁后,所有的孩子结点也都应该被销毁。这样,从父节点到子节点的链接可以用std::unique_ptr来表示,而反过来从子节点到父节点的指针可以用原始指针来实现,因为子节点的生命周期不会比父节点的更长,所以不会出现子节点去解引用一个父节点的悬浮指针的情况。

当然并非所有的基于指针的数据结构都是严格的层级关系的。比如像缓存的情况以及观察者列表的实现,使用std::weak_ptr就非常好。

从效率的角度来看,std::weak_ptr和std::shared_ptr几乎一致。它们尺寸相同,都使用了控制块(见条款19),其构造,析构,赋值都涉及了对引用计数的原子操作。你可能会吃惊,因为我在本条款开始提到了std::weak_ptr不参与引用计数的操作。其实那不是我写的,我写的是std::weak_ptr不涉及对象的共享所有权,因此不影响对象的引用计数。实际山控制块里面有第二个引用计数,std::weak_ptr操作的就是这第二个引用计数。更详细的描述见条款21。

需要记住的事情

1.使用std::weak_ptr来指向可能悬浮的std::shared_ptr一样的指针。

2.可能使用std::weak_ptr的情况包括缓存,观察模式中的观察者列表,以及防止std::shared_ptr环路。

上一篇:20140315 模板类pair的用法 2、visual 2010调整代码格式是ctrl+k+


下一篇:解决Charles Response 中文乱码