计数智能指针要点(shared_ptr)

引言:
 与java等众多支持GC的现代语言不同,C/C++将更多的内存控制权交给程序员,在保证
效率的同时,也给了很多犯错的机会。常见的内存泄露、重复释放等等。智能指针大大
减少了犯错的机会,简化代码,提高可维护性。常用的智能指针有scope_ptr(c++ 11 
unique_ptr),利用RAII特性,保证资源在作用域失效的时候被释放,也保证异常抛出时
栈回滚能够释放资源;引用计数智能指针(shared_ptr),用于某个资源被多个owner共享
,因此容易出现误删,甚至不知道该不该删。shared_ptr会保证只有最后一个owner释放
资源一次。


1.引用计数
简单来说,shared_ptr利用一个owner计数跟踪它所管理的指针,以确保当没有owner的
时候,释放指针。因此这个计数需要被共享,可勾勒出sharedptr成员如下:
伪代码:
class shared_ptr
{
   int*  m_ref; // 引用计数
   T*    m_ptr;
};
如果m_ptr不为空,m_ref应该初始化为1: m_ref = new int(1);
shared_ptr的拷贝构造函数和赋值函数应该增加资源计数:++ *m_ref;
析构函数递减计数:
if (m_ref && -- *m_ref == 0)
{
   delete m_ref;
   delete m_ptr;
}

2.引用计数的管理
从上面可以看出,sharedptr不仅管理资源,还要管理计数,且资源仅支持默认的delete
释放。不是很方便。
因此将引用计数包装一下,当计数降0时自动释放自己。
伪代码:
class CounterBase
{
   CounterBase() : m_ref(1) {}
  virtual ~CounterBase() 
  {
  }
  void Destroy()
  {
     delete this;
   }
   void DecRef()
  {
    if (-- m_ref == 0)
       Destroy();
  }
   // 其余操作略:计数的增加
   int  m_ref;
};

class shared_ptr
{
   CounterBase*  m_ref; // 引用计数,析构时候不用管它了
   T*    m_ptr;
};

3.资源的释放
上面已经说过,资源释放仅仅支持delete;抛开数组new,先考虑以下情形:
FILE*  fp = fopen(xxx);
shared_ptr<FILE>  file(fp);
显然不希望执行delete fp,而是 fclose(fp);
因此需要第二个参数传给sharedptr:deleter func object

为了便于管理资源,将m_ptr移到CounterBase类,由于释放资源的方式不确定,提供虚
函数Dispose();
class CounterBase
{
   void DecRef() 
  {
     if (-- m_ref == 0)
     { 
+      Dispose();
       Destroy();
     }
  }
 +   virtual void Dispose(); // 释放资源
 +   T* m_ptr;
};

派生一个类,该类比基类多了一个deleter对象m_deleter.
Dispose()默认实现是 delete  m_ptr;
定制实现是m_deleter(m_ptr);
对于上面的例子,我们可以传入这样的deleter:
struct CloseFile
{
void operator()(File* fp)
{
  if (fp)  fclose(fp);
}
};对于C++11你可以使用lamdba避免这个冗繁的定义。

FILE*  fp = fopen(xxx);
shared_ptr<FILE>  file(fp, CloseFile());
这样,fp就可以正常的被关闭了。

4.多线程
20年前的C++是只考虑单线程的,但现在早就不一样了。
为了保证计数的线程安全性,需要用到atomic系列函数包装。
比较简单点,用相应API替换原始的++ --操作即可。
boost文档给了一些例子,这里不分析了,如果熟悉实现原理,很容易理解为什么某些操
作不是线程安全的。

分析一下官方文档上给出的一些例子吧。
Examples:
shared_ptr<int> p(new int(42));

// Ex 1
// thread A
shared_ptr<int> p2(p);
// thread B
shared_ptr<int> p3(p);
这是安全的。因为是两个线程对计数同时执行原子递增函数,安全。

// Ex 2
// thread A
p.reset(new int(1912);
// thread B
p2.reset;
这是安全的。
尽管两个智能指针指向同一个资源,reset操作仅仅是让智能指针内部的计数指针和资源指针指向新的资源,对于老的资源,只不过是同时执行了原子递减函数,安全。

// Ex 3
// thread A
p = p3;
// thread B
p3.reset();

这是不安全的。考虑以下执行流:
thread A                    thread B
                                对内部引用计数递减,且刚好减为0;//1
对内部引用计数递增;//2   
                                 释放资源,删除引用计数(假设没有weak_ptr引用该资源)// 3
。。。等着挂吧!

// Ex 4
// thread A
p3 = p2;
// thread B
p2 goes out of scope;
原理同上,一样的是不安全行为;

// Ex 5
// thread A
p3.reset(new int(1));
// thread B
p3.reset(new int(2));
原理同上,一样的是不安全行为;


5.循环引用
考虑如下情形:

class B; // 并不需要B的完整定义。因为shared_ptr仅拥有指针成员,且不执行delete

之类的操作。

class A
{
 shared_ptr<B>  m_pb;
};

class B
{
 shared_ptr<A>  m_pa;
};

shared_ptr<A>  pa(new A);
shared_ptr<B>  pb(new B);
pa->m_pb = pb;
pb->m_pa = pa;

你会发现,pa和pb由于互相指向对方,引用计数不会归0导致资源不释放。
为此引入了一种称作weak_ptr的东西,本质来讲,它并不是智能指针,若访问资源需要
由它构造一个shared_ptr,可以认为它是对引用计数的一个计数(weak_count),不是资
源的计数(share_count)。
因此,只要将shared_ptr<A>  m_pa;改为weak_ptr<A>  m_pa;资源即可正常释放,而此
时很有可能引用计数对象还存在,只要有weak_ptr指向它;所以使用weak_ptr需要先调
用lock()获取shared_ptr,若不为空才能访问资源。

6.典型错误
struct A
{
  int a;
};

shared_ptr<A>  pa(new A);
shared_ptr<int> pint(&pa->a);
上面的代码显然是错误的;
成员a属于A对象,生命期由后者控制。
为此shared_ptr提供了一个成为alias ctor的函数,正确用法如下:
shared_ptr<int> pint(pa, &pa->a);
pint依然与pa共享资源计数,但是,pint包裹的指针不同。这大概也是为什么
shared_ptr有一个T*,计数对象也有一个T*,貌似重复,其实不然。

再引用书上的例子:
int* p = new int(0);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);
这是错误的,p将被释放两次;
正确做法是:shared_ptr<int> sp2(sp1);
另外要关注一下enable_shared_from_this的用法,弄懂了weak_ptr应该是很容易写出实
现的。shared_ptr代码量小,功能又很实用,强烈建议不熟悉的自己手写实现一遍,可
以看boost例子,但是不要去看boost的代码实现。实现完成后可以再去做个对比,有什
么差异、为什么。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值