打怪升级:第92天 |
---|
什么是智能指针
智能指针简单来说就是将指针封装到类中,借助对象的局部作用域有效特性,在出了作用域后自动释放资源。
为什么需要使用智能指针
上一篇文章我们讲解了C++异常的概念,我们也了解到异常的捕捉可以跨好几个函数,这就会引发异常安全的问题,在上一篇文章中我们解决异常安全的方法是在每一个可能会引发异常安全的地方都加上try,catch捕捉异常,处理完安全问题后再抛出;
上面的方法固然可以解决问题,但同时也会使得函数逻辑变得复杂许多,今天我们借助类的特性来大大简化这一逻辑。
C/C++程序中一般我们关心两种方面的内存泄漏:
在linux下内存泄漏检测:linux下几款内存泄漏检测工具
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
#include
using namespace std;
void Div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division of zero");
else
cout << "a / b = " << a / b << endl;
}
void Func()
{
int* p1 = new int;
int* p2 = new int;
Div();
cout << "delete p1" << endl;
delete p1;
cout << "delete p2" << endl;
delete p2;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const char* s)
{
cout << s << endl;
}
}
return 0;
}
这段代码大家应该可以理解,由于Div函数抛出异常,直接跳转到main函数中匹配的catch子句,导致func函数中发生内存泄漏。
防止内存泄漏的改进:
#include
using namespace std;
void Div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division of zero");
else
cout << "a / b = " << a / b << endl;
}
void Func()
{
// p1 new空间时 抛异常会怎样,怎么办
// p2 new空间时 抛异常会怎样,怎么办
// Div 抛异常时怎么办
int* p1 = nullptr;
int* p2 = nullptr;
p1 = new int(10);
try{
p2 = new int(1);
}
catch (...){
cout << "delete p1" << endl;
delete p1;
throw;
}
try{
Div();
}
catch (...){
cout << "delete p1" << endl;
delete p1;
cout << "delete p2" << endl;
delete p2;
throw;
}
cout << "delete p1" << endl;
delete p1;
cout << "delete p2" << endl;
delete p2;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const char* s)
{
cout << s << endl;
}
catch (std::bad_alloc& e) // new错误 抛出的异常类型:bad_alloc
{
cout << "exception: " << e.what() << endl;
}
}
return 0;
}
原本一个十分简单的逻辑,为了使用异常我们就要防止内存泄漏,这使得原本简单的逻辑变得复杂。
智能指针的两点要去:
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象创建时获取资源,之后控制资源的访问在对象整个生命周期内都有效,在对象销毁时释放资源,这样将资源的申请与释放与对象绑定到一起。这样做有两个好处:
下面我们要添加对指针的访问,模拟指针的行为:
#include
using namespace std;
template<class T>
class smart_ptr
{
public:
smart_ptr(T* mp)
:_ptr(mp)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~smart_ptr()
{
cout << "delete" << endl;
delete _ptr;
}
private:
T* _ptr;
};
void Div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw("division of zero");
else
cout << "a / b = " << a / b << endl;
}
void Func()
{
smart_ptr<int>p1(new int(2));
smart_ptr<int>p2(new int(5));
Div();
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const char* s)
{
cout << s << endl;
}
catch (std::bad_alloc& e)
{
cout << "exception: " << e.what() << endl;
}
}
return 0;
}
为什么使用智能指针可以解决这个问题, 底层很复杂吗? – 不见得
我们在一开始就说了, 触发异常时, 函数调用链仍然会一层层出栈(不然栈区空间就泄漏了, 并且, 如果不出栈, 怎么访问到该函数调用链上的其他函数), 那么在出栈时, 函数栈上的变量就会被销毁, 我们的智能指针虽然是用来管理堆区资源的, 但是智能指针本身创建在栈区, 在智能指针被销毁时, 就会调用它的析构函数同步销毁管理的资源. perfect
auto_ptr整体逻辑与上方相同,看起来确实解决了异常安全问题,但是,为何auto_ptr会被使用者以及公司所排斥,甚至禁止使用auto_ptr呢?
让我们继续往下看去。
#include // auto_ptr
#include
using namespace std;
void test_ptr()
{
int* p1 = new int(1);
cout << "*p1 = " << *p1 << endl;
// 指针赋值时,p2此时也指向p1指向的那块空间。
int* p2 = p1;
cout << "*p2 = " << *p2 << endl;
cout << "*p1 = " << *p1 << endl;
}
void test_auto_ptr()
{
auto_ptr<int>ap(new int(10));
cout << "*ap = " << *ap << endl;
auto_ptr<int>ap2(ap);
cout << "*ap2 = " << *ap2 << endl;
cout << "*ap = " << *ap << endl;
}
int main()
{
test_ptr();
//test_auto_ptr();
return 0;
}
经过试验我们发现了auto_ptr的缺陷所在:当auto_ptr对象要进行拷贝构造时,新的对象可以访问拷贝来的资源,而被拷贝对象则无法访问了,这个实际的指针用法并不相符。
这中现象我们称为:悬空指针(没有指向一块实际内存的指针)
这种做法我们称为:管理权转移(ap没有权利访问原本属于它的资源了)。
听起来好像很高大上,底层实现很简单,如下所示:
namespace kz
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* mp)
:_ptr(mp)
{}
auto_ptr(auto_ptr& ap) // 拷贝构造
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr& ap) // 赋值
{
if (_ptr != ap._ptr) // 自己给自己赋值
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
cout << "delete" << endl;
delete _ptr;
}
private:
T* _ptr;
};
}
由于auto_ptr这样在拷贝、赋值方面的奇葩行为,最开始的智能指针auto_ptr并不受人待见,但是由于以及出来的标准就不会再做更改,所以写出管理权转移操作的大佬当时也是追悔莫及。
但是骂归骂,在使用异常时,智能指针在简化代码方面的优势也十分明显,因此大家在使用时还是会自行封装智能指针,为此在c++11中又引入了几个智能指针unique_ptr, shared_ptr, weak_ptr;那么说到这里就不得不提一句boost库了,
由于c++标准更新缓慢,很多大佬等不及体验新版本,而且委员会也有委员想要测试预上线功能的体验效果,因此就有委员会成员发起组织了Boost社区,封装了一系列的很有用的库,c++11的智能指针、右值引用等都脱胎于Boost库。
见名知意,该智能指针为了解决auto_ptr的管理权转移漏洞,直接简单粗暴地禁止智能指针进行拷贝与赋值。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* p = nullptr)
:_ptr(p)
{}
// 防止拷贝的两种做法
/*// c98 -- 拷贝构造和赋值 声明为私有成员
private:
unique_ptr(unique_ptr& up);
unique_ptr& operator=(unique_ptr& up);*/
// c++11 -- delete不需要的成员函数
unique_ptr(unique_ptr& up) = delete;
unique_ptr& operator=(unique_ptr& up) = delete;
// ... 模拟指针行为
~unique_ptr()
{
if (_ptr)
{
cout << "~unique_ptr()" << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
见名知意:shared既共享,unique_ptr限制了拷贝与赋值,也就是一份资源只能有一个智能指针指向它,
而shared是使用引用计数来标识可以有多个指针指向同一份资源,引用计数就是一个整数,记录此时有多少个指针指向这份资源。
举个栗子:
10个学生来上晚自习,自习室代表资源,则unique是一个学生用一个自习室,shared是10个学生可以共用一个自习室,此时引用计数为10;
释放资源时,unique是一个学生使用一个自习室,来时开门,走时锁门,
shared是最后一个走的学生锁门,此时引用计数应当变为0,表示自习室中没有学生。
shared_ptr有两点需要注意:
要实现shared_ptr,我们肯定是要有一个引用计数,
首先我们知道引用计数需要被多个对象共享,既然要让不同对象看到同一个引用计数,那么它就不能是在栈区,因为各个对象的栈区数据是独立的;
剩下还有两种方法:1. 设置为静态成员,2. 开辟到堆区
静态成员在整个类中只有一个,可以被所以类成员共享,那么设置为静态成员确实可以保证不同对象看到同一份数据,如果该智能指针只申请一份资源还可以满足,
但当资源数增加时,使用不同的资源的对象也是看到的那一个引用计数,显然不符合实际情况,实际应当是一个资源匹配一个引用计数才对。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* p = nullptr)
:_ptr(p)
{
_rcount = new int(_ptr != nullptr);
}
shared_ptr(shared_ptr& sp)
:_ptr(sp._ptr)
,_rcount(sp._rcount)
{
if (_ptr) ++(*_rcount);
}
shared_ptr& operator=(shared_ptr& sp)
{
if (_ptr != sp._ptr)
{
// 释放之前的资源
Destory();
// 链接现在的资源
if (sp._ptr)
{
_ptr = sp._ptr;
_rcount = sp._rcount;
}
}
}
// 释放资源
void Destory()
{
if (--(*_rcount) == 0)
{
cout << "Destory()" << endl;
delete _ptr;
delete _rcount;
_ptr = _rcount = nullptr;
}
}
~shared_ptr()
{
Destory();
}
private:
T* _ptr;
int* _rcount;
};
void Func(kz::shared_ptr<int>& sp, int n)
{
for (int i = 0; i < n; ++i)
kz::shared_ptr<int>tmp(sp);
}
void test_shared2()
{
kz::shared_ptr<int>sp(new int(1));
int n = 10000;
thread t1(Func, ref(sp), n);
thread t2(Func, ref(sp), n);
t1.join();
t2.join();
cout << sp.use_count() << endl;
}
下方为多线程情况下,运行的结果:
有结果可知,我们上面的实现无法满足多线程的要求,那么问题出在哪里,
我们可以看一看_rcount的值:2, 4,以及同时出现两个0(析构两次),这说明我们的_rcount控制上有问题,
因为++,–操作不是原子的,所以多线程访问时应该互斥访问 – 加锁。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* p = nullptr)
:_ptr(p)
,_rcount(new int())
,_mtx(new mutex)
{
if(_ptr) AddRcount();
}
shared_ptr(shared_ptr& sp)
:_ptr(sp._ptr)
,_rcount(sp._rcount)
,_mtx(sp._mtx)
{
if (_ptr) AddRcount();
}
void AddRcount()
{
_mtx->lock();
++(*_rcount);
_mtx->unlock();
}
shared_ptr& operator=(shared_ptr& sp)
{
if (_ptr != sp._ptr)
{
// 释放之前的资源
Destory();
// 链接现在的资源
if (sp._ptr)
{
_ptr = sp._ptr;
_rcount = sp._rcount;
_mtx = sp._mtx;
AddRcount();
}
}
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_rcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 释放资源
void Destory()
{
bool lockFlag = false;
_mtx->lock();
if (--(*_rcount) == 0)
{
cout << "Destory()" << endl;
delete _ptr;
delete _rcount;
lockFlag = true;
_ptr = _rcount = nullptr;
}
_mtx->unlock();
if (lockFlag) // _mtx需要先解锁再释放,不能直接在上方释放
{
delete _mtx;
_mtx = nullptr;
}
}
~shared_ptr()
{
Destory();
}
private:
T* _ptr;
int* _rcount;
mutex* _mtx;
};
上方我们对智能指针内部引用计数的访问进行了加锁,保证了线程安全,
那么对智能指针指向资源的访问是否线程安全?
void Func3(kz::shared_ptr<int>& sp, int n, mutex& pmtx)
{
for (int i = 0; i < n; ++i)
{
pmtx.lock();
++(*sp);
pmtx.unlock();
}
}
void test_shared3()
{
kz::shared_ptr<int>sp(new int(1));
int n = 10000;
mutex pmtx;
thread t1(Func3, ref(sp), n, ref(pmtx));
thread t2(Func3, ref(sp), n, ref(pmtx));
t1.join();
t2.join();
cout << *sp << endl;
}
struct ListNode
{
kz::shared_ptr< ListNode> _prev;
kz::shared_ptr< ListNode> _next;
int _val;
};
void test_cycle()
{
kz::shared_ptr<ListNode>d1(new ListNode);
kz::shared_ptr<ListNode>d2(new ListNode);
d1->_next = d2;
d2->_prev = d1;
}
循环等待是shared_ptr的引用计数特性给自己埋下的一个坑,这里的问题就在于next与prev指向智能指针对象时,引用计数进行了++,
导致在析构时rcount值不为0,为了解决这个这个问题,有有了weak_ptr。
weak_ptr专门为shared_ptr设计,用来避免对shared_ptr拷贝或赋值时改变引用计数rcount的值。
namespace kz
{
template<class T>
class weak_ptr
{
public:
weak_ptr(T* p = nullptr)
:_ptr(p)
{}
weak_ptr(shared_ptr<T>& sp) // 模拟库中weak_ptr的实现逻辑,真正的底层并不是如此简单
:_ptr(sp.get())
{}
weak_ptr& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp.get())
{
if (sp.get())
_ptr = sp.get();
}
return *this;
}
private:
T* _ptr;
};
}
struct ListNode
{
kz::weak_ptr<ListNode> _prev;
kz::weak_ptr<ListNode> _next;
int _val;
};
void test_cycle()
{
kz::shared_ptr<ListNode>d1(new ListNode);
kz::shared_ptr<ListNode>d2(new ListNode);
d1->_next = d2;
d2->_prev = d1;
}
标准库中无法打印其他信息,通过引用计数我们也可以看出与我们实现的一致:weak_ptr不会增加引用计数
删除器顾名思义就是用来删除操作的,上方我们看到,我们的析构操作都是使用的delete,那么与之对应的申请空间的操作符就是new,
也就是说,我们上方的智能指针所控制的资源只能是单个的资源,并且只能是通过new申请到的;
如果我们传入的是malloc、或new [ ] 申请的资源,delete很可能不会如我们所愿地完成资源的释放。
为了完成资源申请与释放的两两配对,引入了删除器的概念。
删除器:就是自行设置删除函数。
template<class T>
struct FreeFunc
{
void operator()(T* p)
{
cout << "free" << endl;
free(p);
}
};
template<class T>
struct DeleteArrayFunc
{
void operator()(T* p)
{
cout << "delete array" << endl;
delete[]p;
}
};
struct Date
{
~Date()
{}
int _year;
int _month;
int _day;
};
void test_deleter()
{
std::shared_ptr<int[]>sp1(new int[10] {10});
std::shared_ptr<Date>sp2(new Date[10], DeleteArrayFunc<Date>());
std::shared_ptr<int>sp3((int*)malloc(sizeof(int)), FreeFunc<int>());
}
如此就解决了申请与释放操作相匹配的问题。
总的来说,智能指针提供了一种方便、安全和可靠的方式来管理动态分配的内存和其他资源,可以减少手动内存管理的工作量,提高程序的健壮性和可维护性。