C++ 智能指针_详细解释 📅 2026/8/5 14:52:44 前置说明智能指针基于RAII资源获取即初始化机制自动管理堆内存核心解决裸指针的内存泄漏问题把堆内存的生命周期绑定到栈上的智能指针对象当智能指针对象超出作用域时析构函数会自动释放绑定的堆内存无需手动delete。C 中的裸指针int* p new int;需要手动管理内存忘记delete会导致内存泄漏多次delete会导致未定义行为异常场景下比如delete前抛出异常内存也无法释放。C 标准库在memory头文件中提供了三种核心智能指针unique_ptr、shared_ptr、weak_ptr其中auto_ptr已被废弃设计缺陷。unique_ptr是独占式智能指针轻量、高效shared_ptr是共享式引用计数weak_ptr辅助shared_ptr解决循环引用使用智能指针的核心原则优先选unique_ptr避免裸指针与智能指针混用警惕shared_ptr的循环引用。智能指针的使用原则:优先使用unique_ptr高效、无额外开销仅在需要共享所有权时使用shared_ptr避免用同一个裸指针创建多个智能指针会导致重复释放不要手动delete智能指针管理的裸指针智能指针析构时会再次deleteshared_ptr的循环引用必须用weak_ptr解决优先使用make_unique/make_shared创建智能指针异常安全、更高效。1. std::unique_ptr独占式智能指针特性与使用场景独占所有权同一时间只有一个unique_ptr指向资源拷贝构造 / 赋值被禁用仅支持移动std::move轻量级无额外内存开销仅封装裸指针 删除器效率接近裸指针适用场景函数返回值、容器元素如std::vectorunique_ptrFoo、独占资源的管理如文件句柄、网络连接。核心签名类模板声明// 基础版本针对单个对象 template class T, class Deleter std::default_deleteT class unique_ptr; // 数组特化版本针对动态数组 new T[] template class T, class Deleter class unique_ptrT[], Deleter;部分含义template class T, class Deleter std::default_deleteT模板参数-T智能指针指向的对象类型如int、std::string、自定义类-Deleter删除器类型负责释放资源的函数 / 仿函数默认是std::default_deleteT调用delete释放单个对象unique_ptrT[], Deleter数组特化版本专门处理new T[]分配的动态数组默认删除器会调用delete[]而非普通版本的delete关键成员函数签名常用// 1. 移动构造独占所有权仅支持移动禁止拷贝 template class U, class E unique_ptr(unique_ptrU, E u) noexcept; // 2. 重置释放当前资源接管新资源 void reset(pointer p pointer()) noexcept; // 3. 释放所有权返回裸指针智能指针不再管理 pointer release() noexcept; // 4. C14 辅助创建函数更安全避免裸指针 template class T, class... Args unique_ptrT make_unique(Args... args); // 数组版本的make_unique template class T unique_ptrT make_unique(size_t n);示例代码#include iostream #include memory class MyClass { public: MyClass(int val) : value(val) { std::cout MyClass 构造 value std::endl; } ~MyClass() { std::cout MyClass 析构 value std::endl; } void show() { std::cout 值 value std::endl; } private: int value; }; int main() { // 1. 创建shared_ptr推荐用make_shared更高效 std::shared_ptrMyClass ptr1 std::make_sharedMyClass(10); std::cout 引用计数 ptr1.use_count() std::endl; // 输出1 // 2. 拷贝引用计数1 std::shared_ptrMyClass ptr2 ptr1; std::cout 引用计数 ptr1.use_count() std::endl; // 输出2 std::cout 引用计数 ptr2.use_count() std::endl; // 输出2 // 3. 多个指针共享资源 ptr1-show(); // 输出值10 ptr2-show(); // 输出值10 // 4. 重置指针引用计数-1 ptr1.reset(); std::cout 引用计数 ptr2.use_count() std::endl; // 输出1 // 5. 管理数组C17支持make_shared数组C11/14需手动new std::shared_ptrint[] arr_ptr(new int[5]{1,2,3,4,5}); arr_ptr[1] 200; std::cout arr_ptr[1] std::endl; // 输出200 return 0; // ptr2析构引用计数变为0资源释放 }2. std::shared_ptr共享式智能指针特性与使用场景共享所有权多个shared_ptr指向同一资源每新增一个shared_ptr指向该资源引用计数 1每销毁一个shared_ptr引用计数 - 1引用计数为 0 时自动释放资源线程安全引用计数的增减是原子操作但访问 / 修改指向的对象需手动加锁适用场景需要多个对象共享同一资源的场景如多线程访问同一数据、对象树的交叉引用。核心签名类模板声明template class T class shared_ptr;关键辅助函数 / 转换函数签名// 1. 高效创建一次内存分配对象控制块比直接new更优 template class T, class... Args shared_ptrT make_shared(Args... args); // 2. 类型转换对应普通指针的cast保证引用计数正确 // static_cast 等价版本 template class T, class U shared_ptrT static_pointer_cast(const shared_ptrU r) noexcept; // dynamic_cast 等价版本运行时类型检查 template class T, class U shared_ptrT dynamic_pointer_cast(const shared_ptrU r) noexcept; // const_cast 等价版本 template class T, class U shared_ptrT const_pointer_cast(const shared_ptrU r) noexcept;核心成员函数签名// 1. 拷贝构造增加引用计数 template class U shared_ptr(const shared_ptrU r) noexcept; // 2. 赋值重载引用计数增减 template class U shared_ptr operator(const shared_ptrU r) noexcept; // 3. 获取引用计数 long use_count() const noexcept; // 4. 检查是否是唯一持有者 bool unique() const noexcept; // 5. 重置释放当前引用引用计数-1 void reset() noexcept;部分含义template class T仅需指定指向的对象类型T控制块存储引用计数、删除器等是内部隐式创建的make_sharedArgs... args完美转发参数给T的构造函数在堆上创建T对象并绑定到shared_ptrstatic/dynamic/const_pointer_cast避免直接对shared_ptr的裸指针做 cast会导致多个控制块保证类型转换后引用计数统一示例代码#include iostream #include memory class MyClass { public: MyClass(int val) : value(val) { std::cout MyClass 构造 value std::endl; } ~MyClass() { std::cout MyClass 析构 value std::endl; } void show() { std::cout 值 value std::endl; } private: int value; }; int main() { // 1. 创建shared_ptr推荐用make_shared更高效 std::shared_ptrMyClass ptr1 std::make_sharedMyClass(10); std::cout 引用计数 ptr1.use_count() std::endl; // 输出1 // 2. 拷贝引用计数1 std::shared_ptrMyClass ptr2 ptr1; std::cout 引用计数 ptr1.use_count() std::endl; // 输出2 std::cout 引用计数 ptr2.use_count() std::endl; // 输出2 // 3. 多个指针共享资源 ptr1-show(); // 输出值10 ptr2-show(); // 输出值10 // 4. 重置指针引用计数-1 ptr1.reset(); std::cout 引用计数 ptr2.use_count() std::endl; // 输出1 // 5. 管理数组C17支持make_shared数组C11/14需手动new std::shared_ptrint[] arr_ptr(new int[5]{1,2,3,4,5}); arr_ptr[1] 200; std::cout arr_ptr[1] std::endl; // 输出200 return 0; // ptr2析构引用计数变为0资源释放 }关键问题循环引用shared_ptr的最大陷阱是循环引用两个shared_ptr互相指向对方导致引用计数永远无法变为 0最终内存泄漏。循环引用示例错误#include iostream #include memory class B; // 前向声明 class A { public: std::shared_ptrB b_ptr; ~A() { std::cout A 析构 std::endl; } }; class B { public: std::shared_ptrA a_ptr; ~B() { std::cout B 析构 std::endl; } }; int main() { std::shared_ptrA a std::make_sharedA(); std::shared_ptrB b std::make_sharedB(); a-b_ptr b; // A引用B b-a_ptr a; // B引用A // 循环引用a和b的引用计数都是2析构时各减1变为1永远不会释放 return 0; // 不会输出A 析构和B 析构内存泄漏 }3. std::weak_ptr弱引用智能指针特性与使用场景弱引用不拥有资源不增加shared_ptr的引用计数不影响资源释放解决循环引用shared_ptr循环引用会导致引用计数无法归零weak_ptr可打破循环不能直接解引用必须通过lock()转为shared_ptr后才能访问资源。签名类模板声明template class T class weak_ptr;成员函数签名// 1. 从shared_ptr构造不增加引用计数 template class U weak_ptr(const shared_ptrU r) noexcept; // 2. 锁定为shared_ptr安全访问资源 shared_ptrT lock() const noexcept; // 3. 检查资源是否已释放过期 bool expired() const noexcept; // 4. 获取对应的shared_ptr引用计数仅参考可能瞬时变化 long use_count() const noexcept; // 5. 重置清空弱引用 void reset() noexcept;部分含义template class T指向的对象类型与shared_ptr一致lock()核心函数返回一个shared_ptr若资源未释放则引用计数 1否则返回空shared_ptr是访问弱引用资源的唯一安全方式expired()等价于use_count() 0但更高效无需获取精确的引用计数值解决循环引用的示例正确#include iostream #include memory class B; class A { public: std::weak_ptrB b_ptr; // 改为weak_ptr ~A() { std::cout A 析构 std::endl; } }; class B { public: std::weak_ptrA a_ptr; // 改为weak_ptr ~B() { std::cout B 析构 std::endl; } }; int main() { std::shared_ptrA a std::make_sharedA(); std::shared_ptrB b std::make_sharedB(); a-b_ptr b; // weak_ptr不增加引用计数 b-a_ptr a; // weak_ptr不增加引用计数 // 检查资源是否存在 if (auto temp a-b_ptr.lock()) { // lock()返回shared_ptr若资源存在则有效 std::cout B资源存在 std::endl; } return 0; // a和b析构引用计数变为0资源释放输出A 析构和B 析构 }关键说明weak_ptr不能直接访问资源没有-和*运算符必须通过lock()获取shared_ptr后才能访问expired()方法可以判断weak_ptr指向的资源是否已释放返回true表示已释放weak_ptr的大小和shared_ptr相同因为要存储引用计数的指针。内存泄漏案例以及改良方案内存泄漏的核心本质是堆内存被分配后失去了对它的所有引用导致程序无法再释放这块内存直到程序退出系统会回收但长期运行的程序如服务器会持续占用内存。常见的内存泄漏场景1. 最基础忘记释放手动分配的内存这是新手最易犯的错误 —— 用new/malloc分配堆内存后未调用delete/free释放尤其是在分支、循环等复杂逻辑中更容易遗漏。示例代码错误#include iostream using namespace std; void func() { // 分配堆内存 int* p new int(10); string name test; // 分支逻辑导致忘记释放 if (name test) { cout 分支返回遗漏delete endl; return; // 直接返回p指向的内存永远无法释放 } // 只有走else才会释放本例不会执行 delete p; } int main() { func(); // 执行后内存泄漏4字节int return 0; }避免方法优先使用智能指针unique_ptr/shared_ptr替代裸指针若必须用裸指针遵循 “分配即规划释放” 原则在分配内存时就确定释放的位置。修复后的完整代码#include iostream #include memory // 必须包含智能指针的头文件 using namespace std; void func() { // 用unique_ptr替代裸指针make_unique是创建unique_ptr的推荐方式 unique_ptrint p make_uniqueint(10); string name test; if (name test) { cout 分支返回智能指针自动释放内存 endl; return; // 即使提前返回p也会析构并释放内存 } // 无需手动delete智能指针超出作用域时会自动释放 // 原来的delete p 可以完全删除 } int main() { func(); // 执行后无内存泄漏 return 0; }2. 异常导致的内存泄漏new分配内存后delete执行前抛出异常导致delete语句无法执行进而泄漏内存。这是比 “忘记释放” 更隐蔽的问题。示例代码错误#include iostream #include stdexcept using namespace std; void riskyFunc() { throw runtime_error(突发异常); // 抛出异常 } void func() { int* p new int(20); // 分配内存 riskyFunc(); // 抛出异常后续代码全部跳过 delete p; // 永远执行不到内存泄漏 } int main() { try { func(); } catch (const exception e) { cout 捕获异常 e.what() endl; } return 0; }避免方法核心方案使用智能指针RAII 机制即使抛出异常智能指针对象析构时仍会自动释放内存兜底方案用try-catch包裹但代码冗余且易遗漏不如智能指针可靠。修复后的代码#include iostream #include stdexcept #include memory // 智能指针头文件 using namespace std; void riskyFunc() { throw runtime_error(突发异常); } void func() { unique_ptrint p make_uniqueint(20); // 智能指针 riskyFunc(); // 抛异常也不影响p析构时自动释放 } int main() { try { func(); } catch (const exception e) { cout 捕获异常 e.what() endl; } return 0; // 无内存泄漏 }3. shared_ptr 的循环引用进阶陷阱这是使用智能指针时的高频错误 —— 两个或多个shared_ptr互相持有对方的引用导致引用计数永远无法归 0内存无法释放。示例代码错误#include iostream #include memory using namespace std; class B; // 前向声明 class A { public: shared_ptrB b_ptr; // A持有B的shared_ptr ~A() { cout A 析构 endl; } // 不会执行 }; class B { public: shared_ptrA a_ptr; // B持有A的shared_ptr ~B() { cout B 析构 endl; } // 不会执行 }; int main() { shared_ptrA a make_sharedA(); shared_ptrB b make_sharedB(); a-b_ptr b; // 循环引用开始 b-a_ptr a; // a和b的引用计数都是2析构时各减1变为1永远不会释放 return 0; // 无析构输出内存泄漏 }避免方法将循环引用中的一方或双方的shared_ptr替换为weak_ptr弱引用不增加引用计数修复后的代码可参考上一轮讲解智能指针时的weak_ptr示例。4. 容器存储裸指针未清理vector/list/map等容器存储裸指针时清空容器如clear()仅会删除指针本身容器内的元素但不会释放指针指向的堆内存。示例代码错误#include iostream #include vector using namespace std; int main() { vectorint* vec; // 向容器添加堆内存指针 vec.push_back(new int(1)); vec.push_back(new int(2)); vec.push_back(new int(3)); vec.clear(); // 仅清空容器3个int的堆内存未释放泄漏 return 0; }避免方法容器中存储智能指针如vectorunique_ptrint清空时自动释放内存若必须存裸指针清空容器前遍历 delete 每个元素。修复后的代码#include iostream #include vector #include memory using namespace std; int main() { vectorunique_ptrint vec; vec.push_back(make_uniqueint(1)); vec.push_back(make_uniqueint(2)); vec.push_back(make_uniqueint(3)); vec.clear(); // 自动释放所有堆内存无泄漏 return 0; }5. 动态数组释放错误delete vs delete []用new[]分配的数组若误用delete而非delete[]释放对于类对象数组仅调用第一个元素的析构函数其余元素的析构函数不执行导致内存泄漏对于内置类型数组int/char 等看似无泄漏但属于 “未定义行为”可能引发其他问题。示例代码错误#include iostream using namespace std; class MyClass { public: MyClass() { cout MyClass 构造 endl; } ~MyClass() { cout MyClass 析构 endl; } }; int main() { // 分配对象数组 MyClass* arr new MyClass[3]; // 输出3次构造 delete arr; // 错误仅调用第一个对象的析构后2个泄漏 // 正确写法delete[] arr; return 0; }避免方法严格遵循 “new配deletenew[]配delete[]” 的规则优先使用vector或unique_ptrT[]管理动态数组无需手动释放。6. 全局 / 静态指针的内存泄漏全局或静态指针指向堆内存时若程序结束前未释放虽然程序退出后操作系统会回收内存但长期运行的程序如服务器、后台服务会持续占用内存最终导致内存耗尽不符合 “资源用完即释放” 的编程规范。示例代码错误#include iostream using namespace std; // 全局指针 int* g_ptr new int(100); int main() { // 程序运行期间未释放g_ptr直到退出才被系统回收 cout *g_ptr endl; // 遗漏delete g_ptr; return 0; }避免方法用全局智能指针如static unique_ptrint g_ptr make_uniqueint(100)在程序退出前如 main 结束前显式释放全局 / 静态裸指针。7. 第三方库资源未释放使用第三方库的 API 分配资源如自定义句柄、内存、句柄时未调用库提供的 “释放函数”导致泄漏这类泄漏常被忽略。示例场景伪代码// 第三方库API示例 void* create_obj(); // 分配资源返回指针 void destroy_obj(void* p); // 释放资源 int main() { void* obj create_obj(); // 分配资源 // 业务逻辑... destroy_obj(obj); // 忘记调用资源泄漏 return 0; }避免方法封装成 RAII 类析构函数中调用释放函数记录所有 “分配 - 释放” API 对确保成对调用。