赞
踩
今天我们来讲一下c++中的智能指针。
智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。
动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源
在之前的博客 【C++】异常 中我提到过一个叫做异常的重新抛出的场景:
我们再把那个例子讲一下:
void File() { string filename; cin >> filename; FILE* fout = fopen(filename.c_str(), "r"); if (fout == nullptr) { string errmsg = "打开文件失败:"; errmsg += filename; errmsg += "->"; errmsg += strerror(errno); Exception e(errno, errmsg); throw e; } char ch; while ((ch = fgetc(fout))!=EOF) { cout << ch; } fclose(fout); } double Division(int a, int b) { if (b == 0) { string errmsg = "Division by zero condition!"; Exception e(100, errmsg); throw e; } else { return ((double)a / (double)b); } } void Func() { int* p = new int[100]; int len, time; cin >> len >> time; try { cout << Division(len,time) << endl; File(); } catch (...) { //捕获之后,不是要处理异常,异常由最外层同一处理 //这里捕获异常只是为了处理内存泄漏的问题 delete[]p; throw; } delete[]p; } int main() { try { Func(); } catch (const Exception& e) { cout << e.what() << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }
在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的 析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。
但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。
所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。
借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
我们可以借助RALL思想来写一个简单的 智能指针:
#include<iostream> using namespace std; template<class T> class SmartPtr { public: SmartPtr(T* ptr =nullptr) :_ptr(ptr) {} ~SmartPtr() { if (_ptr)delete _ptr; cout<<"~SmartPtr"<<endl; } private: T* _ptr; }; int main() { int* a = new int(1); SmartPtr<int> sp(a); //将a 指针委托给sp对象管理 SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理 }
上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。
指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。
除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。
所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针,我们下面来一一讲解。
C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景。
在C++98版本的库种,提供了 auto_ptr 的智能指针:
我们使用一下std::auto_ptr:
class Date { public: Date() :_year(0),_month(0),_day(0) {} ~Date(){} int _year; int _month; int _day; }; int main() { auto_ptr<Date>ap(new Date); //拷贝构造 auto_ptr<Date>copy(ap); ap->_year = 2022; }
我们发现报错了,发生了非法访问。
这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)
在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。
虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。
由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。
那么auto_ptr具体是如何实现的呢?很简单.
template<class T> class auto_ptr { public: auto_ptr(T* ptr=nullptr) :_ptr(ptr) {} auto_ptr(auto_ptr<T>& ap) :_ptr(ap._ptr) { ap._ptr = nullptr; //管理权转移 } auto_ptr<T>& operator = (auto_ptr<T>& ap) { if (this != *ap) { delete _ptr; _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } ~SmartPtr() { if (_ptr)delete _ptr; } T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } private: T* _ptr; };
在C++11中,C++11y引入了unique_ptr.
unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。
对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。
我们可以尝试实现一下:
template<class T> class unique_ptr { public: unique_ptr(T* ptr = nullptr) :_ptr(ptr) {} //防拷贝 unique_ptr(unique_ptr<T>& ap) = delete; unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete; ~SmartPtr() { if (_ptr)delete _ptr; } T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } private: T* _ptr; };
C++中还提供了shared_ptr。
shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。
我们可以测试使用一下:
那么shared_ptr的原理是什么?
我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。
再具体一点:
我们可以实现一个简单的shared_ptr:
template<class T> class shared_ptr { public: shared_ptr(T*ptr =nullptr) :_ptr(ptr),_pcount(new int(1)) {} //拷贝构造 shared_ptr(const T& sp) _ptr(sp._ptr),_pcount(sp._pcount) { ++(*_pcount); } //赋值拷贝 shared_ptr<T>& operator = (shared_ptr<T>& sp) { if (_ptr != sp._ptr) { if (--(*_pcount) == 0){ delete _pcount; delete _ptr; } _ptr = sp._ptr; _pcount = sp._pcount; ++(*_pcount); } return *this; } T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } ~shared_ptr() { if (--(*_pcount) == 0 && _ptr) { delete _pcount; delete _ptr; } } private: T* _ptr; int* _pcount; };
我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。
-拷贝构造
赋值拷贝需要注意两点:
其他写法:
shared_ptr<T>& operator=(shared_ptr<T> sp)
{
swap(_ptr, sp._ptr);
swap(_pcount, sp._pcount);
return *this;
}
但是,此时我们的shared_ptr 还面临着 线程安全的问题。
这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分。
template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex) {} void add_ref() { _pmtx->lock(); ++(*_pcount); _pmtx->unlock(); } void release_ref() { bool flag = false; _pmtx->lock(); if (--(*_pcount) == 0 && _ptr) { delete _pcount; delete _ptr; flag = true; cout << "释放资源:" << _ptr << endl; } _pmtx->unlock(); if (flag)delete _pmtx; } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx) { add_ref(); } shared_ptr<T>& operator = (const shared_ptr<T>& sp) { if (_ptr != sp._ptr) { if (--(*_pcount) == 0){ delete _pcount; delete _ptr; } _ptr = sp._ptr; _pcount = sp._pcount; add_ref(); } return *this; } T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } T* get() { return _ptr; } int use_count() { return *_pcount; } ~shared_ptr() { release_ref(); } private: T* _ptr; int* _pcount; mutex* _pmtx; };
不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。
为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete
这里举两个例子
template<class T> struct DeleteArray { void operator()(T* ptr) { delete[]ptr; } }; void test_deletor() { DeleteArray<string>da; //使用仿函数定制 std::shared_ptr<string>s2(new string[10], da); std::shared_ptr<string>s3((string*)malloc(sizeof(string)), [](string* ptr) {free(ptr); }); //使用lamdba 定制 }
如果我们也想自己是想一下呢?
当然是可以的,但是由于我们的实现比库中的简单很多(库中使用多个类),所以我们难以通过传参的方式来定制删除器,我们增加一个模板参数,通过向模板传参来达到相同的目的。
std的框架设计底层用一个类专门管理资源计数,所以它们可以在构造函数传参,把删除器类型传递给专门管理资源的这个类。而我们是一体化的。
template<class T> struct DefaultDel { void operator ()(T* ptr) { delete ptr; } }; template<class T,class D=DefaultDel<T>> //增加模板参数 class shared_ptr { public: explicit shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex) {} void add_ref() { _pmtx->lock(); ++(*_pcount); _pmtx->unlock(); } void release_ref() { bool flag = false; _pmtx->lock(); if (--(*_pcount) == 0 && _ptr) { //定制化删除 D del; del(_ptr); delete _pcount; flag = true; cout << "释放资源:" << _ptr << endl; } _pmtx->unlock(); if (flag)delete _pmtx; } shared_ptr(const shared_ptr<T,D>& sp) :_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx) { add_ref(); } shared_ptr<T,D>& operator = (const shared_ptr<T,D>& sp) { if (_ptr != sp._ptr) { if (--(*_pcount) == 0){ delete _pcount; delete _ptr; } _ptr = sp._ptr; _pcount = sp._pcount; add_ref(); } return *this; } T& operator *() { return *_ptr; } T* operator ->() { return _ptr; } T* get() { return _ptr; } int use_count() { return *_pcount; } ~shared_ptr() { release_ref(); } private: T* _ptr; int* _pcount; mutex* _pmtx; };
此时我们要这样使用:
template<class T> struct DeleteArray { void operator()(T* ptr) { delete[]ptr; } }; void test_deletor() { //使用lamdba 定制 DeleteArray<string>da; std::shared_ptr<string,DeleteArray<string>>s2(new string[10); //使用lamdba 定制 auto ffree = [](string* ptr) {free(ptr); }; std::shared_ptr<string,decltype(ffree)>s3((string*)malloc(sizeof(string))); auto fclose = [](FILE* ptr) {fclose(ptr); }; std::shared_ptr<string,decltype(ffcolse)>s3(fopen("test.cpp","r")); }
虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:
为什么会发生这种情况呢?
在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。
此时我们就要使用 weak_ptr 来解决 循环引用。
weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:
我们只能使用 wek_ptr或者 shared_ptr 去初始化它。
我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。
我们可以自己实现一个weak_ptr:
template<class T> class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(shared_ptr<T>& sp) :_ptr(sp.get()),_pcount(sp.use_count()) {} weak_ptr(weak_ptr<T>& sp) :_ptr(sp._ptr), _pcount(sp._pcount) {} weak_ptr& operator = (shared_ptr<T>& sp) { _ptr = sp.get(); _pcount = sp.use_count(); return *this; } weak_ptr& operator = (weak_ptr<T>& sp) { _ptr = sp._ptr; _pcount = sp._pcount; return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } int use_count() { return *_pcount; } private: T* _ptr; int* _pcount; };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。