智能指针的使用及其原理
目录1. 智能指针的使用2. RAII和智能指针3. C标准库智能指针的使用3.1 auto_ptr3.2 unique_ptr3.3 shared_ptr3.4 weak_ptr3.5 make_shared3.6 explicit 修饰构造函数4. 删除器4.1 数组的特化版本4.2 自定义删除器1. 智能指针的使用只要是堆上 new 出来的资源不想手动管理 delete优先考虑智能指针能用unique_ptr就不用shared_ptr。int main() { try { Func(); } catch (const char* errmsg) { cout errmsg endl; } catch (const std::exception e) { cout e.what() endl; } catch (...) { cout 未知异常 endl; } return 0; }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 (...) { //释放两块内存重新抛出异常 delete[] array1; delete[] array2; throw; } //正常执行路径释放 delete[] array1; delete[] array2; }两个场景分析场景 1Divide()函数抛出异常除 0进入catch(...)执行delete[] array1、array2内存正常释放。 这个场景看起来没问题。场景 2int* array2 new int[10];这里直接抛出异常内存分配失败new分配内存失败会抛出bad_alloc异常array1 new int[10]成功内存分配出来执行 array2 的 new 时抛异常函数立刻终止直接跳出 Func还没有进入 try 块catch 捕获不到array1指向的内存没有释放 → 内存泄漏使用 unique_ptr 改造#include memory void Func() { // 全部用智能指针管理 std::unique_ptrint[] array1 std::make_uniqueint[](10); std::unique_ptrint[] array2 std::make_uniqueint[](10); int len, time; cin len time; cout Divide(len, time) endl; // 不需要任何try-catch手动释放 }分析两种异常情况array2 构造时 new 抛异常array1是局部栈对象已经构造完成。发生异常函数退出array1 自动调用析构释放内存。Divide 函数抛异常 函数提前退出array1、array2都是局部对象离开作用域自动析构两块内存全部释放。2. RAII和智能指针RAII 全称Resource Acquisition Is Initialization资源获取即初始化。 它是 C 管理资源最核心的设计思想。核心思想一句话概括把资源交给局部对象托管对象构造时获取资源对象析构时释放资源依靠对象生命周期保证资源一定会释放。智能指针就是应用 RAII 思想封装裸指针的类模板。 只靠 RAII 还不够我们希望它用起来像普通指针所以还要重载运算符模拟指针行为。两大关键点遵循 RAII构造接收裸指针析构执行delete自动释放内存重载运算符operator*、operator-、operator[]让智能指针拥有和裸指针一样的访问语法。简易智能指针示例templateclass T class SmartPtr { public: // 构造函数获取资源接收new出来的裸指针RAII第一步 SmartPtr(T* ptr) :_ptr(ptr) {} // 析构函数释放资源RAII核心 ~SmartPtr() { cout delete[] _ptr endl; delete[] _ptr; } // 重载解引用 *sp T operator*() { return *_ptr; } // 重载箭头 -sp T* operator-() { return _ptr; } // 重载下标 [] 支持数组访问 sp[i] T operator[](size_t i) { return _ptr[i]; } private: T* _ptr; // 私有成员封装裸指针 };templateclass T类模板支持任意类型指针int*、自定义类型*私有成员T* _ptr封装裸指针资源保存在对象内部构造函数接收外部new出来的地址完成资源获取析构函数对象销毁自动执行delete[]自动释放堆内存三个运算符重载对外提供和原生指针一模一样的使用方式。3. C标准库智能指针的使用智能指针概览C 标准库提供了多种智能指针除了weak_ptr外它们都符合 RAII 行为。它们的主要区别在于拷贝时的思路不同auto_ptr(C98)拷贝时转移管理权已废弃。unique_ptr(C11)独占所有权不支持拷贝只支持移动。shared_ptr(C11)共享所有权基于引用计数。weak_ptr(C11)弱引用不控制生命周期用于解决循环引用问题。3.1 auto_ptrauto_ptr是 C98 设计出来的智能指针。它的特点是拷贝时会把被拷贝对象的资源管理权转移给拷贝对象。致命缺陷这是一个非常糟糕的设计。当发生拷贝时原对象被拷贝者会被置空悬空。如果后续继续访问原对象就会导致程序崩溃。代码示例#include iostream #include memory using namespace std; struct Date { int _year; Date(int year 1) : _year(year) {} ~Date() { cout ~Date() endl; } }; int main() { auto_ptrDate ap1(new Date); // 拷贝时管理权限转移被拷贝对象 ap1 悬空 auto_ptrDate ap2(ap1); // 空指针访问ap1 对象已经悬空下面代码会崩溃 // ap1-_year; return 0; }3.2 unique_ptrunique_ptr翻译过来是“唯一指针”。它的特点非常鲜明不支持拷贝只支持移动。适用场景如果不需要拷贝的场景非常建议使用它。移动语义虽然不支持拷贝但支持std::move移动。注意移动后原对象也会悬空所以使用移动要谨慎。示例#include iostream #include memory using namespace std; struct Date { int _year; Date(int year 1) : _year(year) {} ~Date() { cout ~Date() endl; } }; int main() { unique_ptrDate up1(new Date); // 不支持拷贝 // unique_ptrDate up2(up1); // 编译报错 // 支持移动但是移动后 up1 也悬空 unique_ptrDate up3(move(up1)); return 0; }3.3 shared_ptrshared_ptr是“共享指针”。它的特点是支持拷贝也支持移动。底层是通过引用计数的方式实现的。机制每当有一个新的shared_ptr指向同一块资源引用计数 1每当一个shared_ptr析构引用计数 -1。当引用计数为 0 时释放资源。适用场景如果需要拷贝的场景就需要使用它。示例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; // 输出 2 cout sp2-_year endl; // 输出 2 cout sp3-_year endl; // 输出 2 // 支持移动但是移动后 sp1 也悬空 shared_ptrDate sp4(move(sp1)); return 0; }3.4 weak_ptrweak_ptr是“弱指针”。它完全不同于上面的智能指针。特点不支持 RAII意味着不能用它直接管理资源。它不增加引用计数。产生本质是为了解决shared_ptr的循环引用导致内存泄漏的问题。weak_ptr 的核心价值通常发生在两个对象互相持有对方的 shared_ptr 时导致引用计数永远无法归零。错误版本#include iostream #include memory struct Node { int val; std::shared_ptrNode next; // shared_ptr互相引用 Node(int v) : val(v) { std::cout 构造 Node val \n; } ~Node() { std::cout 析构 Node val \n; } }; int main() { auto a std::make_sharedNode(1); auto b std::make_sharedNode(2); a-next b; b-next a; // 循环引用 // main函数结束a、b离开作用域 // 但是a、b内部互相持有shared_ptr引用计数 ! 0 // 析构函数不会调用 → 内存泄漏 return 0; }weak_ptr修复版本#include iostream #include memory struct Node { int val; std::weak_ptrNode next; // weak_ptr 弱指针不增加引用计数 Node(int v) : val(v) { std::cout 构造 Node val \n; } ~Node() { std::cout 析构 Node val \n; } }; int main() { auto a std::make_sharedNode(1); auto b std::make_sharedNode(2); a-next b; b-next a; // weak_ptr使用lock() 尝试提升为shared_ptr if (auto sp a-next.lock()) { std::cout a 的 next 值 sp-val \n; } // main结束a,b销毁weak_ptr不影响计数对象正常释放 return 0; }3.5 make_sharedshared_ptr除了支持用指向资源的指针构造还支持make_shared用初始化资源对象的值直接构造。template class T, class... Args shared_ptrT make_shared(Args... args);基本用法#include memory // 直接使用构造函数 std::shared_ptrFoo p1(new Foo(1, 2)); // 使用 make_shared推荐 auto p2 std::make_sharedFoo(1, 2);make_shared将参数完美转发给Foo的构造函数返回值是一个shared_ptrFoo。优势内存分配次数少直接构造shared_ptr需要两次内存分配对象本身 控制块而make_shared只分配一次对象和控制块在同一块内存。异常安全避免因参数求值顺序导致的内存泄漏。int main() { // 直接构造 shared_ptrDate sp1(new Date(2024, 9, 11)); // 使用 make_shared 构造 shared_ptrDate sp2 make_sharedDate(2024, 9, 11); // 配合 auto 使用更简洁 auto sp3 make_sharedDate(2024, 9, 11); return 0; }3.6 explicit 修饰构造函数shared_ptr和unique_ptr的构造函数都使用了explicit修饰。这是为了防止普通指针隐式类型转换成智能指针对象。#include iostream #include memory struct Date { int _year; Date(int year 1) : _year(year) {} ~Date() { std::cout ~Date() std::endl; } }; void takeSmartPtr(std::shared_ptrDate sp) { std::cout 管理权已移交当前引用计数: sp.use_count() std::endl; } int main() { // 错误禁止隐式转换 (编译报错) // shared_ptrDate sp1 new Date(); // takeSmartPtr(new Date()); // 正确必须显式调用构造函数 std::shared_ptrDate sp2(new Date()); takeSmartPtr(sp2); // 正确使用 make_shared (更加推荐) auto sp3 std::make_sharedDate(); takeSmartPtr(sp3); return 0; }4. 删除器智能指针析构时默认是进行delete释放资源。这就意味着如果不是new出来的资源交给智能指针管理析构时就会崩溃。为了解决这个问题智能指针支持在构造时给定一个删除器。删除器本质是一个可调用对象仿函数、函数指针、Lambda在智能指针析构时会调用它来释放资源。4.1 数组的特化版本因为new[]经常使用为了简洁unique_ptr和shared_ptr都特化了一份[]的版本。这样在析构时就会自动调用delete[]。// 如果没有特化直接这样写会崩溃因为默认 delete 不匹配 new[] // unique_ptrDate up1(new Date[10]); // 使用特化版本管理 new[] 的资源 unique_ptrDate[] up1(new Date[5]); shared_ptrDate[] sp1(new Date[5]);4.2 自定义删除器unique_ptr和shared_ptr支持删除器的方式有所不同unique_ptr是在类模板参数中支持的。shared_ptr是在构造函数参数中支持的。方法1templateclass T class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; // unique_ptr 需要将删除器类型作为模板参数 unique_ptrDate, DeleteArrayDate up2(new Date[5]); // shared_ptr 只需要在构造函数中传入删除器对象 shared_ptrDate sp2(new Date[5], DeleteArrayDate());方法2// 函数指针做删除器 templateclass T void DeleteArrayFunc(T* ptr) { delete[] ptr; } // unique_ptr 传入函数指针类型 unique_ptrDate, void(*)(Date*) up3(new Date[5], DeleteArrayFuncDate); // shared_ptr 直接传函数指针 shared_ptrDate sp3(new Date[5], DeleteArrayFuncDate); // Lambda 表达式做删除器 auto delArrOBJ [](Date* ptr) { delete[] ptr; }; // unique_ptr 使用 decltype 推导 Lambda 类型 unique_ptrDate, decltype(delArrOBJ) up4(new Date[5], delArrOBJ); // shared_ptr 直接传 Lambda shared_ptrDate sp4(new Date[5], delArrOBJ);
上一篇/下一篇内容由系统自动关联
返回资讯列表 →