目录
为什么需要智能指针
内存泄露问题
使用智能指针解决
智能指针的原理
RAII
C++的智能指针
头文件
std::auto_ptr
std::unique_ptr
std::weak_ptr
std::weak_ptr解决循环引用问题
定制删除器
定制删除器的用法
C++11和boost中智能指针的关系
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 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 sp(p);
SmartPtr< int> sp1(new int);
*sp1 = 10;
SmartPtr> sp2(new pair);
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(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显示地释放资源
采用这种方式,对象所需的资源在其生命周期内始终保持有效。
总结一下智能指针的原理:
但是这样的智能指针还不够完善,会存在智能指针对象拷贝的问题,所以C++出现了不同版本的智能指针
决智能指针对象的拷贝问题:比如上面实现的智能指针 SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃
int main() {
SmartPtr sp1(new int);
SmartPtr sp2 = sp1; // 析构两次报错
}
原因:
编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp1赋值给sp2后,相当于sp1和sp2管理的都是原来sp1管理的空间,当sp1和sp2析构时就会导致这块空间被释放两次
可以采用计数器的方式解决,深拷贝不符合指针赋值的初衷。
智能指针在C++库中已有现成的可以使用,比如auto_ptr, weak_ptr, share_ptr, unique_Ptr等,这些针对上述拷贝的问题都有不同的方法解决
#include
auto_ptr 通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了
int main(){
std::auto_ptr ap1(new int);
std::auto_ptr ap2 = ap1; // 管理权转移
//*ap1 = 1; // 报错
*ap2 = 1;
}
对一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,会造成对象悬空,比如上面的 ap1,继续使用这个对象程序就会直接崩溃,因此使用 auto_ptr之前必须先了解它的机制,否则程序很容易出问题。
auto_ptr是一个失败设计,很多公司也都明确规定了禁止使用auto_ptr
auto_ptr的简单模拟实现
namespace hek {
//C++98 auto_ptr
// 管理权转移 早期设计缺陷,一般公司都明令禁止使用它
template
class auto_ptr {
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr& operator=(const auto_ptr& 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;
};
}
unique_ptr实现原理:防拷贝
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。
int main()
{
std::unique_ptr up1(new int(1));
std::unique_ptr up2(up1); //error,不允许拷贝
return 0;
}
编译报错
unique_ptr简单模拟实现
namespace hek{
//C++11 unique_ptr
// 防拷贝 简单粗暴,推荐使用
//缺陷:如果有需要拷贝的场景,他就没法使用
template
class unique_ptr {
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr& up) = delete;
unique_ptr& operator=(const unique_ptr& 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;
};
}
shared_ptr是 C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题,也就是说shared_ptr支持拷贝
通过这种引用计数的方式就能支持多个对象一起管理某个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数键减为0时才会释放资源,因此保证了同一个资源不会被释放多次。
int main()
std::shared_ptr sp1(new int);
std::shared_ptr sp2(sp1);
cout << sp1.use_count() << endl; // 2
std::shared_ptr sp3(new int);
std::shared_ptr 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 shared_ptr {
public:
template
friend class weak_ptr;
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
//++(*_pcount);
add_ref_count();
}
// sp1 = sp3
shared_ptr& operator=(const shared_ptr& 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对象拷贝和析构 ++/--引用计数 是线程安全的。库的实现也安全
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 _spnext; // 如果用指针是不会引起循环引用的,这里主要突出循环引用
std::shared_ptr _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 spn1(new ListNode);
std::shared_ptr 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;
}
这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用
循环引用分析:
循环引用导致资源未被释放的原因:
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
struct ListNode {
int val;
//std::shared_ptr _spnext; // 如果用指针是不会引起循环引用的,这里主要突出循环引用
//std::shared_ptr _spprev;
std::shared_ptr spn1(new ListNode);
std::shared_ptr spn2(new ListNode);
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main() {
std::shared_ptr spn1(new ListNode);
std::shared_ptr 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的模拟实现
namespace hek{
// 严格来说weak_ptr 不是智能指针,因为他没有RAII资源管理机制
// 专门解决shared_ptr的循环引用问题
template
class weak_ptr {
public:
weak_ptr() = default;
weak_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
{}
weak_ptr& operator=(const shared_ptr& 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
shared_ptr (U* p, D del);
参数说明:
class A {
public:
~A()
{
cout << "~A" << endl;
}
private:
int _a1;
int _a2;
};
template
struct DeleteArry {
void operator()(T* pa) {
cout << "delete[] pa " << endl;
delete[] pa;
}
};
template
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 sp1(new A);
shared_ptr sp2(new A[10], DeleteArry());
shared_ptr sp3((A*)malloc(sizeof(A)), Free());
shared_ptr sp4((FILE*)fopen("test.txt", "w"), Fclose());
shared_ptr sp4((FILE*)fopen("test.txt","w"),Fclose());
return 0;
}
定制删除器的简单实现:
要在当前模拟实现的shared_ptr的基础上支持定制删除器,可以给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。
然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。
最好在设置一个默认的删除器,如果用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源。
namespace hek
{
//默认的删除器
template
struct Delete
{
void operator()(const T* ptr)
{
delete ptr;
}
};
template>
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& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
add_ref_count();
}
// sp1 = sp3
shared_ptr& operator=(const shared_ptr& 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
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 sp6((FILE*)fopen("test.txt", "w"), f);
hek::shared_ptr> sp8((FILE*)fopen("test.txt", "w"), [](FILE* ptr) {
cout << "fclose:2 " << ptr << endl;
fclose(ptr);
});
// 仿函数示例
hek::shared_ptr> sp7((FILE*)fopen("test.txt", "w"), Fclose());
hek::shared_ptr sp1(new A);
hek::shared_ptr> sp2(new A[10], DeleteArry());
cout << typeid(Fclose()).name() << endl;
cout << typeid(f).name() << endl;
}
提升C++库 (boost.org)
说明一下:boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。