目录
为什么需要智能指针
内存泄露问题
int div() {
int a, b;
cin >> a >> b;
if (b == 0) {
throw invalid_argument("除0错误"); // 非法参数
}
return a / b;
}
void f1() {
int* p = new int;
try {
cout << div() << endl;
// ...
}
//catch (exception& e) {
// delete p;
// throw e;
//}
catch (...) {
delete p; // 一样的代码
throw; // 重新抛出
}
delete p; // 一样的代码
}
int main() {
try {
f1();
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}
如果输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到 f1函数中的 catch块中执行,如果 catch块中没有释放空间会导致 f1函数中申请的内存资源没有得到释放,如果申请的资源过多,代码的冗余就会增多,且调试困难。所以为了解决这个问题就出现了智能指针。
补充:内存泄漏分类,C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak):堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具
使用智能指针解决
template<class T>
class SmartPtr { // 自己定义一个智能指针
public:
SmartPtr(T* ptr) //保存资源
:_ptr(ptr)
{}
~SmartPtr() //释放资源
{
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
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 f1() {
int* p = new int;
SmartPtr<int> sp(p);
SmartPtr< int> sp1(new int);
*sp1 = 10;
SmartPtr<pair<int,int>> sp2(new pair<int,int>);
sp2->first = 20;
sp2->second = 30;
cout << div() << endl;
//无论是函数正常结束,还是抛异常,都会导致sp对象的生命周期到了以后,调用析构函数
// 智能指针
//RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源
//RAII与智能指针的关系
//RAII是一种托管资源的思想,智能指针是依靠这种思想实现的,
//unique_lock/lock_guard 也是
}
int main() {
try {
f1();
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}
上面的代码中将申请到的内存空间交给了一个SmartPtr的对象 sp1、sp2进行管理,这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
智能指针的原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显示地释放资源
采用这种方式,对象所需的资源在其生命周期内始终保持有效。
总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为
但是这样的智能指针还不够完善,会存在智能指针对象拷贝的问题,所以C++出现了不同版本的智能指针
决智能指针对象的拷贝问题:比如上面实现的智能指针 SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃
int main() {
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2 = sp1; // 析构两次报错
}
原因:
编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp1赋值给sp2后,相当于sp1和sp2管理的都是原来sp1管理的空间,当sp1和sp2析构时就会导致这块空间被释放两次
可以采用计数器的方式解决,深拷贝不符合指针赋值的初衷。
智能指针在C++库中已有现成的可以使用,比如auto_ptr, weak_ptr, share_ptr, unique_Ptr等,这些针对上述拷贝的问题都有不同的方法解决
C++的智能指针
头文件
#include<memory>
std::auto_ptr
auto_ptr 通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了
int main(){
std::auto_ptr<int> ap1(new int);
std::auto_ptr<int> ap2 = ap1; // 管理权转移
//*ap1 = 1; // 报错
*ap2 = 1;
}
对一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,会造成对象悬空,比如上面的 ap1,继续使用这个对象程序就会直接崩溃,因此使用 auto_ptr之前必须先了解它的机制,否则程序很容易出问题。
auto_ptr是一个失败设计,很多公司也都明确规定了禁止使用auto_ptr
auto_ptr的简单模拟实现
namespace hek {
//C++98 auto_ptr
// 管理权转移 早期设计缺陷,一般公司都明令禁止使用它
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(const auto_ptr<T>& ap) {
if (this != &ap) {
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr) {
cout << "delete:" << _ptr << endl; // 打印地址
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
std::unique_ptr
unique_ptr实现原理:防拷贝
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。
int main()
{
std::unique_ptr<int> up1(new int(1));
std::unique_ptr<int> up2(up1); //error,不允许拷贝
return 0;
}
编译报错
unique_ptr简单模拟实现
namespace hek{
//C++11 unique_ptr
// 防拷贝 简单粗暴,推荐使用
//缺陷:如果有需要拷贝的场景,他就没法使用
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr == nullptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
std::shared_ptr
shared_ptr是 C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题,也就是说shared_ptr支持拷贝
- 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
- 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行–。
- 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。
通过这种引用计数的方式就能支持多个对象一起管理某个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数键减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
int main()
std::shared_ptr<int> sp1(new int);
std::shared_ptr<int> sp2(sp1);
cout << sp1.use_count() << endl; // 2
std::shared_ptr<int> sp3(new int);
std::shared_ptr<int> sp4(new int);
sp1 = sp3;
cout << sp3.use_count() << endl; // 2
return 0;
}
use_count成员函数,用于获取当前对象管理的资源对应的引用计数。
shared_ptr的模拟实现
namespace hek{
//C++11 shared_ptr
// 引用计数的共享拷贝
// 缺陷:循环引用
template<class T>
class shared_ptr {
public:
template<class T>
friend class weak_ptr;
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
//++(*_pcount);
add_ref_count();
}
// sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (this != &sp) {
//if (--(*_pcount) == 0 && _ptr) {
// if(_ptr)
// delete _ptr;
// delete _pcount;
//}
//减减引用计数,如果我是最后一个管理资源的对象,则释放资源
//if (--(*_pcount) == 0) { // --(*_pcount) 已经减减;了
// delete _pcount;
// if(_ptr)
// delete _ptr;
//}
release();
//我开始跟你一起管理资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
//++(*_pcount);
add_ref_count();
}
return *this;
}
void add_ref_count() {
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void release() {
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0 ) {
if(_ptr) // 释放一个空指针并不会报错
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmtx->unlock();
if (flag)
{
delete _pmtx;
_pmtx = nullptr;
}
}
~shared_ptr()
{
/*if (--(*_pcount) == 0) {
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pcount;
}*/
release();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
int use_count() {
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};
}
- 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
- 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
- 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
- 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数–(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
- 在析构函数中,将管理资源对应的引用计数–,如果减为0则需要将该资源释放。
- 对*和->运算符进行重载,使用shared_ptr对象具有指针一样的行为。
由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题
shared_ptr对象拷贝和析构 ++/--引用计数 是线程安全的。库的实现也安全
std::weak_ptr
shared_ptr存在一个致命的缺陷:循环引用,为了解决这个问题,产生了weak_ptr。 weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,weak_ptr 是对 shared_ptr的补充
循环引用问题
比如定义如下的结点类:在堆上新建了两个节点,并将这两个结点连接起来,最后再释放这两个节点
struct ListNode {
int val;
//ListNode* _next;
//ListNode* _prev;
std::shared_ptr<ListNode> _spnext; // 如果用指针是不会引起循环引用的,这里主要突出循环引用
std::shared_ptr<ListNode> _spprev;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main() {
//ListNode* n1 = new ListNode;
//ListNode* n2 = new ListNode;
//n1->_next = n2;
//n2->_prev = n1;
//delete n1;
//delete n2;
// 上述注释的程序是没有问题的,两个结点都能够正确释放。
// 为了防止程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点分别交给两个shared_ptr对象进行管理,
// 这时为了让连接节点时的赋值操作能够执行,就需要把ListNode类中的next和prev成员变量的类型也改为shared_ptr类型
std::shared_ptr<ListNode> spn1(new ListNode);
std::shared_ptr<ListNode> spn2(new ListNode);
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
// 循环引用
spn1->_spnext = spn2;
spn2->_spprev = spn1;
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
return 0;
}
这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用
循环引用分析:
循环引用导致资源未被释放的原因:
- 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
- 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。
std::weak_ptr解决循环引用问题
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
- weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。
struct ListNode {
int val;
//std::shared_ptr<ListNode> _spnext; // 如果用指针是不会引起循环引用的,这里主要突出循环引用
//std::shared_ptr<ListNode> _spprev;
std::shared_ptr<ListNode> spn1(new ListNode);
std::shared_ptr<ListNode> spn2(new ListNode);
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main() {
std::shared_ptr<ListNode> spn1(new ListNode);
std::shared_ptr<ListNode> spn2(new ListNode);
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
// 循环引用
spn1->_spnext = spn2; // 解决方式 使用weak_ptr,不增加引用计数
spn2->_spprev = spn1;
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
return 0;
}
通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。
weak_ptr的模拟实现
- 提供一个无参的构造函数,比如刚才new ListNode时就会调用weak_ptr的无参的构造函数。
- 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
- 支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源。
- 对*和->运算符进行重载,使weak_ptr对象具有指针一样的行为。
namespace hek{
// 严格来说weak_ptr 不是智能指针,因为他没有RAII资源管理机制
// 专门解决shared_ptr的循环引用问题
template<class T>
class weak_ptr {
public:
weak_ptr() = default;
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
_ptr = sp._ptr;
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
shared_ptr还会提供一个get函数,用于获取其管理的资源。
定制删除器
当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete
的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new
方式申请到的内存空间,智能指针管理的也可能是以new[]
的方式申请到的空间,或管理的是一个文件指针。
这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);
参数说明:
- p:需要让智能指针管理的资源。
- del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。
定制删除器的用法
class A {
public:
~A()
{
cout << "~A" << endl;
}
private:
int _a1;
int _a2;
};
template<class T>
struct DeleteArry {
void operator()(T* pa) {
cout << "delete[] pa " << endl;
delete[] pa;
}
};
template<class T>
struct Free {
void operator()(T* pa) {
cout << "free(pa)" << endl;
free(pa); // 不会调用析构
}
};
struct Fclose {
void operator()(FILE* pa) {
cout << "fclose(pa)" << endl;
fclose(pa);
}
};
int main() {
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2(new A[10], DeleteArry<A>());
shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free<A>());
shared_ptr<FILE> sp4((FILE*)fopen("test.txt", "w"), Fclose());
shared_ptr<FILE> sp4((FILE*)fopen("test.txt","w"),Fclose());
return 0;
}
定制删除器的简单实现:
要在当前模拟实现的shared_ptr的基础上支持定制删除器,可以给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。
然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。
最好在设置一个默认的删除器,如果用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源。
namespace hek
{
//默认的删除器
template<class T>
struct Delete
{
void operator()(const T* ptr)
{
delete ptr;
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
private:
void release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
_del(_ptr); //使用定制删除器释放资源
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
//void release() {
// _pmtx->lock();
// bool flag = false;
// if (--(*_pcount) == 0) {
// if (_ptr) // 释放一个空指针并不会报错
// {
// cout << "delete:" << _ptr << endl;
// delete _ptr;
// _ptr = nullptr;
// }
// delete _pcount;
// _pcount = nullptr;
// flag = true;
// }
// _pmtx->unlock();
// if (flag)
// {
// delete _pmtx;
// _pmtx = nullptr;
// }
//}
public:
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
, _del(del)
{}
// 以下代码没有变化
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
add_ref_count();
}
// sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (this != &sp) {
//我开始跟你一起管理资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
//++(*_pcount);
add_ref_count();
}
return *this;
}
void add_ref_count() {
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
~shared_ptr()
{
/*if (--(*_pcount) == 0) {
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pcount;
}*/
release();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
int use_count() {
return *_pcount;
}
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
mutex* _pmtx; //管理的资源对应的互斥锁
D _del; //管理的资源对应的删除器
};
}
这时我们模拟实现的shared_ptr就支持定制删除器了,但是使用起来没有C++标准库中的那么方便。
需要在构造shared_ptr对象时指明仿函数的类型。
可以将lambda表达式、仿函数的类型指明为一个函数包装器类型,让编译器传参时自行进行推演,
也可以先用auto接收lambda表达式,然后再用decltype来声明删除器的类型。
如下:
struct Fclose {
void operator()(FILE* pa) {
cout << "fclose(pa)" << endl;
fclose(pa);
}
};
class A {
public:
~A()
{
cout << "~A" << endl;
}
private:
int _a1;
int _a2;
};
template<class T>
struct DeleteArry {
void operator()(T* pa) {
cout << "delete[] pa " << endl;
delete[] pa;
}
};
int main(){
//lambda示例
auto f = [](FILE* ptr) {
cout << "fclose:1 " << ptr << endl;
fclose(ptr);
};
hek::shared_ptr<FILE, decltype(f)> sp6((FILE*)fopen("test.txt", "w"), f);
hek::shared_ptr<FILE, function<void(FILE*)>> sp8((FILE*)fopen("test.txt", "w"), [](FILE* ptr) {
cout << "fclose:2 " << ptr << endl;
fclose(ptr);
});
// 仿函数示例
hek::shared_ptr<FILE, function<void(FILE*)>> sp7((FILE*)fopen("test.txt", "w"), Fclose());
hek::shared_ptr<A> sp1(new A);
hek::shared_ptr<A, DeleteArry<A>> sp2(new A[10], DeleteArry<A>());
cout << typeid(Fclose()).name() << endl;
cout << typeid(f).name() << endl;
}
C++11和boost中智能指针的关系
- C++11和boost中智能指针的关系
- C++98中产生了第一个智能指针auto_ptr。
- C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。
- C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。
- C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。
说明一下:boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。