设计思想
最近在看《C++ Primer Plus》,不得不说这本书确实非常棒,里面把C++很多的语法都讲到的很清楚。这几天看了智能指针,所以在此,简单的进行一下剖析。
智能指针,顾名思义,既其首先是指针,其次其具有智能部分。具体是什么意思呢?
- RAII(Resource Acquisition Is Initialization)
资源分配即初始化,定义一个类来封装资源的分配和释放,在构造函数完成资源
的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释 放。
我们来考虑这种情况,当我们new一块空间后,应用完后就要进行delete,但是在我们delete前,程序throw了一个异常,跳转了程序,那么可想而知,造成的后果必然是内存的泄露。
可能有人会想到,那为什么不把delete放在异常前面呢?对的,这样妥协的话,确实是可以解决现在的问题。但是,有多少人在书写代码时都会忘记释放动态空间,更何况现在还要刻意的去避免这种情况;再者,当在一个比较大的项目或工程中时,代码的情况更为复杂,这时候只要有一点点的空间没有得到释放,那么对这个工程来说,就是一场灾难。
那么这个时候智能指针派上用场了。可想而知,智能指针则能自动获取内存空间,并且在应用完毕后自动完成空间的释放,不会存在内存泄漏。那么这一点是不是和类创建的对象很相像。当创造类对象时,会自动调用构造函数来进行初始化,而当类对象失效时,则会自动的调用析构函数来进行清理工作。实质上,智能指针也正是利用了这个思想。
- 智能指针
用类对象的思想创建一个模板类(以适应不同类型的需求),而后将基本类型指针进行了一层封装,使其成为类对象指针,能够自适应开辟空间,并且在析构函数内部调用delete来完成空间的释放。
深入剖析
1. auto_ptr
最早期的智能指针当属C++98提出的auto_ptr,这个版本的智能指针相当简单,既只是简单的完成了空间释放上的问题,当期存在赋值操作的时候,则进行的是所有权权的转移,因此其存在巨大的缺陷。简单来说,既可能会造成一定程度上的内存泄漏和系统崩溃。
所有权(ownership):对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。既称此指针具有该对象的所有权。
#include <iostream>
#include<memory>
void auto_ptrTest()
{
std::auto_ptr<int> ptr1(new int(100));
std::auto_ptr<int> ptr2;
ptr2 = ptr1;
std::cout << *ptr2 << std::endl;
*ptr1 = 50;
std::cout << *ptr1 << std::endl;
}
程序运行结果:
可以很清楚的看到程序运行崩溃,这就是因为ptr1进行了所有权的转移,之后其已经成为了一个空指针,因此不能够再去进行访问。
下面来看我简单模拟实现的auto_ptr指针:
//auto_ptr
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
AutoPtr(AutoPtr& p)
:_ptr(p._ptr)
{
p._ptr=NULL;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& a)
{
if (this != &a)
{
if (_ptr)
{
delete _ptr;
}
//所有权转移
_ptr = a._ptr;
a._ptr = NULL;
}
return *this;
}
~AutoPtr()
{
if (_ptr!=NULL)
{
delete _ptr;
}
}
private:
T* _ptr;
};
2. scoped_ptr / unique_ptr
由于auto_ptr中存在的巨大缺陷,对于很多工程和项目来说都是潜在危险,所以随之就诞生了相对来说更完善的智能指针模型。
boost库中的 scoped_ptr 以及 C++11中的unique_ptr。这两个智能指针模型底层实现基本一样,因此,在这里概为一体,进行论述。
这两个智能指针人送外号:守卫指针。
”守卫“既保护,scoped_ptr/unique_ptr为了避免auto_ptr的所有权转移问题,进行了相当简单粗暴的规定,既直接规定,不允许进行拷贝构造和赋值运算等相关运算。
#include <iostream>
#include<memory>
void unique_ptrTest()
{
std::unique_ptr<int> ptr1(new int(100));
std::unique_ptr<int> ptr2;
ptr2 = ptr1;
std::unique_ptr<int> ptr3(ptr1);
}
程序运行结果:
这样一来因为不允许拷贝构造和赋值运算,所以也就压根就不会存在内存崩溃的问题。但是这样却不像指针了。
下面是我模拟实现的scoped_ptr 指针:
//scoped_ptr
template <class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~ScopedPtr()
{
if (_ptr!=NULL)
{
delete _ptr;
_ptr = NULL;
}
}
private:
//防拷贝
//1.定义为私有的
//2.只声明,不定义
ScopedPtr(const ScopedPtr<T>& p);
ScopedPtr<T> operator=(const ScopedPtr<T>& p);
private:
T* _ptr;
};
以下是测试用例:
void ScopedPtrTest()
{
ScopedPtr<int> p1(new int(2));
*p1 = 10;
cout << *p1 << endl;
ScopedPtr<double> p2(new double(1));
*p2 = 1.0;
cout << *p2 << endl;
//不可访问
//ScopedPtr<int> p3(p1);
//ScopedPtr<int> p4(new int(2));
//p4 = p1;
}
可以看到,scoped_ptr为了达到守卫效果,首先是将函数定义为私有的,这样外界无法访问,其次只声明,不定义。这样即便是在类内部业无法进行函数的调用。这样通过两方面,来达到守卫效果。
3. shared_ptr
shared_ptr 人称:共享指针。
boost库中的shared_ptr与C++11中的shared_ptr具有同样的思想,运用引用计数来进行拷贝构造和赋值运算。这样较为完美的解决了之前的问题(有关引用计数详见之间博客)。虽然这样解决了问题,但是也同样带来了麻烦,shared_ptr相比于其他两个智能指针要更为复杂,应用起来也更为麻烦。
#include <iostream>
#include<memory>
void shared_ptrTest()
{
std::shared_ptr<int> ptr1(new int(10));
std::shared_ptr<int> ptr2(ptr1);
std::shared_ptr<int> ptr3;
ptr3 = ptr2;
std::cout << "ptr1->" << *ptr1 << std::endl;
std::cout << "ptr2->" << *ptr2 << std::endl;
std::cout << "ptr3->" << *ptr3 << std::endl;
}
程序运行结果:
下面的是我简单模拟实现的shared_ptr指针:
//shared_ptr
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr=NULL)
:_ptr(ptr)
,_refcount(new int(1))
{}
SharedPtr(const SharedPtr<T>& s1)
:_ptr(s1._ptr)
,_refcount(s1._refcount)
{
(*_refcount)++;
}
SharedPtr<T>& operator=(const SharedPtr<T>& s1)
{
if (this != &s1)
{
if (--(*_refcount) == 0)
{
delete _ptr;
delete _refcount;
}
_ptr = s1._ptr;
_refcount = s1._refcount;
(*_refcount)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int UseCount()
{
return *_refcount;
}
~SharedPtr()
{
if (--(*_refcount)==0)
{
delete _ptr;
delete _refcount;
}
cout << "~SharedPtr()" << endl;
}
private:
T* _ptr;
int* _refcount;//引用计数
};
以下为测试用例:
void SharedPtrTest()
{
SharedPtr<int> s1(new int(1));
*s1 = 3;
SharedPtr<int> s2(new int(2));
*s2 = 3;
SharedPtr<int> s3(s1);
cout << *s3 << endl;
SharedPtr<int> s4;
s4 = s2;
cout << *s2 << endl;
}
我们可以看到上面的程序都运行无误,那么是不是说shared_ptr就已经完美解决了我们的问题呢?
来看代码:
#include <iostream>
#include<memory>
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
ListNode()
{
std::cout << "调用析构函数" << std::endl;
}
};
void shared_ptrTest()
{
std::shared_ptr<ListNode> p1(new ListNode);
std::shared_ptr<ListNode> p2(new ListNode);
std::cout << "p1->" << p1.use_count() << std::endl;
std::cout << "p1->" << p2.use_count() << std::endl;
p1->_next = p2;
p2->_prev = p1;
std::cout << "p1->" << p1.use_count() << std::endl;
std::cout << "p1->" << p2.use_count() << std::endl;
}
程序运行结果:
上面程序运行没有报错,没有警告,看似很成功的运行了代码。但是仔细考虑一下就会发现问题。程序结束后应该要调用析构函数的,这里怎么没有调用?shared_ptr的引用计数在析构之前应该是1呀,这里怎么变成了2?
这就是shared_ptr的一个缺陷。
这里存在的问题是循环引用。下面我通过一副简单的图来进行分析:
可以看到,由于_next和_prev的指向关系,导致p1与p2的引用计数分别加1,而当释放空间的时候,由于引用计数为2,减减不为0,所以就没有调用析构函数进行空间的清理,造成了内存的泄露。
为了解决这个问题,就需要用到另外一个智能指针。
4. weak_ptr
weak_ptr 又称弱指针。其是专为shared_ptr而生的。首先来看代码:
struct ListNode
{
int _data;
//std::shared_ptr<ListNode> _next;
//std::shared_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
std::cout << "调用析构函数" << std::endl;
}
};
void shared_ptrTest()
{
std::shared_ptr<ListNode> p1(new ListNode);
std::shared_ptr<ListNode> p2(new ListNode);
std::cout << "p1->" << p1.use_count() << std::endl;
std::cout << "p1->" << p2.use_count() << std::endl;
p1->_next = p2;
p2->_prev = p1;
std::cout << "p1->" << p1.use_count() << std::endl;
std::cout << "p1->" << p2.use_count() << std::endl;
}
程序运行结果:
可以看到,在将ListNode内部的指针更改为weak_ptr型的后,成功的调用了析构函数,完成了空间的释放。
那么weak_ptr是怎么样实现的呢?
以下是我简单模拟的weak_ptr指针:
template <class T>
class WeakPtr
{
public:
WeakPtr()
:_ptr(NULL)
{}
WeakPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
{}
WeakPtr<T>& operator=(const SharedPtr<T>& p)
{
_ptr = p._ptr;
return *this;
}
private:
T* _ptr;
};
可以看到,weak_ptr 对shared_ptr进行了引用,而且只是单纯的进行了赋值操作,并未改变引用计数。
可以分析:weak_ptr 是 shared_ptr 的观察者(Observer)对象,观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。
所以,weak_ptr的作用就是观测shared_ptr的对象,时刻掌握资源的使用情况。而其本身并未具备资源的管理权。
总结
智能指针在实际项目和工程中的应用很广泛,所以,要清楚熟悉掌握其特性,最后在此略作总结:
拒绝使用auto_ptr ,除非万不得已,因为auto_ptr 危害太大,而且企业不符合C++的编程思想。
如果对象需要进行共享的话,那么就用boost库中的scope_ptr 或者C++11的unique_ptr。
- 如果需要共享对象的话,则用shared_ptr,但是切记weak_ptr要和shared_ptr配合使用,否则会出现内存泄漏等未知情况。