智能指针解析与实现

看一些开源项目,经常发现用智能指针的地方。以前不止一位老大哥给我说过,不要随便用智能指针,用不好就是大坑。每次想用时候想起先人的话就放下了,频繁分配内存的地方都从内存池上分配。报这怀疑的态度,想一探究竟,上网找了一些资料,然后总结了一下。

1 什么是智能指针

从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。

2 智能指针有哪些

c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr, scoped_ptr(boost), 其中后三个是c++11支持,并且第一个已经被c++11弃用。

2.1 scoped_ptr

这是比较简单的一种智能指针,正如其名字所述,scoped_ptr所指向的对象在作用域之外会自动得到析构,一个例子是:此外,scoped_ptr是non-copyable的,也就是说你不能去尝试复制一个scoped_ptr的内容到另外一个scoped_ptr中,这也是为了防止错误的多次析构同一个指针所指向的对象。

2.2 shared_ptr

所实现的本质是引用计数(reference counting),也就是说shared_ptr是支持复制的,复制一个shared_ptr的本质是对这个智能指针的引用次数加1,而当这个智能指针的引用次数降低到0的时候,该对象自动被析构。

需要特别指出的是,如果shared_ptr所表征的引用关系中出现一个环,那么环上所述对象的引用次数都肯定不可能减为0那么也就不会被删除,为了解决这个问题引入了weak_ptr。

2.3 weak_ptr

对weak_ptr起的作用,很多人有自己不同的理解,我理解的weak_ptr和shared_ptr的最大区别在于weak_ptr在指向一个对象的时候不会增加其引用计数,因此你可以用weak_ptr去指向一个对象并且在weak_ptr仍然指向这个对象的时候析构它,此时你再访问weak_ptr的时候,weak_ptr其实返回的会是一个空的shared_ptr。

实际上,通常shared_ptr内部实现的时候维护的就不是一个引用计数,而是两个引用计数,一个表示strong reference,也就是用shared_ptr进行复制的时候进行的计数,一个是weak reference,也就是用weak_ptr进行复制的时候的计数。weak_ptr本身并不会增加strong reference的值,而strong reference降低到0,对象被自动析构。

为什么要采取weak_ptr来解决刚才所述的环状引用的问题呢?需要注意的是环状引用的本质矛盾是不能通过任何程序设计语言的方式来打破的,为了解决环状引用,第一步首先得打破环,也就是得告诉C++,这个环上哪一个引用是最弱的,是可以被打破的,因此在一个环上只要把原来的某一个shared_ptr改成weak_ptr,实质上这个环就可以被打破了,原有的环状引用带来的无法析构的问题也就随之得到了解决。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

.src-C++}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}

智能指针循环计数

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr\ pb_; 改为weak_ptr\ pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

3 简单实现

我们定义一个_counter类来记录引用次数,把_counter类的所有成员设定为private,因为其他的类型并不需要访问_counter,只有SmartPointer对其进行操作就行了,SmartPointer将设为其友元类。

.src-C++}
1
2
3
4
5
6
7
8
class _counter{
template<typename T> friend class SmartPointer;
_counter(int u):use(u){}
~_counter(){}
int use;
};

在SmartPointer类中,保留_counter的指针。

.src-C++}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
template<typename T>
class SmartPointer {
public:
SmartPointer(T *t): pc(new _counter(1)){
cout<<"SmartPointer copy invoked use is: " << pc->use<<endl;
this->pt = t;
}
SmartPointer(SmartPointer<T> &rhs){
this->pc = rhs.pc;
this->pt = rhs.pt;
this->pc->use++;
cout<<"SmartPointer copy invoked use is: "<< pc->use<<endl;
}
~SmartPointer(){
pc->use--;
cout<<"SmartPointer::~SmartPointer() invoded use is: "<<pc->use<<endl;
if(pc->use == 0){
delete pt;
delete pc;
}
}
SmartPointer<T>& operator=(SmartPointer<T> rhs){
if(rhs == *this){
return *this;
}
this->pt = rhs.pt;
this->pc = rhs.pc;
this->pc->use++;
cout << "SmartPointer::operator=() invoked use is " << pc->use << endl;
return *this;
}
private:
T *pt;
_counter *pc;
};

例如:我们有一个HasPtr类,其类成员中有一个为指针*p。

.src-C++}
1
2
3
4
5
6
7
8
9
10
11
12
class HasPtr {
public :
HasPtr(int val):value(val), p(new int(3)){
cout <<"HasPtr::HasPtr() invoked"<<endl;
}
~HasPtr(){delete p; cout<<"HasPtr::~HasPtr() invoded" <<endl;}
private:
int *p;
int value;
};

测试:

.src-C++}
1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char *argv[])
{
HasPtr *php = new HasPtr(3);
cout<<"---1"<<endl;
SmartPointer<HasPtr> psp(php);
cout<<"---2"<<endl;
SmartPointer<HasPtr> npsp(psp);
cout<<"---3"<<endl;
SmartPointer<HasPtr> nnpsp = npsp;
cout<<"---4"<<endl;
return 0;
}

4 如何选择智能指针

如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:

  • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
  • 两个对象包含都指向第三个对象的指针;
  • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

5 参考

/home/hujd/project/org/blog/smartpointer.html

JasonThink wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!