c++异常和智能指针
2026/9/15 7:49:56 网站建设 项目流程

一,异常

1,异常概念

1.1,什么是异常?

异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。

C 语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出一个对象,这个对象可以携带更全面的各种信息。

1.2,异常的抛出和捕获

程序出现问题时,我们通过抛出 (throw) 一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个 catch 的处理代码来处理该异常。

被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。

当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从 throw 位置跳到与之匹配的 catch 模块,catch 可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数中的 catch,控制权从 throw 位置转移到了 catch 位置。这里还有两个重要的含义:1、沿着调用链的函数可能提早退出。2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。

抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在 catch 子句后销毁。(这里的处理类似于函数的传值返回)

1.3,栈展开

抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的 catch 子句,首先检查 throw 本身是否在 try 块内部,如果在则查找匹配的 catch 语句,如果有匹配的,则跳到 catch 的地方进行处理。

如果当前函数中没有 try/catch 子句,或者有 try/catch 子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的 catch 过程被称为栈展开。

如果到达 main 函数,依旧没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止程序。

如果找到匹配的 catch 子句处理后,catch 子句代码会继续执行。

#include<iostream> using namespace std; double Divide(int a, int b) { try { // 当b == 0时抛出异常 if (b == 0) { string s("Divide by zero condition!"); throw s; } else { return ((double)a / (double)b); } } catch (int errid) { cout << errid << endl; } return 0; } void Func() { int len, time; cin >> len >> time; try { cout << Divide(len, time) << endl; } //catch (const char* errmsg) catch (const string& errmsg) { cout << errmsg << endl; } cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl; } int main() { while (1) { try { Func(); } catch (const string& errmsg) { cout << errmsg << endl; } } return 0; }

2,异常规范

对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。

C++98 中函数参数列表的后面接 throw (),表示函数不抛异常,函数参数列表的后面接 throw (类型 1, 类型 2...) 表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。

C++98 的方式这种方式过于复杂,实践中并不好用,C++11 中进行了简化,函数参数列表后面加 noexcept 表示不会抛出异常,啥都不加表示可能会抛出异常。

编译器并不会在编译时检查 noexcept,也就是说如果一个函数用 noexcept 修饰了,但是同时又包含了 throw 语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的 (有些编译器可能会报个警告)。但是一个声明了 noexcept 的函数抛出了异常,程序会调用 terminate 终止程序。

noexcept (expression) 还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回 false,不会就返回 true。

3,异常安全问题

