“智能指针”的成长
智能指针是什么,他的真面目就是一个类生成的对象,这个类中包含了基本的重载->、*等一些指针形态的用法,最主要的是这个类里面有一个指针参数:所有智能指针类中都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:
template<class T>
class auto_ptr{
explicit auto_ptr(T* p = 0);
};
代码中构造函数用了explicit关键字主要是防止隐式转换,举个例子:
auto_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; //error
pd = shared_ptr<double>(p_reg); //error
shared_ptr<double> pshared = p_reg; //error
shared_ptr<double> pshared(p_reg);
上面解释了一下智能指针的本质是什么,那它有什么作用呢,看代码看不出来,但仔细想,作为一个类对象,创建时需要构造,那待生命周期结束,也是需要自动析构的,所以就带出了他的功能,就是为了保证其指向的New出来的对象,可以在生命周期结束时自动Delete,作为程序员我们都知道,Delete这个东西我们经常忘写不说,也在很多return的地方忽略了写Delete,这就导致了内存泄漏,很致命。
所以C++就提供了一种智能指针的模式去防止这种现象,现有最常用的智能指针,分别有 std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array、boost::weak_ptr。这6个智能指针都有各自的使用范畴,话不多说,研究下就知道。
⑴ std::auto_ptr
首先auto_ptr这个智能指针,属于STL,同属于STL的还有unique_ptr、shared_ptr等,感兴趣的可以去查查,但本文就不进行介绍了。先来看一段代码
class Simple {
public:
Simple(int param = 0) {
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple() {
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething() {
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
void fun()
{
std::auto_ptr<Simple> ptr (new Simple(1));
if(ptr.get())
{
ptr->PrintSomething();
ptr.get()->info_extend = "Hello";
(*prt)->info_extend = "World";
}
}
上一段代码运行的结果为:
Simple:1
PrintSometing:
~Simple:1
这是最标准的一种智能指针的使用方法,对于auto_ptr这种智能指针也是完全能驾驭的,但假如我们在代码中加入另外几句
std::auto_ptr<Simple> ptr2;
ptr2 = ptr;
pt2->PrintSomething();
pt1->PrintSomething(); // 崩溃
如果将上面的代码加到程序中就会崩溃,为什么呢?看源码中,重载=时,进行了
reset(_Right.release());
继续向里执行release的代码
可以看到,这里是将原本的指针控制权交给了别人,自身已经被置为NULL了。所以我们进行调用,不崩溃等什么呢?
然后加另一端代码:
if (ptr.get()) {
ptr.release();
}
我们可以直接调用release函数,这就很尴尬了,直接返回值为当前指向的控制权,但没有指针接收,直接丢弃了,这就更尴尬了,导致只进行了构造,没有析构。。所以真要使用release函数的话,必须这样
Simple* ptr2 = ptr.release();
delete ptr2;
这恶心的用法总给人一种多此一举的做法,所以我release函数的直接使用是没有任何意义的。对于这种手动的释放内存,直接调用
ptr.reset();
所以我们对auto_ptr进行一个总结:
1,尽量不要使用"="对智能指针进行赋值,如果使用,那先前对象就时一个空指针的。
2,release函数单独使用没有任何意义,不会释放对象,还有可能导致对象丢失。他的作用仅仅是返回所有权。
3,不要把智能指针对象当做参数传递进入函数,因为也会调用release,导致调用完函数,回来对象已经被析构。
上述3个仅仅是个人发现的,对于auto_ptr本身就有很多不合理的地方,虽然还有人使用,但具体项目中用的已经不多了,所以就在auto_ptr的基础上,增加了各种其他的智能指针,用以修复这个使用不合理的地方。
⑵ boost::scoped_ptr
scoped_ptr属于boost库,他与auto_ptr相比,也是避免了上述几个问题,主要的原因就是scoped的设计为独享所有权。主要的体现就是调用release函数和‘=’的时候是会报错的。虽然表面上的确是避免了auto_ptr中出现的问题,但种感觉是一种逃避的方法,那就有了下面的另一种智能指针。
⑶ boost::shared_ptr
scoped_ptr属于boost库,与上述的scoped_ptr相比,它添加了共享所有权的功能,也就是可以使用‘=’方法,关于做法,原理就是在类中使用了一个引用计数,当进行赋值的时候对该数字加1,写一段代码看看:
void TestSharedPtr(boost::shared_ptr<Simple> ptr)
{
std::cout << "TestSharedPtr2 UseCount: " << ptr.use_count() << std::endl;
}
boost::shared_ptr<Simple> ptr(new Simple(1));
std::cout << "TestSharedPtr2 UseCount: " << ptr.use_count() << std::endl;
{
boost::shared_ptr<Simple> ptr2 = ptr;
std::cout << "TestSharedPtr2 UseCount: " << ptr.use_count() << std::endl;
TestSharedPtr(ptr);
std::cout << "TestSharedPtr2 UseCount: " << ptr.use_count() << std::endl;
}
std::cout << "TestSharedPtr2 UseCount: " << ptr.use_count() << std::endl;
这一段代码运行下来的打印结果是:
TestSharedPtr2 UseCount: 1
TestSharedPtr2 UseCount: 2
TestSharedPtr2 UseCount: 3
TestSharedPtr2 UseCount: 2
TestSharedPtr2 UseCount: 1
大概解释一下,在开始只有一个ptr时,打印出来的引用计数为1,当另外使用一个ptr2时,再次打印出来为2,意思就是当前共有两个智能指针同时使用当前对象。进入函数后,又进行了一次拷贝构造到临时变量,所以打印值又变成3,后续退出函数和退出ptr2的生命周期,引用计数的值各剪了1,当代码执行完毕后,引用u计数值归0,调用析构函数时使用Delete释放空间。
⑷ boost::scoped_array
以数组的方式管理多个对象,但属性与scoped_ptr完全一致,独享所有权,而其使用方法也与数组没有太大的区别:
boost::scoped_array<Simple> ptr_array(new Simple[2]);
ptr_array[0].PrintSomething();
⑸ boost::shared_array
以数组的方式管理多个对象,属性与shared_ptr王权一致,共享所有权,内部使用引用计数
⑹ boost::weak_ptr
weak_ptr是一中比较特殊的智能指针,其应用的场景比较单一,主要是对shared_ptr进行观察的一种对象,也就是说用weak_ptr类型的对象对shared_ptr类型的对象进行引用,是可以正常访问,但不会改变后者的引用计数值,当后者被销毁后,前者也就不能使用了。现在说说它的应用场景,如果子类中有一个shared_ptr类型的对象,那虚基类中就可以定义一个weak_ptr类型的对象,通过访问基类的weak_ptr对象是否为空,就可以判断子类是否对自己进行赋值。
网上看了一些博客,发现weak_ptr还有一个很常用的场景,如果shared_ptr产生循环引用问题,就需要使用弱指针week与强引用指针相结合使用,什么是循环引用问题呢?看段代码:
{
shared_ptr<int> tr1(new Node());
shared_ptr<int> tr2(new Node());
tr1 -> next = tr2;
tr2 -> pre = tr1;
}
这段代码如果运行到}处,导致tr1,tr2生命周期结束,需要自己调用析构函数时,就会发生崩溃,原因是什么呢?主要就是在调用析构,发现当前引用计数为1,那就先去让别人析构,但tr2的引用计数同样为2,两个互相让对方先析构,就陷入了一个的僵局,导致最后均不释放,内存泄漏。对于这种情况解决的办法可以有好几种:
⑴ 做一个监控,当最后发现两个智能指针陷入循环阶段,手动打破僵局(定制一个定时器就可以解决)
⑵ 模仿子类或父类的析构方式,如果陷入僵局,则先析构后构造的对象,比如先析构tr2,再析构tr1
⑶ 最简单也是最常用的,使用弱引用型智能指针week_ptr,不会改变引用计数的值,当需要访问的对象已经被析构,可以通过lock()函数返回一个空的shared_ptr,所以就可以自动处理了。所以只需要将上面的代码tr2类型改为weel_ptr即可解决循环引用的问题。
本文有部分函数名和编码方式是采用网上其他博主的风格(因为觉得这种讲解方法特别清晰,先简单介绍,然后上代码直接理解),所以有些抄袭的嫌疑,但全部均本人理解,并重新编码之后写上去的,很多地方也是本人通过理解源码,写出来的部分东西,如果有误,请及时沟通修正。