一、智能指针的初步认识
1.1 使用场景
使用智能指针是解决内存泄露问题的良好手段
int Div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void func() { int* ptr = new int; //... cout << Div() << endl; //... delete ptr; } int main() { try { func(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
执行上述代码时,若用户输入的除数为0,那么Div()函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func()函数中申请的内存资源没有得到释放
利用异常的重新捕获解决
对于这种情况,可以在func()函数中先对Div()函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出
int Div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void func() { int* ptr = new int; try { cout << Div() << endl; } catch (...) { delete ptr; throw; } delete ptr; } int main() { try { func(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
但这种方式并完全不可靠,有时可能会疏忽一些异常情况
利用智能指针解决
#include <iostream> using namespace std; template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr(){ cout << "delete:" << _ptr << endl; delete _ptr; } 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 Func() { SmartPtr<int> sp1(new int);//是否抛异常都会释放 SmartPtr<int> sp2(new int); *sp1 = 0; *sp2 = 2; cout << Div() << endl; } int main() { try { Func(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理
在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间的地址保存起来
在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放
为了让SmartPtr对象能够像原生指针一样使用,还需要对*和->运算符进行重载
无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放
1.2 原理
实现智能指针时需要考虑以下三个方面的问题:
在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性
对*和->运算符进行重载,使得该对象具有像指针一样的行为
智能指针对象的拷贝问题
概念说明: RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术
智能指针对象的拷贝问题
对于当前实现的SmartPtr类,若用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃
int main() { SmartPtr<int> sp1(new int); SmartPtr<int> sp2(sp1); //拷贝构造 SmartPtr<int> sp3(new int); SmartPtr<int> sp4(new int); sp3 = sp4; //拷贝赋值 return 0; }
原因如下:
编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次
编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放
智能指针就是要模拟原生指针的行为,当将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里就应该进行浅拷贝。但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针
二、std::auto_ptr
2.1 管理权转移
auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源只有一个智能指针对象在对其进行管理,同一个资源就不会被多次释放了
#include <iostream> using namespace std; int main() { std::auto_ptr<int> ap1(new int(1)); std::auto_ptr<int> ap2(ap1); *ap2 = 10; //*ap1 = 20; //error std::auto_ptr<int> ap3(new int(1)); std::auto_ptr<int> ap4(new int(2)); ap3 = ap4; cout << *ap3 << endl;//2 //cout << *ap4 << endl;//error return 0; }
但使用管理权转移的方式来解决问题并不优秀。对象的管理权转移后也就意味着,不能再用该对象对原来管理的资源进行访问了,否则程序就会崩溃
使用auto_ptr之前必须先了解其机制,否则程序极易出问题,很多公司也规定禁止使用auto_ptr
2.2 auto_ptr的模拟实现
在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为
在拷贝构造函数中,用传入对象管理的资源来构造当前对象,将传入对象管理资源的指针置空
在拷贝赋值函数中,将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空
namespace bjy { template<class T> class auto_ptr { public: auto_ptr(T* ptr = nullptr):_ptr(ptr) {} ~auto_ptr() { if (_ptr != nullptr) { delete _ptr; _ptr = nullptr; } } auto_ptr(auto_ptr<T>& ap) { _ptr = ap._ptr; ap._ptr = nullptr; } auto_ptr& operator=(auto_ptr<T>& ap) { if (this != &ap) { delete _ptr; _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr;//指向所管理的资源 }; }
三、std::unique_ptr
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,ji即简单粗暴的防止对智能指针对象进行拷贝,保证资源不会被多次释放
int main() { std::unique_ptr<int> up1(new int(10)); //std::unique_ptr<int> up2(up1); //error return 0; }
但防拷贝其实也不是一个很好的办法,总有一些场景需要进行拷贝
unique_ptr的模拟实现
在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
对 * 和 -> 运算符进行重载,使unique_ptr对象具有指针一样的行为
用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式=delete,防止外部调用
namespace bjy { template<class T> class unique_ptr { public: unique_ptr(T* ptr = nullptr) :_ptr(ptr) {} ~unique_ptr() { if (_ptr != nullptr) { delete _ptr; _ptr = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //防拷贝 unique_ptr(unique_ptr<T>& ap) = delete; unique_ptr& operator=(unique_ptr<T>& ap) = delete; private: T* _ptr;//指向所管理的资源 }; }
四、std::shared_ptr
4.1 基础设计
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题
每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源
当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--
当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放
通过引用计数的方式就能支持多个对象一起管理某一个资源,即支持了智能指针的拷贝,并且只有当资源对应的引用计数减为0时才会释放资源,保证了同一个资源不会被释放多次
#include <iostream> int main() { std::shared_ptr<int> sp1(new int(1)); std::shared_ptr<int> sp2(sp1); *sp1 = 10; *sp2 = 20; std::cout << sp1.use_count() << std::endl; //2 std::shared_ptr<int> sp3(new int(1)); std::shared_ptr<int> sp4(new int(2)); sp3 = sp4; std::cout << *sp3 << std::endl;//2 std::cout << sp3.use_count() << std::endl; //2 return 0; }
注意: use_count()成员函数,用于获取当前对象管理的资源对应的引用计数
shared_ptr的模拟实现
在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数
在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前有一个对象在管理该资源
在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++
在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(若减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++
在析构函数中,将管理资源对应的引用计数--,若减为0则需要将该资源释放
对 * 和 -> 运算符进行重载,使shared_ptr对象具有指针一样的行为
namespace bjy { template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new size_t(1)) {} ~shared_ptr() { if (--(*_pCount) == 0) { if (_ptr != nullptr) {//shared_ptr可能管理的是0地址处的空间 delete _ptr; _ptr = nullptr; } delete _pCount; _pCount = nullptr; } } shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount) { ++(*_pCount); } shared_ptr<T>& operator=(shared_ptr<T>& sp) { if (_ptr != sp._ptr) { if (--(*_pCount) == 0) {//若引用计数为0,则释放该对象 delete _ptr; delete _pCount; } _ptr = sp._ptr; _pCount = sp._pCount; ++(*_pCount); } return *this; } size_t GetCount() { return *_pCount; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; }; private: T* _ptr; size_t* _pCount; }; }
为什么引用计数需要存放在堆区?
shared_ptr中的引用计数不能单纯的定义成一个整型类型的成员变量,否则每个shared_ptr对象都有各自的引用计数,而当多个对象要管理同一个资源时,这些对象应该用的是同一个引用计数
shared_ptr中的引用计数也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数
若将shared_ptr中的引用计数定义成一个指针,当资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,若有其他对象也想要管理这个资源,那么除了需要这个资源的地址之外,还需要引用计数的地址 。此时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数,相当于将各个资源与其对应的引用计数进行了绑定
注意:由于引用计数的内存空间也是在堆上开辟的,因此当资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放