引言:
与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
对内部引用计数递增;//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的代码实现。实现完成后可以再去做个对比,有什
么差异、为什么。