十五、智能指针

发布时间:2026/7/12 22:03:05

十五、智能指针 注这个是博主复习使用的专题仅适用于自己以及学习过C知识点的同学文章目录前言一、智能指针的使用及原理1.1. 智能指针的使用1.2. 智能指针的原理RAII二、C 中的智能指针2.1. std::auto_ptr介绍 auto_ptrauto_ptr 的模拟实现2.2. std::unique_ptr介绍 unique_ptr实现unique_ptr2.3. std::shared_ptr介绍 shared_ptr实现shared_ptrshared_ptr 的线程安全问题2.4. std::weak_ptr三、C11和boost中智能指针的关系前言注这个是博主复习使用的专题仅适用于自己以及学习过C知识点的同学。一、智能指针的使用及原理1.1. 智能指针的使用内存泄漏问题内存泄露是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。比如#includeiostream using namespace std; double Divide(int a, int b) { // 当b 0 时抛出异常 if (b 0) { throw Divide by zero condition!; } else { return (double)a / (double)b; } } void Func() { int* array1 new int[10]; int* array2 new int[10]; // 抛异常呢 try { int len, time; cin len time; cout Divide(len, time) endl; } catch(...) { cout delete [] array1 endl; cout delete [] array2 endl; delete[] array1; delete[] array2; throw; // 异常重新抛出捕获到什么抛出什么 } cout delete [] array1 endl; delete[] array1; cout delete [] array2 endl; delete[] array2; } int main() { try { Func(); } catch(const char* errmsg) { cout errmsg endl; } catch(const exception e) { cout e.what() endl; } catch(...) { cout 未知异常 endl; } return 0; }如果发生除 0 错误抛出异常另外下面的array和array2没有得到释放。所以这里捕获异常后并不处理异常异常还是交给外面处理这里捕获了再重新抛出去。但是如果array2new的时候抛异常呢就还需要套一层捕获释放逻辑这里更好解决方案 是智能指针否则代码太戳了使用智能指针进行解决。比如templateclass T class SmartPtr { public: SmartPtr(T* ptr):_ptr(ptr) {} ~SmartPtr() { cout delete [] _ptr endl; delete[] _ptr; } // 重载运算符模拟指针行为方便访问资源 T operator*() { return *_ptr; } T* operator-() { return *_ptr; } T operator[](size_t i) { return _ptr[i]; } private: T* _ptr; }; double Divide(int a,int b) { if (b 0) { throw Divide by zero condition; } else { return (double)a / (double)b; } } void func() { // 使用 智能指针进行 管理 new 出来的数组程序简单很多 SmartPtrint sp1 new int[10]; SmartPtrint sp2 new int[10]; for (size_t i 0; i 10; i) { sp1[i] sp2[i] i; } int len, time; cin len time; cout Divide(len,time) endl; } int main() { try { func(); } catch (const char* errmsg) { cout errmsg endl; } catch (const exception e) { cout e.what() endl; } catch (...) { cout 未知异常 endl; } return 0; }代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。在构造SmartPtr对象时SmartPtr将传入的需要被管理的内存空间保存起来。在SmartPtr对象析构时SmartPtr的析构函数中会自动将管理的内存空间进行释放。此外为了让SmartPtr对象能够像原生指针一样使用还需要对*和-运算符进行重载。这样一来无论程序是正常执行完毕返回了还是因为某些原因中途返回了或是因为抛异常返回了只要SmartPtr对象的生命周期结束就会调用其对应的析构函数进而完成内存资源的释放。1.2. 智能指针的原理RAIIRAII 是 Resource Acquisition Is Initializaion 的缩写它是一种管理资源类的设计思想本质是一种利用对象生命周期来管理获取到的动态资源避免资源泄漏这里的资源可以是内存、文件指针、网络连接、互斥锁等。RAII 在获取资源时资源委托给一个对象接着控制对资源的访问资源在对象的生命周期内始终保持有效最后在对象析构的时候释放资源这样保障了资源的正常释放避免了资源泄露的问题。实现智能指针时需要考虑以下三个方面的问题在对象构造时获取资源在对象析构的时候释放资源利用对象的生命周期来控制程序资源即RAII特性。对 * 和 - 运算符进行重载使得该对象具有像指针一样的行为。智能指针对象的拷贝问题。为什么要解决智能指针对象的拷贝问题对于当前实现的 SmartPtr 类如果用一个 SmartPtr 对象来 拷贝另一个 SmartPtr 对象或是将一个 SmartPtr 对象赋值给另一个 SmartPtr 对象都会导致 程序崩溃例如int main() { SmartPtrint sp1(new int); SmartPtrint sp2(sp1); //拷贝构造 SmartPtrint sp3(new int); SmartPtrint sp4(new int); sp3 sp4; //拷贝赋值 return 0; }原因如下编译器默认生成的拷贝构造函数对内置类型完成值拷贝浅拷贝因此用sp1拷贝构造sp2后相当于这sp1和sp2管理了同一块内存空间当sp1和sp2析构时就会导致这块空间被释放两次。编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝浅拷贝因此将sp4赋值给sp3后相当于sp3和sp4管理的都是原来sp3管理的空间当sp3和sp4析构时就会导致这块空间被释放两次并且还会导致sp4原来管理的空间没有得到释放。需要注意的是智能指针就是要模拟原生指针的行为当我们将一个指针赋值给另一个指针时目的就是让这两个指针指向同一块内存空间所以这里本就应该进行浅拷贝但单纯的浅拷贝又会导致空间被多次释放因此根据解决智能指针拷贝问题方式的不同从而衍生出了不同版本的智能指针。二、C 中的智能指针2.1. std::auto_ptr介绍 auto_ptrauto_ptr 是 C98 设计出来的智能指针他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象但是这是一个非常糟糕的设计因为会遇到被拷贝对象悬空访问报错的问题强烈不建议使用 auto_ptr#include memory struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout ~Date() endl; } }; int main() { auto_ptrDate ap1(new Date); auto_ptrDate ap2(ap1); // 此时 ap1 就消失了对 Date 的管理了 ap1-_month; // error return 0; }auto_ptr 的模拟实现简易版的 auto_ptr 步骤如下在 构造函数中获取资源在析构函数中释放资源利用对象的生命周期来控制资源。对 * 和 - 运算符进行重载使 auto_ptr 对象具有指针一样的行为。在拷贝构造函数中用传入对象管理的资源来构造当前对象并将掺入对象管理资源的指针置空。在拷贝赋值函数中先将当前对象管理的资源释放然后再接管传入对象管理的资源最后将传入对象管理资源的指针置空。代码如下struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout ~Date() endl; } }; namespace xxhh { templateclass T class auto_ptr { public: auto_ptr(T* ptr):_ptr(ptr) {} // 拷贝构造函数 auto_ptr(auto_ptrT ap): _ptr(ap._ptr) { // 管理权转移 ap._ptr nullptr; } // 赋值拷贝函数 auto_ptr operator(auto_ptr ap) { if (this ! ap) { // 先释放 _ptr 的资源 if (_ptr) delete[] _ptr; // 把 ap 的资源 转给 _ptr _ptr ap._ptr; ap._ptr nullptr; } return *this; } T operator*() { return *_ptr; } T* operator-() { return _ptr; } ~auto_ptr() { if (_ptr) { cout delete [] _ptr endl; delete[] _ptr; } } private: T* _ptr; // 管理的资源 }; } int main() { xxhh::auto_ptrDate ap1(new Date); xxhh::auto_ptrDate ap2(ap1); ap1-_day; return 0; }2.2. std::unique_ptr介绍 unique_ptrunique_ptr 是 C11 设计出来的智能指针他的特点是不支持拷贝支持移动。int main() { unique_ptrint up1(new int); //unique_ptrint up2(up1); // error return 0; }实现unique_ptr简易版的unique_ptr的实现步骤如下在构造函数中获取资源在析构函数中释放资源利用对象的生命周期来控制资源。对*和-运算符进行重载使unique_ptr对象具有指针一样的行为。用C98的方式将拷贝构造函数和拷贝赋值函数声明为私有或者用C11的方式在这两个函数后面加上delete防止外部调用。代码如下namespace xxhh { templateclass T class unique_ptr { public: unique_ptr(T* ptr):_ptr(ptr) {} T operator*() { return *_ptr; } T* operator-() { return _ptr; } unique_ptrT operator(unique_ptrT up) delete; unique_ptrT(unique_ptrT up) delete; ~unique_ptr() { if (_ptr) { cout delete _ptr endl; delete _ptr; _ptr nullptr; } } private: T* _ptr; }; } int main() { xxhh::unique_ptrint up1(new int); //xxhh::unique_ptrint up2(up1); // error return 0; }2.3. std::shared_ptr介绍 shared_ptrshared_ptr是C11中引入的智能指针shared_ptr通过引用计数的方式解决智能指针的拷贝问题。每一个被管理的资源都有一个对应的引用计数通过这个引用计数记录着当前有多少个对象在管理着这块资源。当新增一个对象管理这块资源时则将该资源对应的引用计数进行当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了这时就可以将该资源进行释放了。通过这种引用计数的方式就能支持多个对象一起管理某一个资源也就是支持了智能指针的拷贝并且只有当一个资源对应的引用计数减为0时才会释放资源因此保证了同一个资源不会被释放多次。比如struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout ~Date() endl; } }; int main() { shared_ptrDate sp1(new Date); // 支持拷贝 shared_ptrDate sp2(sp1); shared_ptrDate sp3(sp2); cout sp1.use_count() endl; // 3 sp1-_year; cout sp1-_year endl; cout sp2-_year endl; cout sp3-_year endl; // 支持移动但是移动后sp1也悬空所以使用移动要谨慎 shared_ptrDate sp4(move(sp1)); cout sp4.use_count() endl; // 3 return 0; }说明一下 use_count成员函数用于获取当前对象管理的资源对应的引用计数。实现shared_ptr简易版的shared_ptr的实现步骤如下在shared_ptr类中增加一个成员变量count表示智能指针对象管理的资源对应的引用计数。在构造函数中获取资源并将该资源对应的引用计数设置为1表示当前只有一个对象在管理这个资源。在拷贝构造函数中与传入对象一起管理它管理的资源同时将该资源对应的引用计数。在拷贝赋值函数中先将当前对象管理的资源对应的引用计数--如果减为0则需要释放然后再与传入对象一起管理它管理的资源同时需要将该资源对应的引用计数。在析构函数中将管理资源对应的引用计数--如果减为0则需要将该资源释放。对*和-运算符进行重载使shared_ptr对象具有指针一样的行为。拷贝构造赋值拷贝代码如下namespace xxhh { templateclass T class shared_ptr { public: shared_ptr(T* ptr) :_ptr(ptr), _pcount(new int(1)) {} shared_ptr(shared_ptrT sp) :_ptr(sp._ptr) , _pcount(sp._pcount) { if (_ptr ! nullptr) (*_pcount); } shared_ptr operator(shared_ptrT sp) { if (_ptr ! sp._ptr) { // 先减去 sp 的引用计数 if ((*_pcount--) 0) { // 进行销毁 delete _ptr; delete _pcount; } _ptr sp._ptr; _pcount sp._pcount; (*_pcount); } return *this; } T operator*() { return *_ptr; } T* operator-() { return _ptr; } int use_count() { return *_pcount; } ~shared_ptr() { // 先检查 -- 后 引用计数是否为 0 if (--(*_pcount) 0) { if (_ptr ! nullptr) { cout delete: _ptr endl; delete _ptr; _ptr nullptr; } delete _pcount; _pcount nullptr; } } private: T* _ptr; int* _pcount; }; } struct Date { int _year; int _month; int _day; Date(int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout ~Date() endl; } }; int main() { xxhh::shared_ptrDate sp1(new Date); // 支持拷贝 xxhh::shared_ptrDate sp2(sp1); xxhh::shared_ptrDate sp3 sp2; cout sp1.use_count() endl; // 3 sp1-_year; cout sp1-_year endl; cout sp2-_year endl; cout sp3-_year endl; return 0; }shared_ptr 的线程安全问题shared_ptr的引用计数对象在堆上如果多个shared_ptr对象在多个线程中进⾏shared_ptr的拷贝析构时会访问修改引用计数就会存在线程安全问题所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。shared_ptr指向的对象也是有线程安全的问题的但是这个对象的线程安全问题不归shared_ptr管它也管不了应该有外层使用shared_ptr的进行线程安全的控制。最简单的方法就是把上面的 int* _pcount 改成 atomicint* _pcount。2.4. std::weak_ptrweak_ptr不支持RAII也不支持访问资源所以我们看文档发现weak_ptr构造时不支持绑定到资源只支持绑定到shared_ptr绑定到shared_ptr时不增加shared_ptr的引用计数那么就可以解决上述的循环引用问题。weak_ptr也没有重载operator*和operator-等因为他不参与资源管理那么如果他绑定的shared_ptr已经释放了资源那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的资源是否过期use_count也可获取shared_ptr的引用计数weak_ptr想访问资源时可以调用 lock返回⼀个管理资源的shared_ptr如果资源已经被释放返回的shared_ptr是⼀个空对象如果资源没有释放则通过返回的shared_ptr访问资源是安全的。int main() { std::shared_ptrstring sp1(new string(111111)); std::shared_ptrstring sp2(sp1); std::weak_ptrstring wp sp1; cout wp.expired() endl; cout wp.use_count() endl; // sp1和sp2都指向了其他资源则weak_ptr就过期了 sp1 make_sharedstring(222222); cout wp.expired() endl; cout wp.use_count() endl; sp2 make_sharedstring(333333); cout wp.expired() endl; cout wp.use_count() endl; wp sp1; //std::shared_ptrstring sp3 wp.lock(); auto sp3 wp.lock(); cout wp.expired() endl; cout wp.use_count() endl; *sp3 ###; cout *sp1 endl; return 0; }说明一下shared_ptr还会提供一个get 函数用于获取其管理的资源。三、C11和boost中智能指针的关系C11和boost中智能指针的关系C98中产生了第一个智能指针auto_ptr。Cboost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。CTR1引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。C11引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是unique_ptr对应的就是boost中的scoped_ptr并且这些智能指针的实现原理是参考boost中实现的。

相关新闻