目录
长路漫漫,唯剑作伴。
为什么需要智能指针
RAII
使用RAII思想管理内存
重载 * 和->
总结一下智能指针的原理:
C++的智能指针和拷贝问题
auto_ptr (C++98) 编辑
auto_ptr的实现原理:管理权转移的思想,
unique_ptr (C++11)
sheared_ptr 的简化实现
关于循环引用的问题(Circular reference)
weak_ptr 打破循环引用(C++11)
weak_ptr 的简化实现
定制删除器
定制删除器的使用
unique_ptr 定制删除器的模拟实现
相关链接 :
int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void func() { int* p1 = new int[10]; // 这里亦可能会抛异常 int* p2 = new int[10]; // 这里亦可能会抛异常 int* p3 = new int[10]; // 这里亦可能会抛异常 int* p4 = new int[10]; // 这里亦可能会抛异常 try { div(); } catch (...) { delete[] p1; delete[] p2; delete[] p3; delete[] p4; throw; } delete[] p1; delete[] p2; delete[] p3; delete[] p4; } int main() { try { func(); } catch (const exception& e) { cout << e.what() << endl; // ... } return 0;
可以发现上面这段代码是典型的异常处理中可能会碰到内存泄漏或析构异常的例子,我们当然可以为每个new[]都尝试着抛出异常,但那样太麻烦了,所以我们引入了智能指针
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
使用RAII思想管理内存
// 使用RAII思想设计的SmartPtr类 template
class SmartPtr{ public: SmartPtr(T* ptr = nullptr) : _ptr(ptr){ cout << "get " << _ptr << endl; } ~SmartPtr(){ if (_ptr) cout << "delet " <<_ptr<< endl; delete _ptr; } private: T* _ptr; }; void Func(){ int* pa = new int(10); cout << " pa " << pa << endl; SmartPtr sp1(pa); } int main(){ Func(); return 0; } 重载 * 和->
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可 以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
#include
#include using namespace std; //使用RAII 思想管理内存 template class SmartPtr{ public: SmartPtr(T* ptr = nullptr) : _ptr(ptr){ //cout << "get " << _ptr << endl; } ~SmartPtr(){ if (_ptr) //cout << "delet " <<_ptr<< endl; delete _ptr; } T& operator*(){ return *_ptr; } T* operator->(){ return _ptr; } private: T* _ptr; }; struct Date{ int _year; int _month; int _day; }; int main(){ SmartPtr sp1(new int); *sp1 = 10; cout << *sp1 << endl; SmartPtr spDate(new Date); // 需要注意的是这里应该是spDate.operator->()->_year = 2018; // 本来应该是spDate->->_year这里语法上为了可读性,省略了一个-> spDate->_year = 2060; spDate->_month = 6; spDate->_day = 14; cout << spDate->_year << "/" << spDate->_month << "/" << spDate->_day << endl; return 0; } 但是尽管如此,我们仍旧面临拷贝问题和析构问题,这里先抛出问题,文章后面会解释C++智能指针是如何解决这类的问题的,一步一步来,我们会先解决拷贝问题,然后再解决析构问题(定制删除器)
void copy_issue(){ SmartPtr
sp1(new int); SmartPtr sp2 = sp1; //这里会析构2次而导致程序异常终止 } void destructor_issue(){ SmartPtr sp1(new int[10]); //这里的析构存在类型不匹配,没办法正确的调用 delete[], //类似的还有文件描述符,也没办法正确 close()... } 总结一下智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为
- 处理拷贝问题
- 定制删除器
auto_ptr (C++98)
auto_ptr的实现原理:管理权转移的思想,
下面简化模拟实现了一份skate::auto_ptr来了解它的原理:
namespace skate{ template
class auto_ptr{ public: auto_ptr(T* ptr) :_ptr(ptr){ } auto_ptr(auto_ptr & sp) //拷贝构造 :_ptr(sp._ptr){ // 管理权转移 sp._ptr = nullptr; } auto_ptr & operator=(auto_ptr & 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是一个失败设计,很多公司明确要求不能使用auto_ptr int main(){ skate::auto_ptr sp1(new int); *sp1 = 10; skate::auto_ptr sp2(sp1); // 管理权转移 //sp1悬空 *sp1 = 20; //空指针异常 *sp2 = 30; cout << *sp2 << endl; cout << *sp1 << endl; return 0; } unique_ptr (C++11)
C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理// C++11库才更新智能指针实现 // C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr // C++11将boost库中智能指针精华部分吸收了过来 // C++11->unique_ptr/shared_ptr/weak_ptr // unique_ptr/scoped_ptr // 原理:简单粗暴 -- 防拷贝 namespace skate{ template
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 & sp) = delete; unique_ptr & operator=(const unique_ptr & sp) = delete; private: T* _ptr; }; } std::shared_ptr (C++11)
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
class MyClass{ public: MyClass(){ std::cout << "MyClass constructor called" << std::endl; } ~MyClass(){ std::cout << "MyClass destructor called" << std::endl; } }; int main(){ std::shared_ptr
ptr1(new MyClass); //调用构造函数 std::cout << ptr1.get() << " _ptr-> ptr1.use_count() = " << ptr1.use_count() << std::endl; std::shared_ptr ptr2 = ptr1;//调用赋值重载 计数++ std::cout << ptr2.get() << " _ptr-> ptr2.use_count() = " << ptr2.use_count() << std::endl; std::shared_ptr ptr3(ptr1);//调用拷贝构造 计数++ std::cout << ptr3.get() << " _ptr-> ptr3.use_count() = " << ptr3.use_count() << std::endl; ptr1.reset(); //计数-- ptr2.reset();//计数-- ptr3.reset();//计数为0 调用析构 std::cout << ptr3.get() << " _ptr-> ptr3.use_count() = " << ptr3.use_count() << std::endl; return 0; } sheared_ptr 的简化实现
template
class shared_ptr { public: void Release()// 释放指向的对象和引用计数 { if (--(*_pCount) == 0 && _ptr) //计数为0进行析构 { cout << "delete" << _ptr << endl; delete _ptr; _ptr = nullptr; delete _pCount; _pCount = nullptr; } } // RAII思想 shared_ptr(T* ptr) :_ptr(ptr) , _pCount(new int(1)) {} ~shared_ptr() { Release();//析构先时候 --计数,后确认析构 } shared_ptr(const shared_ptr & sp) :_ptr(sp._ptr) , _pCount(sp._pCount) { (*_pCount)++; } // sp1 = sp3 shared_ptr & operator=(const shared_ptr & sp) { //if (this != &sp) if (_ptr != sp._ptr) { Release();//先 --计数,后确认析构 _ptr = sp._ptr; _pCount = sp._pCount; ++(*_pCount); } return *this; } // 像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T* get() const { return _ptr; } int use_count() { return *_pCount; } private: T* _ptr; int* _pCount; }; 需要注意的是,这只是一份简单的shared_ptr模拟实现,并没有考虑线程安全等方面的问题。实际上,C++11中的标准库中的shared_ptr实现更加复杂和完善,具有线程安全等特性。
关于循环引用的问题(Circular reference)
循环引用是指两个或多个智能指针对象直接或间接地引用彼此,形成一个引用链,导致资源无法正确释放。当涉及的对象之间的引用计数永远不会降为0时,就会发生循环引用问题。
这种情况下,我们可以使用weak_ptr来打破循环引用,从而避免内存泄漏的问题。
循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
weak_ptr 打破循环引用(C++11)
weak_ptr 的简化实现
// 不直接参与指向资源的释放管理 template
class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(const shared_ptr & sp) :_ptr(sp.get()) {} weak_ptr & operator=(const shared_ptr & sp) { if (_ptr != sp.get()) { _ptr = sp.get(); } return *this; } // 像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } public: T* _ptr; }; }
定制删除器的使用
class Date{ public: ~Date(){ cout << "~Date()" << endl; } private: int _year = 1; int _month = 1; int _day = 1; }; // unique_ptr/shared_ptr 默认释放资源用的delete // 如何匹配申请方式去对应释放呢? template
struct DeleteArray{ void operator()(T* ptr){ cout << "delete[]" << ptr << endl; delete[] ptr; } }; template struct Free{ void operator()(T* ptr){ cout << "free" << ptr << endl; free(ptr); } }; struct Fclose{ void operator()(FILE* ptr){ cout << "fclose" << ptr << endl; fclose(ptr); } }; void test_unique(){ cout << "\ntest_unique()\n"; // unique_ptr传类型 std::unique_ptr up1(new Date); std::unique_ptr > up2(new Date[5]); std::unique_ptr > up3((Date*)malloc(sizeof(Date) * 5)); std::unique_ptr up4((FILE*)fopen("Test.cpp", "r")); } void test_shared(){ cout << "\ntest_shared()\n"; // shared_ptr传对象 std::shared_ptr sp1(new Date[5], DeleteArray ()); std::shared_ptr sp2(new Date[5], [](Date* ptr){cout << "lambda delete[] " << ptr << endl;; delete[] ptr; }); } //定制删除器 int main(){ test_unique(); test_shared(); return 0; } unique_ptr 定制删除器的模拟实现
ps:
C++11中的标准库中的shared_ptr实现更加复杂和完善,具有线程安全等特性。封装的也更加彻底,这里就不写模拟实现了
https://cplusplus.com/reference/memory/auto_ptr/
https://cplusplus.com/reference/memory/unique_ptr/
https://cplusplus.com/reference/memory/shared_ptr/
https://cplusplus.com/reference/memory/weak_ptr/