异常抛出后,后面的代码就不再执行,前面申请了资源 (内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后面再重新抛出,当然后面智能指针章节讲的 RAII 方式解决这种问题是更好的。

其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放 10 个资源,释放到第 5 个时抛出异常,则也需要捕获处理,否则后面的 5 个资源就没释放,也资源泄漏了。《Effctive C++》第 8 个条款也专门讲了这个问题,别让异常逃离析构函数。

#include<iostream> using namespace std; double Divide(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { /*throw "Division by zero condition!";*/ throw std::runtime_error("除零错误"); } return (double)a / (double)b; } void Func() { // 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array没有得到释放。 // 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再 // 重新抛出去。 int* array = new int[10]; try { int len, time; cin >> len >> time; cout << Divide(len, time) << endl; } catch (const char* s) { // 捕获异常释放内存 cout << "delete []" << array << endl; delete[] array; throw; // 异常重新抛出,捕获到什么抛出什么 } cout << "delete []" << array << endl; delete[] array; } int main() { try { Func(); } //catch (const char* errmsg) //{ // cout << errmsg << endl; //} catch (const exception& e) { cout << e.what() << endl; } //catch (...) //{ // cout << "Unkown Exception" << endl; //} return 0; }

二,智能指针

1,智能指针使用场景

#include<iostream> #include<string> using namespace std; double Divide(double x, double y) { try { if (y == 0) { string s("divide-by-zero error"); throw s; } } catch (const char* except) { cout << except << endl; } return x / y; } void func() { int* array1 = new int[10]; int* array2 = new int[20]; try { double a = 0, b = 0; cin >> a >> b; cout << Divide(a, b) << endl; } catch(int except) { cout << except << endl; } delete[] array1; array1 = nullptr; delete[] array2; array2 = nullptr; } int main() { while (1) { try { func(); } catch (const string& except) { cout << except << endl; } } return 0; }

通过以上代码,new之后,我们也delete了,但是如果除数为0,也就是y==0 为真,就会抛异常,会去找抛出的 string对象 s ,与该对象类型匹配且离抛出异常位置最近的那一个catch,最合适的是main函数里的catch(const string& except),所以,抛出异常后,就会退出当前函数,最后匹配到main函数里的catch,跳转至此处,不会在执行delete了,就会导致内存泄漏。

使用智能指针之前:

#include<iostream> #include<string> #include<exception> using namespace std; double Divide(double a, double b) { if (b == 0) { throw string("divide-by-zero error"); //throw std::runtime_error("divide-by-zero error"); } return a / b; } void func() { //如果这里发生除零错误抛异常,下面两个动态开辟的数组就不会释放, //所以这里捕获异常之后,并不处理异常,除零异常交给上层去处理,这里 //捕获了,再重新抛出去。但是如果array2动态开辟的时候抛异常呢,还需 //要套一层捕获释放逻辑,这里更好的解决方案是智能指针。不然现在这样代码很挫。 int* array1 = new int[10]; int* array2 = new int[10]; try { double a = 0, b = 0; cin >> a >> b; cout << Divide(a, b) << endl; } catch (...) { cout << "delete array1: " << array1 << endl; cout << "delete array2: " << array2 << endl; delete[] array1; delete[] array2; throw;//异常重新抛出,捕获到什么抛出什么 } cout << "delete array1: " << array1 << endl; cout << "delete array2: " << array2 << endl; delete[] array1; delete[] array2; } int main() { while (1) { try { func(); } catch (const string& str) { cout << str << endl; } catch (const exception& e) { cout << e.what() << endl; } catch (...) { cout << "unknown error" << endl; } } return 0; }

RAII 是 Resource Acquisition Is Initialization 的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII 在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

智能指针类除了满足 RAII 的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载 operator*/operator->/operator [] 等运算符,方便访问资源

使用自定义智能指针之后:

#include<iostream> #include<string> #include<exception> using namespace std; class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) ,_month(month) ,_day(day) { cout << "Date(int year = 0, int month = 0, int day = 0)" << endl; } ~Date() { _year = _month = _day = 0; cout << "~Date()" << endl; } int _year; int _month; int _day; }; template<class T> class smart_pointer { public: smart_pointer(T* ptr) :_ptr(ptr) { cout << "smart_pointer(T* ptr)---->构造函数" << endl; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T& operator[](int i) { return *(_ptr + i); } ~smart_pointer() { cout << "~smart_pointer()---->析构函数" << endl; delete[] _ptr; } private: T* _ptr; }; double Divide(double a, double b) { if (b == 0) { throw string("divide-by-zero error"); //throw std::runtime_error("divide-by-zero error"); } return a / b; } void func() { int* arr1 = new int[10]; int* arr2 = new int[10]; Date* pd1 = new Date[10]; *arr1 = 10; arr1[5] = 7; pd1->_month = 15; smart_pointer<int> array1 = new int[10]; smart_pointer<int> array2 = new int[10]; smart_pointer<Date> pd2 = new Date[10]; *array1 = 10; array1[5] = 7; pd2->_month = 15; try { double a = 0, b = 0; cin >> a >> b; cout << Divide(a, b) << endl; } catch (...) { throw;//异常重新抛出,捕获到什么抛出什么 } } int main() { while (1) { try { func(); } catch (const string& str) { cout << str << endl; } catch (const exception& e) { cout << e.what() << endl; } catch (...) { cout << "unknown error" << endl; } } return 0; }

但是这个自定义智能指针有个很严重的问题,就是不能拷贝,因为当前的拷贝我们没有写,就会使用编译器默认生成的拷贝,编译器默认生成的拷贝是浅拷贝(值拷贝),会让两个指针指向同一块空间,当我们改变其中一个指针指向的某个值时,另一个指针指向的值也会跟着改变,后面析构的时候,这块空间会被释放两次,程序直接崩溃。

2,c++标准库智能指针的使用

C++ 标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以使用了,智能指针有好几种,除了 weak_ptr 他们都符合 RAII 和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。

2.1,auto_ptr的使用及模拟实现

auto_ptr 是 C++98 时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会导致被拷贝对象悬空,访问报错的问题,C++11 设计出新的智能指针后,强烈建议不要使用 auto_ptr。其他 C++11 出来之前很多公司也是明令禁止使用这个智能指针的。

2.1.1,如何使用(不建议使用)

#include<iostream> #include<memory> using namespace std; class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) ,_month(month) ,_day(day) { cout << "Date(int year = 0, int month = 0, int day = 0)" << endl; } ~Date() { _year = _month = _day = 0; cout << "~Date()" << endl; } int _year; int _month; int _day; }; int main() { //auto_ptr<int> pi1 = new int(1);//不支持隐式类型转换 auto_ptr<int> pi1(new int(1)); Date* pd = new Date[5]{ {2021,4,23},{2017,9,5},{2019,10,18},{2025,1,9},{2026,12,28} }; //auto_ptr天生不支持数组管理,auto_ptr的析构函数(以及reset()方法)内部只会执行delete 指针,用于释放单个对象。 auto_ptr<Date> pd1(new Date[5]{ {2021,4,23},{2017,9,5},{2019,10,18},{2025,1,9},{2026,12,28} }); auto_ptr<int> pi2(new int[10]); printf("%d年%d月%d日\n", pd1.get()->_year, pd1->_month, (*(pd1))._day);//get()返回底层的指针 pi1.reset();//调用delete delete[] pd; //将 auto_ptr 内的指针设置为空指针(表示它不指向任何对象),而无需销毁当前由 auto_ptr 所指向的对象。 //该函数返回一个指向调用之前所指向对象的指针,现在不再有义务负责对该对象进行销毁操作。 auto ret = pd1.release(); return 0; }

2.1.2,模拟实现

#include<iostream> using namespace std; struct A { int _x; int _y; A(int x = 0, int y = 0) :_x(x) ,_y(y) { } }; namespace zsw { template<class T> class auto_ptr { public: explicit auto_ptr(T* ptr = nullptr) :_ptr(ptr) { } auto_ptr(auto_ptr<T>& pt) :_ptr(pt._ptr) { pt._ptr = nullptr; } T& operator*()noexcept { return *get(); } T* operator->()noexcept { return get(); } T* release()noexcept { T* ret = _ptr; _ptr = nullptr; return ret; } auto_ptr<T>& operator=(auto_ptr<T>& pt) { reset(pt.release()); return *this; } void reset(T* p = nullptr)noexcept { if (_ptr != p) { delete _ptr; } _ptr = p; } T* get()noexcept { return _ptr; } ~auto_ptr()noexcept { delete _ptr; } private: T* _ptr; }; } int main() { zsw::auto_ptr<int> p1(new int(9)); zsw::auto_ptr<int> p2(new int(15)); zsw::auto_ptr<int> p3; p3 = p1; cout << *p2 << endl; //不支持使用new[],会内存泄漏,如果A显示写析构,程序直接崩溃。 zsw::auto_ptr<A> pa1(new A[5]{ {3,4},{2,6},{8,5},{5,7},{3,1} }); cout << pa1->_x << ":" << pa1.get()->_y << endl; p1.reset(); p2.reset(); p3.reset(); pa1.release(); return 0; }

2.2,unique_ptr的使用及模拟实现

2.2.1,如何使用

unique_ptr 是 C++11 设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。

#include<iostream> #include<memory> #include<functional> using namespace std; class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) ,_month(month) ,_day(day) { cout << "Date(int year = 0, int month = 0, int day = 0)" << endl; } ~Date() { _year = _month = _day = 0; cout << "~Date()" << endl; } int _year; int _month; int _day; }; struct DeleteDate { void operator()(Date* ptr) { delete[] ptr; } }; void DeleteArray(Date* ptr) { delete[] ptr; } class Fclose { public: void operator()(FILE* ptr) { fclose(ptr); } }; int main() { unique_ptr<int> ui1(new int(9)); unique_ptr<int> ui2(unique_ptr<int>(new int(5))); unique_ptr<int[]> ui3(new int[5] {1, 2, 3, 4, 5}); //auto_ptr<Date> ai1(new Date(2026, 9, 13)); ////unique_ptr<int> ui4(ui1); //不支持拷贝 //cout << ui3 << endl; //重载为指针 ////cout << ai1 << endl;//未重载 //unique_ptr<Date> ud1(new Date(2026, 9, 13)); //cout << ud1->_year << ":" << ud1.get()->_month << ":" << ud1->_day << endl; //自定义类型数组 //unique_ptr<Date> ud2(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}});//这样写会崩 //正确写法 //1,new[],实现了一个特化版本 T[],这个特化版本析构时用delete[]。 unique_ptr<Date[]> ud2(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}}); //2,仿函数对象做删除器。 unique_ptr<Date,DeleteDate> ud3(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }); //3,lambda表达式做删除器 auto Del = [](Date* ptr) {delete[] ptr; }; unique_ptr < Date, decltype(Del)> ud4(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, Del); //4,函数指针 unique_ptr<Date, void(*)(Date*)> ud6(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, DeleteArray); //5,实现其他资源管理的删除器 unique_ptr<FILE, Fclose> uf1(fopen("test.cpp", "r"), Fclose()); //6,包装器 function<void(Date*)> fun1 = [](Date* ptr) {delete[] ptr; }; unique_ptr<Date, function<void(Date*)>> ud7(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, fun1); return 0; }

2.2.2,模拟实现

namespace zsw { template<class T> struct default_delete { void operator()(T* ptr) const noexcept { delete ptr; } }; template<class T> struct default_delete<T[]> { void operator()(T* ptr) const noexcept { delete[] ptr; } }; template<class T, class D = default_delete<T>> class unique_ptr { public: explicit unique_ptr(T* ptr = nullptr) noexcept : _ptr(ptr), _del() { } unique_ptr(T* ptr, D del) noexcept : _ptr(ptr), _del(del) { } unique_ptr(const unique_ptr<T, D>&) = delete; unique_ptr<T, D>& operator=(const unique_ptr<T, D>&) = delete; unique_ptr(unique_ptr<T, D>&& pt) noexcept : _ptr(pt._ptr), _del(pt._del) { pt._ptr = nullptr; } unique_ptr<T, D>& operator=(unique_ptr<T, D>&& pt) noexcept { if (this != &pt) { if (_ptr) _del(_ptr); _ptr = pt._ptr; _del = pt._del; pt._ptr = nullptr; } return *this; } ~unique_ptr() { if (_ptr) _del(_ptr); } T* release() noexcept { T* ret = _ptr; _ptr = nullptr; return ret; } void reset(T* pt = nullptr) noexcept { if (_ptr != pt) { T* old = _ptr; _ptr = pt; if (old) _del(old); } } T* get() const noexcept { return _ptr; } D& get_deleter() noexcept { return _del; } const D& get_deleter() const noexcept { return _del; } T& operator*() const { return *_ptr; } T* operator->() const noexcept { return _ptr; } explicit operator bool() const noexcept { return _ptr != nullptr; } T& operator[](int i) const { return _ptr[i]; } private: T* _ptr; D _del; }; template<class T, class D> class unique_ptr<T[], D> { public: explicit unique_ptr(T* ptr = nullptr) noexcept : _ptr(ptr), _del() { } unique_ptr(T* ptr, D del) noexcept : _ptr(ptr), _del(del) { } unique_ptr(const unique_ptr<T[], D>&) = delete; unique_ptr<T[], D>& operator=(const unique_ptr<T[], D>&) = delete; unique_ptr(unique_ptr<T[], D>&& pt) noexcept : _ptr(pt._ptr), _del(pt._del) { pt._ptr = nullptr; } unique_ptr<T[], D>& operator=(unique_ptr<T[], D>&& pt) noexcept { if (this != &pt) { if (_ptr) _del(_ptr); _ptr = pt._ptr; _del = pt._del; pt._ptr = nullptr; } return *this; } ~unique_ptr() { if (_ptr) _del(_ptr); } T* release() noexcept { T* ret = _ptr; _ptr = nullptr; return ret; } void reset(T* pt = nullptr) noexcept { if (_ptr != pt) { T* old = _ptr; _ptr = pt; if (old) _del(old); } } T* get() const noexcept { return _ptr; } D& get_deleter() noexcept { return _del; } const D& get_deleter() const noexcept { return _del; } T& operator[](size_t i) const { return _ptr[i]; } explicit operator bool() const noexcept { return _ptr != nullptr; } private: T* _ptr; D _del; }; }

2.3,shared_ptr和weak_ptr的使用及模拟实现

2.3.1,如何使用

shared_ptr 是 C++11 设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

weak_ptr 是 C++11 设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持 RAII,也就意味着不能用它直接管理资源,weak_ptr 的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。

智能指针析构时默认是进行 delete 释放资源,这也就意味着如果不是 new 出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为 new [] 经常使用,所以为了简洁一点,unique_ptr 和 shared_ptr 都特化了一份 [] 的版本,使用时 unique_ptr<Date []> up1 (new Date [5]);shared_ptr<Date []> sp1 (new Date [5]); 就可以管理 new [] 的资源。

template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args); shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。

shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回 false,否则返回 true,意味着我们可以直接把智能指针对象给 if 判断是否为空。

shared_ptr 和 unique_ptr 的构造函数都使用 explicit 修饰,防止普通指针隐式类型转换成智能指针对象。

#include<iostream> #include<memory> #include<functional> using namespace std; class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) ,_month(month) ,_day(day) { cout << "Date(int year = 0, int month = 0, int day = 0)" << endl; } ~Date() { _year = _month = _day = 0; cout << "~Date()" << endl; } int _year; int _month; int _day; }; struct DeleteDate { void operator()(Date* ptr) { delete[] ptr; } }; void Deletefunc(Date* ptr) { delete[] ptr; } int main() { shared_ptr<int> si1(new int(1)); shared_ptr<Date> sd1(new Date(2026, 7, 26)); cout << sd1 << endl; cout << sd1->_year << ":" << sd1.get()->_month << ":" << (*(sd1))._day << endl; //支持拷贝,引用计数实现 shared_ptr<Date> sd2(sd1); shared_ptr<Date> sd3(new Date(1999, 8, 4)); cout << sd2 << endl; cout << sd2->_year << ":" << sd2.get()->_month << ":" << (*(sd2))._day << endl; cout << sd1.use_count() << endl; cout << sd2.use_count() << endl; cout << sd1.unique() << endl; //0,nullptr也不是唯一的 sd1.reset(new Date(1999, 8, 4)); cout << sd1.unique() << endl; //1 shared_ptr<Date> sd4 = make_shared<Date>(2020, 12, 14); cout << sd4.use_count() << endl; cout << sd4.unique() << endl; cout << sd4 << endl; cout << sd4->_year << ":" << (*(sd4))._month << ":" << sd4.get()->_day << endl; //自定义类型数组 //shared_ptr<Date> sd11(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}});//这样写会崩 //正确写法 //1,new[],实现了一个特化版本 T[],这个特化版本析构时用delete[]。 shared_ptr<Date[]> sd11(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}}); //2,仿函数对象做删除器。 shared_ptr<Date> sd12(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, DeleteDate()); //3,lambda表达式做删除器 shared_ptr<Date> sd13(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, [](Date* ptr)->void {delete[] ptr; }); //4,函数指针 shared_ptr<Date> sd14(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, Deletefunc); //5,实现其他资源管理的删除器 shared_ptr<FILE> sf1(fopen("test.cpp", "r"), [](FILE* ptr)->void { cout << "fclose()" << endl; fclose(ptr); }); //6,包装器 function<void(Date*)> fun1 = [](Date* ptr) { cout << "function<void(Date*)>" << endl; delete[] ptr; }; shared_ptr<Date> sd15(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, fun1); return 0; }

