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的用法例子:
- #include <iostream>
- #include <memory>
- class Woman;
- class Man{
- private:
- std::weak_ptr<Woman> _wife;
- //std::shared_ptr<Woman> _wife;
- public:
- void setWife(std::shared_ptr<Woman> woman){
- _wife = woman;
- }
- void doSomthing(){
- if(_wife.lock()){
- }
- }
- ~Man(){
- std::cout << "kill man\n";
- }
- };
- class Woman{
- private:
- //std::weak_ptr<Man> _husband;
- std::shared_ptr<Man> _husband;
- public:
- void setHusband(std::shared_ptr<Man> man){
- _husband = man;
- }
- ~Woman(){
- std::cout <<"kill woman\n";
- }
- };
- int main(int argc, char** argv){
- std::shared_ptr<Man> m(new Man());
- std::shared_ptr<Woman> w(new Woman());
- if(m && w) {
- m->setWife(w);
- w->setHusband(m);
- }
- return 0;
- }
在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:
假设有一个指针从B回指向A,那么这个指针应该用什么类型的指针呢?
有三中选择:
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环路。
std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题的更多相关文章
- (译+原)std::shared_ptr及std::unique_ptr使用数组
转载请注明出处: http://www.cnblogs.com/darkknightzh/p/5462363.html 参考网址: http://stackoverflow.com/questions ...
- Item 20: 使用std::weak_ptr替换会造成指针悬挂的类std::shared_ptr指针
本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 矛盾的是,我们很容易就能创造出一个和std::shared_ptr ...
- C++11 std::shared_ptr总结与使用
最近看代码,智能指针用的比较多,自己平时用的少,周末自己总结总结.方便后续使用. std::shared_ptr大概总结有以下几点: (1) 智能指针主要的用途就是方便资源的管理,自动释放没有指针引用 ...
- 深入学习c++--智能指针(二) weak_ptr(打破shared_ptr循环引用)
1. 几种智能指针 1. auto_ptr: c++11中推荐不使用他(放弃) 2. shared_ptr: 拥有共享对象所有权语义的智能指针 3. unique_ptr: 拥有独有对象所有权语义的智 ...
- Item 21: 比起直接使用new优先使用std::make_unique和std::make_shared
本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 让我们先从std::make_unique和std::make_s ...
- (转)C++——std::string类的引用计数
1.概念 Scott Meyers在<More Effective C++>中举了个例子,不知你是否还记得?在你还在上学的时候,你的父母要你不要看电视,而去复习功课,于是你把自己关在房间里 ...
- std::shared_ptr
在std::shared_ptr被引入之前,C++标准库中实现的用于管理资源的智能指针只有std::auto_ptr一个而已.std::auto_ptr的作用非常有限,因为它存在被管理资源的所有权转移 ...
- std::shared_ptr(二)
Defined in header <memory> template< class T > class shared_ptr; (since C++11) ...
- std::shared_ptr<void>的工作原理
前戏 先抛出两个问题 如果delete一个指针,但是它真实的类型和指针类型不一样会发生什么? 是谁调用了析构函数? 下面这段代码会发生什么有趣的事情? // delete_diff_type.cpp ...
随机推荐
- Codeforces Round #294 (Div. 2) A and B and Lecture Rooms(LCA 倍增)
A and B and Lecture Rooms time limit per test 2 seconds memory limit per test 256 megabytes input st ...
- You are my brother NBUT - 1218
问题描述 Little A gets to know a new friend, Little B, recently. One day, they realize that they are fam ...
- 兼容IE8
由于IE8不支持HTML5,而它又是Win7的默认浏览器,我们即使讨厌它,在这几年却也拿它没办法. 最近做了个需要兼容IE8的项目,不可避免地用了HTML5+CSS3,甚至canvas和svg,做兼容 ...
- <摘录>linux 默认的include
#include <linux/module.h> 中的module.h默认是在哪个目录下呢?我在/usr/include/linux下并没有找到这个文件. 另外想问一下,不同内核版本的l ...
- Go -- php 中的pack("H*", $string) 转换成go
pack("H*", $string) 转化成这样: //16进制字符串转[]byte func HexToByte(hex string) []byte { length := ...
- Android 卡顿优化 2 渲染优化
1.概述 2015年初google发布了Android性能优化典范,发了16个小视频供大家欣赏,当时我也将其下载,通过微信公众号给大家推送了百度云的下载地址(地址在文末,ps:欢迎大家订阅公众号),那 ...
- 关于weblogic server对docker的支持
Docker是目前比较热门的一个技术话题,WebLogic Server从12.1.3版本支持Docker,但对于操作系统和内核有一定的要求,具体参考下表 我曾在ubuntu,Oracle Enter ...
- Asp.net Core CORS(跨域资源共享)实验
环境:Asp.Net Core 2 1.问题 最近项目在调用远程UI时遇到点麻,在调用远程CSS文件时无法加载其中的字体文件.远程CSS文件对字体的定义: @font-face { font-fami ...
- linux下C++修改文件内容
C fwrite在任意位置写入文件,并可修改文件内容 想实现类似迅雷那样下载时可以从文件半中间写入的功能 #include<stdio.h> int main() { FILE *fp; ...
- 推断是否是有效的IP地址
#include<stdio.h> #include<string.h> bool isValidIp(char *s) { int len=strlen(s); int i= ...