智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。
动态分配的资源,交给一个类对象去管理,当类对象生命周期结束时,自动调用析构函数释放资源。
RAII是resource acquisition is initialization的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。
这样做有两大好处
1、C++98中产生了第一个智能指针auto_ptr
2、C++boost给出了更加实用的scope_ptr和shared_ptr和weak_ptr
3、C++11引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中实现的。
内存泄漏是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* ptr = new int;
//...
cout << div() << endl;
//...
delete ptr;
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
执行上述代码时,如果用户输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func函数中申请的内存资源没有得到释放。
对于这种情况,我们可以在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* ptr = new int;
try
{
cout << div() << endl;
}
catch (...)
{
delete ptr;
throw;
}
delete ptr;
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
SmartPtr<int> sp(new int);
//...
cout << div() << endl;
//...
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。
这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
概念说明:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存,文件句柄、互斥量等等)的简单技术。
对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1); //拷贝构造
SmartPtr<int> sp3(new int);
SmartPtr<int> sp4(new int);
sp3 = sp4; //拷贝赋值
return 0;
}
原因
需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。
auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。
int main()
{
std::auto_ptr<int> ap1(new int(1));
std::auto_ptr<int> ap2(ap1);
*ap2 = 10;
//*ap1 = 20; //error
std::auto_ptr<int> ap3(new int(1));
std::auto_ptr<int> ap4(new int(2));
ap3 = ap4;
return 0;
}
但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题。
namespace zl
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//管理权转移
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void test_auto()
{
auto_ptr<int> ap1(new int(1));
auto_ptr<int> ap2(ap1);
*ap1 = 1; //管理权转移以后导致ap1悬空,不能访问(存在被拷贝对象悬空的问题)
*ap2 = 1;
}
}
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。
int main()
{
std::unique_ptr<int> up1(new int(0));
//std::unique_ptr up2(up1); //error
return 0;
}
但是防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。
namespace zl
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//C++11思路:语法直接支持
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
//防拷贝
//拷贝构造和赋值是默认成员函数,我们不写会自动生成,所以我们不需写
//C++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以再加一条,声明为私有
private:
//unique_ptr(const unique_ptr& up);
//unique_ptr& operator=(const unique_ptr& up);
private:
T* _ptr;
int* _pcount;
};
void test_unique()
{
unique_ptr<int> up1(new int(1));
unique_ptr<int> up2(up1);
}
}
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
namespace zl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int(1))
,_pmtx(new mutex)
{}
template<class D>
shared_ptr(T* ptr ,D del)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
,_del(del)
{}
~shared_ptr()
{
Release();
}
void Release()
{
_pmtx->lock();
bool deleteFlag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
//cout << "delete:" << _ptr << endl;
//delete _ptr;
//删除器进行删除
_del(_ptr);
}
delete _pcount;
deleteFlag=true;
//delete _pmtx; //如何解决?
}
_pmtx->unlock();
if (deleteFlag)
{
delete _pmtx;
}
}
void AddCount()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
AddCount();
}
//sp1=sp4
//sp1=sp1
//sp1=sp2
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (--(*_pcount) == 0)
{
/*cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;*/
Release();
}
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddCount();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
//static int* _count; //静态这种方案是不行的,因为属于所有对象,多个对象可能多个资源,每个资源应该配对一个引用计数
int* _pcount;
mutex* _pmtx;
//包装器
function<void(T*)> _del = [](T* ptr) {
cout << "lambda delete:" << ptr << endl;
delete ptr;
};
};
}
首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。
其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。
而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。
当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题。
要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例。
namespace zl
{
template<class T>
class shared_ptr
{
private:
//++引用计数
void AddRef()
{
_pmutex->lock();
(*_pcount)++;
_pmutex->unlock();
}
//--引用计数
void ReleaseRef()
{
_pmutex->lock();
bool flag = false;
if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmutex->unlock();
if (flag == true)
{
delete _pmutex;
}
}
public:
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
{}
~shared_ptr()
{
ReleaseRef();
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmutex(sp._pmutex)
{
AddRef();
}
shared_ptr& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
{
ReleaseRef(); //将管理的资源对应的引用计数--
_ptr = sp._ptr; //与sp对象一同管理它的资源
_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
_pmutex = sp._pmutex; //获取sp对象管理的资源对应的互斥锁
AddRef(); //新增一个对象来管理该资源,引用计数++
}
return *this;
}
//获取引用计数
int use_count()
{
return *_pcount;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
mutex* _pmutex; //管理的资源对应的互斥锁
};
}
说明
当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]的方式申请到的空间,或管理的是一个文件指针。
这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式进行释放,而文件指针必须通过调用fclose函数进行释放。
这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);
参数说明
当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。因此当智能指针管理的资源不是以new的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。
template<class T>
struct DelArr
{
void operator()(const T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
});
return 0;
}
namespace zl
{
//默认的删除器
template<class T>
struct Delete
{
void operator()(const T* ptr)
{
delete ptr;
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
private:
void ReleaseRef()
{
_pmutex->lock();
bool flag = false;
if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
_del(_ptr); //使用定制删除器释放资源
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmutex->unlock();
if (flag == true)
{
delete _pmutex;
}
}
//...
public:
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
, _del(del)
{}
//...
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
mutex* _pmutex; //管理的资源对应的互斥锁
D _del; //管理的资源对应的删除器
};
}
template<class T>
struct DelArr
{
void operator()(const T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
//仿函数示例
zl::shared_ptr<ListNode, DelArr<ListNode>> sp1(new ListNode[10], DelArr<ListNode>());
//lambda示例1
zl::shared_ptr<FILE, function<void(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
});
//lambda示例2
auto f = [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
};
zl::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "r"), f);
return 0;
}
struct ListNode
{
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
//...
return 0;
}
这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。
当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。
将这两个结点连接起来后,资源1当中的next成员与node2一同管理资源2,资源2中的prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。
当出了main函数的作用域后,node1和node2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1。
循环引用导致资源未被释放的原因
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。
struct ListNode
{
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//...
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。
namespace zl
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
}
1.C++98中产生了第一个智能指针auto_ptr。
2.C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。
3.C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。
4.C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。
说明:
boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。