2.3.2,智能指针原理

上面我们模拟实现了 auto_ptr 和 unique_ptr 的核心功能,这两个智能指针的实现比较简单,大家了解一下原理即可。auto_ptr 的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr 的思路是不支持拷贝。

重点要看看 shared_ptr 是如何设计的,尤其是引用计数的设计,这里是一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要 new 一个引用计数出来。多个 shared_ptr 指向资源时就 ++ 引用计数,shared_ptr 对象析构时就 -- 引用计数,引用计数减到 0 时代表当前析构的 shared_ptr 是最后一个管理资源的对象,则析构资源。

2.3.3,shared_ptr循环引用问题,引入weak_ptr

shared_ptr 大多数情况下管理资源非常合适,支持 RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用 weak_ptr 解决这种问题。

如下图所述场景,sp1 和 sp2 析构后,管理两个节点的引用计数减到 1。

1,右边的节点什么时候释放呢,左边节点中的_next 管着呢,_next 析构后,右边的节点就释放了。

2,_next 什么时候析构呢,_next 是左边节点的的成员,左边节点释放,_next 就析构了。

3,左边节点什么时候释放呢,左边节点由右边节点中的_prev 管着呢,_prev 析构后,左边的节点就释放了。

4,_prev 什么时候析构呢,_prev 是右边节点的成员,右边节点释放,_prev 就析构了。 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏。把 ListNode 结构体中的_next 和_prev 改成 weak_ptr,weak_ptr 绑定到 shared_ptr 时不会增加它的引用计数,_next 和_prev 不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题.

