为什么需要智能指针
首先来分析一段代码
int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void Func() { int* p1 = new int; int* p2 = new int; cout << div() << endl; delete p1; delete p2; } int main() { try { Func(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
根据异常的性质可以得知,如果发生了异常抛出异常后,throw下面的代码就不会执行了。那么上面的代码问题在哪里呢,因为new本身就是会调用底层的函数,那么new本身就有可能会出现异常。如果new异常了那下面的释放空间就不会执行了,这样就导致了内存泄露的问题。因此为了这种的情况的避免,就引入了智能指针的概念。
什么是智能指针
在了解什么是智能指针前先来了解一个简单的技术
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
因为在C++中一个类对象在释放后会自动地调用类的析构函数,所以可以利用这个性质将指向开辟好空间的指针作为构造参数放到一个类中,这样该类中的指针成员指向的空间就和传入的指针的地址是一样的,将空间的释放放到类的析构中,这样即使异常抛出,只要对象销毁所开辟的空间也会自动销毁。这就是实现智能指针的基本思想
那么根据这种思想可以来对上述的代码进行一个改造
template<class T> class SmartPtr { public: SmartPtr(T* p = nullptr) : _p(p) {} ~SmartPtr() { if(_p) delete _p; } private: T* _p; }; int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void Func() { SmartPtr<int> p1(new int); SmartPtr<int> p2(new int); cout << div() << endl; } int main() { try { Func(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
这样即使new p2的参数时抛出异常也不会影响到p1的释放。
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针,就是上述代码的思想,不过上述代码并没有实现出指针的用法,而智能指针是可以跟指针一样用的。那么就可以给上述代码的类进一步改造就是模拟C++98库中的auto_ptr了
template<class T> class Auto_ptr { public: Auto_ptr(T* ptr) :_ptr(ptr) {} Auto_ptr(auto_ptr<T>& sp) :_ptr(sp._ptr) { // 管理权转移 sp._ptr = nullptr; } Auto_ptr<T>& operator=(auto_ptr<T>& ap) { // 检测是否为自己给自己赋值 if (this != &ap) { // 释放当前对象中资源 if (_ptr) delete _ptr; // 转移ap中资源到当前对象中 _ptr = ap._ptr; ap._ptr = NULL; } return *this; } ~Auto_ptr() { if (_ptr) { cout << "delete:" << _ptr << endl; delete _ptr; } } // 像指针一样使用 // 实现解引用和取地址 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
但是这种设计是有很大问题的:资源的管理权转移意味该对象不能在对原来管理的资源进行访问了,如果进行访问,会导致程序崩溃,很容易出现问题,因此绝大多数的公司都是明确要求不能使用auto_ptr的
unique_ptr
C++11中就出现了更为靠谱的unique_ptr,这个实现的原理就是简单粗暴的禁止拷贝。
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; } // 禁止拷贝 Unique_ptr(const unique_ptr<T>& sp) = delete; Unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete; private: T* _ptr; };
但是直接防止拷贝也并不是很好,这样子的应用场景就很受限
shared_ptr
shared_ptr就再次改进,更靠谱也支持拷贝
这个的原理就是:
通过引用计数的方式来实现多个shared_ptr对象之间共享资源,shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
那么用什么计数呢,首先肯定不能使用内置成员变量计数,因为每一个对象都会有属于自己独立的成员变量。其次也不能使用静态成员变量,如果是静态成员变量,那么是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象都是用同一个引用计数
真正的解决办法是利用指针,让所有对象都能看到同一个地址,利用该地址空间存储计数,因为这个计数是属于临界资源,为了保证安全可以利用加锁来保证
template<class T> class Shared_ptr { public: Shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pRefCount(new int(1)) , _pmtx(new mutex) {} Shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pRefCount(sp._pRefCount) , _pmtx(sp._pmtx) { AddRef(); } // 计数减1 void Release() { _pmtx->lock(); bool flag = false; if (--(*_pRefCount) == 0 && _ptr) { cout << "delete:" << _ptr << endl; delete _ptr; delete _pRefCount; flag = true; } _pmtx->unlock(); if (flag == true) delete _pmtx; } // 计数加1 void AddRef() { _pmtx->lock(); ++(*_pRefCount); _pmtx->unlock(); } Shared_ptr<T>& operator=(const shared_ptr<T>& sp) { if (_ptr != sp._ptr) { Release(); _ptr = sp._ptr; _pRefCount = sp._pRefCount; _pmtx = sp._pmtx; AddRef(); } return *this; } // 返回当前有多少个对象共享 int use_count() { return *_pRefCount; } ~Shared_ptr() { Release(); } // 像指针一样使用 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T* get() const { return _ptr; } private: T* _ptr; int* _pRefCount; // 计数 mutex* _pmtx; // 锁 };