主页 > 电脑硬件  > 

【C++】34.智能指针(1)

【C++】34.智能指针(1)

文章目录 1. 智能指针的使用场景分析2. RAII和智能指针的设计思路3. C++标准库智能指针的使用代码1:代码2:代码3:四个智能指针:auto_ptrunique_ptrshared_ptr(重要)weak_ptr 4. 智能指针的原理


1. 智能指针的使用场景分析

下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。

// 除法函数,可能抛出异常 double Divide(int a, int 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; // 可能抛出除0异常 } 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) // 捕获字符串异常(除0错误) { cout << errmsg << endl; } catch (const exception& e) // 捕获标准异常 { cout << e.what() << endl; } catch (...) // 捕获其他所有类型的异常 { cout << "未知异常" << endl; } return 0; }
2. RAII和智能指针的设计思路 RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

代码1:

// SmartPtr类:一个简单的智能指针模板类实现 template<class T> class SmartPtr { public: // 构造函数:接收一个原始指针 // RAII原则:Resource Acquisition Is Initialization(资源获取即初始化) // 当对象构造时获取资源,在对象析构时释放资源 // 防止资源泄露,保证异常安全 SmartPtr(T* ptr) :_ptr(ptr) // 初始化成员指针 {} // 析构函数:当智能指针对象销毁时自动调用 // 负责释放所管理的内存资源 ~SmartPtr() { cout << "delete:" << _ptr << endl; // 打印被删除的指针地址 delete _ptr; // 释放内存 } // 重载操作符* // 使智能指针可以像普通指针一样,通过*访问所指向的对象 T& operator*() { return *_ptr; } // 重载操作符-> // 使智能指针可以像普通指针一样通过->访问成员 T* operator->() { return _ptr; } private: T* _ptr; // 存储原始指针的成员变量 }; // 测试函数 void f() { // 创建管理pair<string, string>类型的智能指针对象 SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222")); SmartPtr<pair<string, string>> sp2(new pair<string, string>); SmartPtr<pair<string, string>> sp3(new pair<string, string>); // 创建管理string类型的智能指针对象 SmartPtr<string> sp4(new string("xxxxx")); // 使用重载的操作符访问数据 cout << *sp4 << endl; // 通过*操作符访问string内容,打印xxxxx cout << sp1->first << endl; // 通过->操作符访问pair的first成员,打印1111 cout << sp1->second << endl; // 通过->操作符访问pair的second成员,打印22222 div(); // 调用某个分隔函数(代码中未给出具体实现) } // 主函数:包含异常处理 int main() { try { f(); // 调用测试函数 } catch (const exception& e) // 捕获并处理标准异常 { cout << e.what() << endl; // 打印异常信息 } return 0; }

代码2:

// SmartPtr类:一个简单的智能指针模板类实现 template<class T> class SmartPtr { public: // 构造函数:接收一个原始指针 // RAII原则:Resource Acquisition Is Initialization(资源获取即初始化) // 当对象构造时获取资源,在对象析构时释放资源 // 防止资源泄露,保证异常安全 SmartPtr(T* ptr) :_ptr(ptr) // 初始化成员指针 {} // 析构函数:当智能指针对象销毁时自动调用 // 负责释放所管理的内存资源 ~SmartPtr() { cout << "delete:" << _ptr << endl; // 打印被删除的指针地址 delete _ptr; // 释放内存 } // 重载操作符* // 使智能指针可以像普通指针一样,通过*访问所指向的对象 T& operator*() { return *_ptr; } // 重载操作符-> // 使智能指针可以像普通指针一样通过->访问成员 T* operator->() { return _ptr; } private: T* _ptr; // 存储原始指针的成员变量 }; // C++98 auto_ptr // 管理权转移的智能指针,已被废弃 // 缺陷:管理权转移后,原指针悬空,可能导致访问空指针 // C++11 unique_ptr // 独占所有权的智能指针 // 特点:不允许拷贝,只允许移动,避免了auto_ptr的问题 // C++11 shared_ptr // 共享所有权的智能指针 // 特点:使用引用计数,多个指针可以共享同一块资源 // C++11 weak_ptr // 弱引用智能指针 // 特点:配合shared_ptr使用,不增加引用计数,用于解决循环引用问题 int main() { SmartPtr<string> sp1(new string("xxxxx")); SmartPtr<string> sp2(new string("yyyyy")); // 问题代码:这里会导致严重问题 // 1. 浅拷贝:sp2的指针直接赋值给sp1 // 2. 原有sp1的资源没有释放,导致内存泄漏 // 3. sp1和sp2指向同一块内存 // 4. 当sp1和sp2析构时,同一块内存会被删除两次,导致程序崩溃 sp1 = sp2; // 这是一个有问题的赋值操作! return 0; }