#include<iostream> #include<memory> using namespace std; template<class T> struct ListNode { T _data; //ListNode<T>* _next; //ListNode<T>* _prev; //shared_ptr<ListNode<T>> _next; //shared_ptr<ListNode<T>> _prev; weak_ptr<ListNode<T>> _next; weak_ptr<ListNode<T>> _prev; //ListNode(const T& data) // :_data(data) // ,_next(nullptr) // ,_prev(nullptr) //{ } ListNode(const T& data) :_data(data) { } ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode<int>> sp1(new ListNode<int>(3)); shared_ptr<ListNode<int>> sp2(new ListNode<int>(7)); cout << "sp1:" << sp1.use_count() << endl; // 1 cout << "sp2:" << sp2.use_count() << endl; // 1 sp1->_next = sp2; sp2->_prev = sp1; cout << "sp1:" << sp1.use_count() << endl; // 2-->1 cout << "sp2:" << sp2.use_count() << endl; // 2-->1 return 0; }

2.3.4,weak_ptr

weak_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 访问资源是安全的。

#include<iostream> #include<memory> #include<string> using namespace std; int main() { std::shared_ptr<string> sp1(new string("111111")); std::shared_ptr<string> sp2(sp1); std::weak_ptr<string> wp = sp1; cout << wp.expired() << endl; cout << wp.use_count() << endl; // sp1和sp2都指向了其他资源,则weak_ptr就过期了 sp1 = make_shared<string>("222222"); cout << wp.expired() << endl; cout << wp.use_count() << endl; sp2 = make_shared<string>("333333"); cout << wp.expired() << endl; cout << wp.use_count() << endl; wp = sp1; //std::shared_ptr<string> sp3 = wp.lock(); auto sp3 = wp.lock(); cout << wp.expired() << endl; cout << wp.use_count() << endl; *sp3 += "###"; cout << *sp1 << endl; return 0; }