这就是为什么我们需要使用标准库提供的智能指针:

unique_ptr:适用于独占资源的场景shared_ptr:适用于资源需要共享的场景weak_ptr:适用于需要解决循环引用的场景

代码3:

// 智能指针模板类,用于自动管理动态分配的内存 template<class T> class SmartPtr { public: // 构造函数,接管原始指针 // RAII(Resource Acquisition Is Initialization)模式 // 在构造时获取资源,在析构时释放资源 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) { // 除数为0时抛出异常 if (b == 0) { throw "Divide by zero condition!"; } else { return (double)a / (double)b; } } void Func() { // 使用智能指针管理动态数组 // 不需要手动释放内存,智能指针会在函数结束或发生异常时自动释放 SmartPtr<int> sp1 = new int[10]; SmartPtr<int> 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; // 函数结束时,sp1和sp2的析构函数会自动调用,释放内存 } int main() { try { Func(); } // 异常处理 catch (const char* errmsg) { cout << errmsg << endl; } catch (const exception& e) { cout << e.what() << endl; } catch (...) { cout << "未知异常" << endl; } return 0; }
3. C++标准库智能指针的使用 C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。auto_ptr 是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。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 修饰,防止普通指针隐式类型转换成智能指针对象。 代码1: // 日期结构体,用于演示智能指针的行为 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() { // 1. auto_ptr (C++17已废弃) auto_ptr<Date> ap1(new Date); // auto_ptr的拷贝构造会转移所有权 // ap1在拷贝后变为空指针,这是一个危险的特性 auto_ptr<Date> ap2(ap1); // 危险:ap1已经为空,访问会导致未定义行为 //ap1->_year++; // 程序崩溃,但编译器不会提示错误 // 2. unique_ptr (推荐使用) unique_ptr<Date> up1(new Date); // unique_ptr禁止拷贝,体现独占所有权的语义 //unique_ptr<Date> up2(up1); // 编译错误 // unique_ptr支持移动语义 // 使用move显式转移所有权,up1在移动后变为空 unique_ptr<Date> up3(move(up1)); // 此时up1为空,up3获得对象的所有权 up1->_year++;// 编译错误!编译器会提示up1可能为空(编译会报错) // 3. shared_ptr (推荐使用) shared_ptr<Date> sp1(new Date); // shared_ptr支持拷贝,通过引用计数实现共享所有权 shared_ptr<Date> sp2(sp1); // 引用计数+1 shared_ptr<Date> sp3(sp2); // 引用计数+1 // 显示当前引用计数 cout << sp1.use_count() << endl; // 输出3 // 所有指向同一对象的shared_ptr都可以正常访问对象 sp1->_year++; cout << sp1->_year << endl; // 所有指针访问的是同一个对象 cout << sp2->_year << endl; // 值相同 cout << sp3->_year << endl; // 值相同 // shared_ptr也支持移动语义 // 移动后sp1变为空,但不影响sp2和sp3 shared_ptr<Date> sp4(move(sp1)); return 0; // 离开作用域时,最后一个shared_ptr销毁时才会删除Date对象 }
代码2: // 函数模板作为删除器,用于删除数组 template<class T> void DeleteArrayFunc(T* ptr) { delete[] ptr; } // 仿函数类作为删除器,用于删除数组 template<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; // 文件指针的删除器 class Fclose { public: void operator()(FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); } }; int main() { // 错误示范:直接使用new[]会导致析构错误 // unique_ptr<Date> up1(new Date[10]); // 错误:会用delete释放 // shared_ptr<Date> sp1(new Date[10]); // 错误:会用delete释放 // 方案1:使用数组特化版本 // C++标准库为数组提供了特化版本,自动使用delete[] unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]); // 方案2:使用自定义删除器 // 2.1 使用仿函数作为删除器 // unique_ptr要在模板参数中指定删除器类型 unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); // shared_ptr在构造函数中指定删除器对象 shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>()); // 2.2 使用函数指针作为删除器 // unique_ptr需要在模板参数中指定函数指针类型 unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>); // shared_ptr直接传入函数指针 shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>); // 2.3 使用lambda表达式作为删除器 auto delArrOBJ = [](Date* ptr) {delete[] ptr; }; // unique_ptr需要使用decltype推导lambda类型 unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); // shared_ptr直接传入lambda shared_ptr<Date> sp4(new Date[5], delArrOBJ); // 管理其他资源:文件指针 // 使用仿函数关闭文件 shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose()); // 使用lambda关闭文件 shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); }); return 0; }
代码3: int main() { // 1. 创建shared_ptr的几种方式 // 方式1:构造函数直接传递new的对象 shared_ptr<Date> sp1(new Date(2024, 9, 11)); // 方式2:使用make_shared函数(推荐) // make_shared更安全且效率更高,因为只分配一次内存 shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11); // 方式3:使用auto进行类型推导 auto sp3 = make_shared<Date>(2024, 9, 11); // 创建空的shared_ptr shared_ptr<Date> sp4; // 2. 智能指针的布尔转换 // 检查智能指针是否为空 // 实际上调用operator bool() if (sp1) cout << "sp1 is not nullptr" << endl; // 检查智能指针是否为空 if (!sp4) cout << "sp1 is nullptr" << endl; // 3. 错误用法示例 // 错误:不能直接用=赋值原始指针 // 必须使用显式构造或make_shared //shared_ptr<Date> sp5 = new Date(2024, 9, 11); // 编译错误 // 错误:同样不能直接赋值原始指针 //unique_ptr<Date> sp6 = new Date(2024, 9, 11); // 编译错误 // 正确的写法: shared_ptr<Date> sp5(new Date(2024, 9, 11)); unique_ptr<Date> sp6(new Date(2024, 9, 11)); return 0; }

代码4:

struct Node { A _val; // 节点存储的数据 // 方案1:普通指针 //Node* _next; // 指向下一个节点 //Node* _prev; // 指向前一个节点 // 缺点:需要手动管理内存,容易造成内存泄漏或重复释放 // 方案2:shared_ptr方式 //bit::shared_ptr<Node> _next; // 指向下一个节点的智能指针 //bit::shared_ptr<Node> _prev; // 指向前一个节点的智能指针 // 缺点:会导致循环引用,引用计数永不为0,无法释放内存 // 方案3:weak_ptr方式(最佳解决方案) bit::weak_ptr<Node> _next; // 弱引用指向下一节点 bit::weak_ptr<Node> _prev; // 弱引用指向前一节点 // 优点: // 1. weak_ptr不增加引用计数 // 2. 可以访问资源但不参与生命周期管理 // 3. 成功解决循环引用问题 }; int main() { // 方案1:使用普通指针 //Node* n1 = new Node; //Node* n2 = new Node; // 需要手动释放内存 //delete n1; //delete n2; // 方案2和3:使用智能指针 // 创建两个节点,初始引用计数均为1 bit::shared_ptr<Node> sp1(new Node); bit::shared_ptr<Node> sp2(new Node); // 打印初始引用计数 cout << sp1.use_count() << endl; // 输出1 cout << sp2.use_count() << endl; // 输出1 // 建立双向链接 sp1->_next = sp2; // 使用weak_ptr,不会增加sp2的引用计数 sp2->_prev = sp1; // 使用weak_ptr,不会增加sp1的引用计数 // 再次打印引用计数 cout << sp1.use_count() << endl; // 仍然是1 cout << sp2.use_count() << endl; // 仍然是1 // main函数结束时: // 1. sp1和sp2离开作用域 // 2. 引用计数变为0 // 3. Node对象被正确释放 return 0; }

这段代码演示了 shared_ptr 循环引用的问题,以及如何使用 weak_ptr 解决:

首先看结构体定义中三种不同的指针方式: struct Node { A _val; // 方式1:普通指针 // Node* _next; // Node* _prev; // 方式2:shared_ptr - 会导致循环引用 // bit::shared_ptr<Node> _next; // bit::shared_ptr<Node> _prev; // 方式3:weak_ptr - 解决循环引用 bit::weak_ptr<Node> _next; bit::weak_ptr<Node> _prev; }; 使用普通指针的问题: Node* n1 = new Node; Node* n2 = new Node; // ... 需要手动管理内存 delete n1; delete n2; 需要手动管理内存容易忘记释放导致内存泄漏不够安全 使用 shared_ptr 时的循环引用问题: bit::shared_ptr<Node> sp1(new Node); // sp1引用计数=1 bit::shared_ptr<Node> sp2(new Node); // sp2引用计数=1 cout << sp1.use_count() << endl; // 输出1 cout << sp2.use_count() << endl; // 输出1 // 创建循环引用 sp1->_next = sp2; // sp2的引用计数变为2 sp2->_prev = sp1; // sp1的引用计数变为2 cout << sp1.use_count() << endl; // 输出2 cout << sp2.use_count() << endl; // 输出2

问题:

sp1引用了sp2,sp2引用了sp1当main函数结束时,sp1和sp2的引用计数都不会降到0导致内存泄漏

为什么引用计数会变为2:

// 初始状态:每个shared_ptr引用计数为1 bit::shared_ptr<Node> sp1(new Node); // count = 1 bit::shared_ptr<Node> sp2(new Node); // count = 1 /* sp1 --> [Node1] count=1 sp2 --> [Node2] count=1 */ // 当执行 sp1->_next = sp2 时: // Node1的_next成员(它是shared_ptr)获得了一份sp2的拷贝 sp1->_next = sp2; // sp2的计数+1,变为2 /* sp1 ----------------> [Node1] count=1 | | _next ↓ sp2 ----------------> [Node2] count=2 */ // 当执行 sp2->_prev = sp1 时: // Node2的_prev成员(它是shared_ptr)获得了一份sp1的拷贝 sp2->_prev = sp1; // sp1的计数+1,变为2 /* sp1 ----------------> [Node1] count=2 <---- _prev | | | _next | ↓ | sp2 ----------------> [Node2] count=2 ------ */

关键点:

每个Node结构体中的 _next 和 _prev 是 shared_ptr当一个 shared_ptr 被赋值给另一个 shared_ptr 时,会调用拷贝构造或赋值运算符这会导致引用计数增加

所以:

sp1->_next = sp2 相当于又产生了一个指向Node2的 shared_ptrsp2->_prev = sp1 相当于又产生了一个指向Node1的 shared_ptr

这就是为什么:

Node1被sp1和Node2的_prev共同指向,引用计数为2Node2被sp2和Node1的_next共同指向,引用计数为2

这种情况下,即使main函数结束,sp1和sp2离开作用域:

sp1的计数从2减为1(Node2的_prev还在引用它)sp2的计数从2减为1(Node1的_next还在引用它)由于计数都不为0,两个Node对象都不会被释放造成内存泄漏

这就是为什么需要使用 weak_ptr - 它可以指向对象但不增加引用计数,从而打破这种循环引用。


使用 weak_ptr 解决循环引用: bit::shared_ptr<Node> sp1(new Node); bit::shared_ptr<Node> sp2(new Node); sp1->_next = sp2; // weak_ptr不增加引用计数 sp2->_prev = sp1; // weak_ptr不增加引用计数 cout << sp1.use_count() << endl; // 仍然是1 cout << sp2.use_count() << endl; // 仍然是1

weak_ptr的优势:

不增加引用计数可以访问资源不参与资源的生命周期管理当原始的shared_ptr被销毁时,weak_ptr会自动变为无效

总结:

weak_ptr 专门用来解决 shared_ptr 循环引用的问题weak_ptr 不是 RAII 智能指针,它不管理资源的生命周期在需要打破循环引用的地方使用 weak_ptr 替代 shared_ptr
四个智能指针:

四个智能指针:

auto_ptrunique_ptrshared_ptr🌟weak_ptr auto_ptr template<class T> class auto_ptr { public: // RAII // 像指针一样 auto_ptr(T* ptr) :_ptr(ptr) {} ~auto_ptr() { cout << "delete:" << _ptr << endl; delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } // ap3(ap1) // 管理权转移 auto_ptr(auto_ptr<T>& ap) :_ptr(ap._ptr) { ap._ptr = nullptr; } private: T* _ptr; };
unique_ptr template<class T> class unique_ptr { public: // RAII // 像指针一样 unique_ptr(T* ptr) :_ptr(ptr) {} ~unique_ptr() { cout << "delete:" << _ptr << endl; delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } // ap3(ap1) // 管理权转移 // 防拷贝 unique_ptr(unique_ptr<T>& ap) = delete; unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete; private: T* _ptr; };
shared_ptr(重要) template<class T> class shared_ptr { public: // 构造函数:创建新的shared_ptr对象 // ptr: 指向要管理的对象的指针 // 初始化:设置指针和引用计数(初始为1) shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pcount(new int(1)) // 新建计数器,初始值为1 {} // 析构函数:当shared_ptr对象销毁时调用 // 减少引用计数,当计数为0时释放资源 ~shared_ptr() { if (--(*_pcount) == 0) // 引用计数减1,如果变为0 { cout << "delete:" << _ptr << endl; delete _ptr; // 释放管理的对象 delete _pcount; // 释放计数器 } } // 重载*运算符:访问管理的对象 T& operator*() { return *_ptr; } // 重载->运算符:访问管理的对象的成员 T* operator->() { return _ptr; } // 拷贝构造函数:创建一个新的shared_ptr,共享同一个对象 // sp: 现有的shared_ptr对象 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) // 共享被管理的对象 , _pcount(sp._pcount) // 共享引用计数 { ++(*_pcount); // 增加引用计数 } // 赋值运算符:将一个shared_ptr赋值给另一个 // 需要处理自赋值和原有资源的释放 shared_ptr<T>& operator=(const shared_ptr<T>& sp) { // 自赋值检查 if (_ptr == sp._ptr) return *this; // 释放原有资源(如果引用计数变为0) if (--(*_pcount) == 0) { delete _ptr; delete _pcount; } // 共享新资源 _ptr = sp._ptr; _pcount = sp._pcount; ++(*_pcount); // 增加新资源的引用计数 return *this; } // 获取当前引用计数 int use_count() const { return *_pcount; } // 获取原始指针 T* get() const { return _ptr; } private: T* _ptr; // 指向管理的对象的指针 int* _pcount; // 指向引用计数的指针 // 不能用static int _pcount的原因: // 1. static成员是类共享的,所有shared_ptr实例会共享同一个计数 // 2. 无法区分不同对象组的引用计数 // 3. 不能正确管理多个不同对象的生命周期 };

这里不能用static int _pcount;而是用int* _pcount;这涉及到 shared_ptr 的核心设计:

// 错误方式:使用静态计数器 template<class T> class SharedPtr { private: T* _ptr; static int _pcount; // 静态成员,所有SharedPtr对象共享同一个计数器 }; int main() { // 问题演示 SharedPtr<Date> sp1(new Date(2024, 2, 14)); // _pcount = 1 SharedPtr<Date> sp2(new Date(2025, 1, 1)); // _pcount = 2 // 问题:sp1和sp2共享同一个静态计数器! // 无法区分不同对象的引用计数 }

正确方式:每组共享指针有自己的计数器:

template<class T> class SharedPtr { private: T* _ptr; // 指向实际对象的指针 int* _pcount; // 指向引用计数的指针 public: SharedPtr(T* ptr = nullptr) : _ptr(ptr) , _pcount(new int(1)) // 每个新对象组都有自己的计数器 {} }; int main() { // 正确示例 SharedPtr<Date> sp1(new Date(2024, 2, 14)); // _pcount指向值为1的计数器 SharedPtr<Date> sp2(sp1); // 共享sp1的计数器,值增加到2 SharedPtr<Date> sp3(new Date(2025, 1, 1)); // 新的计数器,值为1 SharedPtr<Date> sp4(sp3); // 共享sp3的计数器,值为2 // sp1和sp2共享一个计数器 // sp3和sp4共享另一个计数器 }

使用静态计数器的问题:

所有 SharedPtr 对象会共享同一个计数器无法区分不同对象组的引用计数不能正确管理多个不同对象的生命周期

使用成员指针计数器的优势:

每组共享同一对象的指针有自己的计数器可以准确跟踪每个对象被引用的次数当计数器归零时,才释放对应的对象

这就是为什么要将计数器设计为指针成员而不是静态成员的原因。


weak_ptr template<class T> class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get()) {} weak_ptr<T>& operator=(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
4. 智能指针的原理 下面我们模拟实现了auto_ptr和unique_ptr的核心功能,这两个智能指针的实现比较简单,大家了解一下原理即可。auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr的思路是不支持拷贝。大家重点要看看shared_ptr是如何设计的,尤其是引用计数的设计,主要这里一份资源就需要一个引用计数,所以引用计数才用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就–引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。

namespace bit { // auto_ptr实现 - C++17已废弃的设计 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) { delete _ptr; // 释放当前资源 _ptr = ap._ptr; // 获取ap的资源 ap._ptr = nullptr; // ap置空 } return *this; } ~auto_ptr() { if (_ptr) { cout << "delete:" << _ptr << endl; delete _ptr; } } // 指针操作符重载 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; }; // unique_ptr实现 - 独占所有权 template<class T> class unique_ptr { public: // explicit防止隐式转换 explicit 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; // 支持移动语义 unique_ptr(unique_ptr<T>&& sp) :_ptr(sp._ptr) { sp._ptr = nullptr; } unique_ptr<T>& operator=(unique_ptr<T>&& sp) { delete _ptr; _ptr = sp._ptr; sp._ptr = nullptr; return *this; } private: T* _ptr; }; // shared_ptr实现 - 共享所有权 template<class T> class shared_ptr { public: // 构造函数 explicit shared_ptr(T* ptr = nullptr) : _ptr(ptr) , _pcount(new int(1)) {} // 支持自定义删除器的构造 template<class D> shared_ptr(T* ptr, D del) : _ptr(ptr) , _pcount(new int(1)) , _del(del) {} // 拷贝构造 - 增加引用计数 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pcount(sp._pcount) , _del(sp._del) { ++(*_pcount); } // 释放资源 void release() { if (--(*_pcount) == 0) { _del(_ptr); // 使用删除器释放资源 delete _pcount; _ptr = nullptr; _pcount = nullptr; } } // 赋值运算符 shared_ptr<T>& operator=(const shared_ptr<T>& sp) { if (_ptr != sp._ptr) { release(); _ptr = sp._ptr; _pcount = sp._pcount; ++(*_pcount); _del = sp._del; } return *this; } ~shared_ptr() { release(); } // 访问接口 T* get() const { return _ptr; } int use_count() const { return *_pcount; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; // 管理的资源指针 int* _pcount; // 引用计数 // 在实际实现中应使用原子计数 //atomic<int>* _pcount; // 默认删除器 function<void(T*)> _del = [](T* ptr) {delete ptr; }; }; // weak_ptr实现 - 弱引用 template<class T> class weak_ptr { public: weak_ptr() {} // 通过shared_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; }; } int main() { // auto_ptr测试 - 展示所有权转移的问题 bit::auto_ptr<Date> ap1(new Date); bit::auto_ptr<Date> ap2(ap1); // ap1变为空 //ap1->_year++; // 错误:ap1已经为空 // unique_ptr测试 - 展示独占所有权 bit::unique_ptr<Date> up1(new Date); //bit::unique_ptr<Date> up2(up1); // 错误:不能拷贝 bit::unique_ptr<Date> up3(move(up1)); // 正确:可以移动 // shared_ptr测试 - 展示共享所有权 bit::shared_ptr<Date> sp1(new Date); bit::shared_ptr<Date> sp2(sp1); // 引用计数增加 bit::shared_ptr<Date> sp3(sp2); // 引用计数增加 cout << sp1.use_count() << endl; // 输出引用计数 sp1->_year++; // 修改共享对象 cout << sp1->_year << endl; // 所有指针看到相同的值 cout << sp2->_year << endl; cout << sp3->_year << endl; return 0; }
标签:

【C++】34.智能指针(1)由讯客互联电脑硬件栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【C++】34.智能指针(1)