2.3.5,模拟实现

#include<iostream> #include<functional> using namespace std; namespace zsw { template<class T> class shared_ptr { public: explicit shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _count(new int(1)) { cout << "explicit shared_ptr(T* ptr = nullptr)---->构造" << endl; } template<class D> shared_ptr(T* ptr, D del) : _ptr(ptr) , _count(new int(1)) , _del(del) { } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _count(sp._count) , _del(sp._del) { // 若拷贝的是已被移动走的对象(_count 为空),无需递增计数 if (_count) (*_count)++; cout << "shared_ptr(const shared_ptr<T>& sp)---->拷贝构造" << endl; } void swap(shared_ptr<T>& pt) { std::swap(_ptr, pt._ptr); std::swap(_count, pt._count); std::swap(_del, pt._del); } // 移动构造:直接“窃取” pt 的资源(指针、引用计数、删除器), // 不新建引用计数;随后把 pt 置空,防止其析构时重复释放。 shared_ptr(shared_ptr<T>&& pt) noexcept : _ptr(pt._ptr) , _count(pt._count) , _del(std::move(pt._del)) { pt._ptr = nullptr; pt._count = nullptr; cout << "shared_ptr(shared_ptr<T>&& pt)---->移动构造" << endl; } // 拷贝赋值:copy-and-swap 保证异常安全。 // 先用 pt 拷贝构造临时对象 tmp,再与 *this 交换; // 旧资源交由 tmp 析构时自动释放,天然支持自赋值与共享指针场景。 shared_ptr<T>& operator=(const shared_ptr<T>& pt) { cout << "shared_ptr<T>& operator=(const shared_ptr<T>& pt)---->拷贝赋值" << endl; if (this != &pt) { shared_ptr<T> tmp(pt); swap(tmp); } return *this; } // 移动赋值:先窃取 pt 的资源到 tmp,再与 *this 交换; // 本对象原有资源随 tmp 析构释放,pt 被置空。 shared_ptr<T>& operator=(shared_ptr<T>&& pt) noexcept { cout << "shared_ptr<T>& operator=(shared_ptr<T>&& pt)---->移动赋值" << endl; if (this != &pt) { shared_ptr<T> tmp(std::move(pt)); swap(tmp); } return *this; } T& operator*()const { return *_ptr; } T* operator->()const { return _ptr; } T* get()const { return _ptr; } int use_count()const { // 已被移动走的对象 _count 为空,返回 0 而不是解引用空指针 return _count ? *_count : 0; } bool unique()const { // 标准语义:仅当引用计数为 1 时独占 return use_count() == 1; } void reset(T* ptr = nullptr) { // 用 ptr 构造临时对象建立独立的引用计数,再交换; // 旧资源随 tmp 析构释放,避免残留旧计数或空指针。 shared_ptr<T> tmp(ptr); swap(tmp); } ~shared_ptr() { release(); } private: void release() { // 可重入:已被释放(_count 为空)时直接返回,防止二次析构崩溃 if (_count == nullptr) return; if (--(*_count) == 0) { _del(_ptr); delete _count; _ptr = nullptr; _count = nullptr; } } private: T* _ptr; // 引用计数:当前实现非线程安全;如需多线程共享请改用 std::atomic<int> int* _count; //atomic<int>* _count; function<void(T*)> _del = [](T* ptr)->void {delete ptr; }; }; template<class T> ostream& operator<<(ostream& out, const shared_ptr<T>& sp) { out << sp.get(); return out; } template<class T> class weak_ptr { public: weak_ptr() { } weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get()) { } weak_ptr<T>& operator=(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; } private: T* _ptr = nullptr; }; } class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) , _month(month) , _day(day) { cout << "Date(int year = 0, int month = 0, int day = 0)" << endl; } ~Date() { _year = _month = _day = 0; cout << "~Date()" << endl; } int _year; int _month; int _day; }; struct ListNode { int _data; //ListNode* _next; //ListNode* _prev; //会有循环引用问题 //zsw::shared_ptr<ListNode> _next; //zsw::shared_ptr<ListNode> _prev; //ListNode(int data = 0) // :_data(data) // , _next(nullptr) // , _prev(nullptr) //{ } //weak_ptr解决循环引用问题 zsw::weak_ptr<ListNode> _next; zsw::weak_ptr<ListNode> _prev; ListNode(int data = 0) :_data(data) { } ~ListNode() { cout << "~ListNode()" << endl; } }; void test1() { zsw::shared_ptr<int> sp1(new int(1)); std::shared_ptr<Date> sd1(new Date[5], [](Date* ptr)->void {delete[] ptr; }); zsw::shared_ptr<Date> mysd1(new Date[5], [](Date* ptr)->void {delete[] ptr; }); cout << sd1 << endl; cout << mysd1 << endl; cout << sd1.use_count() << endl; cout << mysd1.use_count() << endl; //引用计数测试 std::shared_ptr<Date> sd2(sd1); zsw::shared_ptr<Date> mysd2(mysd1); cout << sd2 << endl; cout << mysd2 << endl; cout << sd1.use_count() << endl; cout << mysd1.use_count() << endl; cout << sd2.use_count() << endl; cout << mysd2.use_count() << endl; zsw::shared_ptr<int> sp11(new int(1)); zsw::shared_ptr<int> sp21(sp11); zsw::shared_ptr<int> sp31(new int(4)); zsw::shared_ptr<int> sp41(move(sp31)); zsw::shared_ptr<int> sp51; sp51 = sp11; zsw::shared_ptr<int> sp61(new int(8)); zsw::shared_ptr<int> sp71(new int(19)); sp61 = move(sp71); } void test2() { zsw::shared_ptr<ListNode> sp1(new ListNode(5)); zsw::shared_ptr<ListNode> sp2(new ListNode(9)); sp1->_next = sp2; sp2->_prev = sp1; } int main() { test1();//基础测试 test2();//循环引用测试 return 0; }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询