C++ 智能指针完全指南(一):unique_ptr 深度详解

发布时间:2026/6/9 23:31:36
C++ 智能指针完全指南(一):unique_ptr 深度详解 引言C 程序员最头疼的问题是什么不是语法、不是模板、不是多线程——而是内存管理。void func() { int* p new int(42); // ... 复杂逻辑 ... if (某个条件) return; // 忘记 delete p → 内存泄漏 // ... 更多逻辑 ... delete p; // 如果中间抛异常永远执行不到这里 }手动new/delete有三大痛点忘记 delete→ 内存泄漏提前 return 或抛异常→ delete 被跳过多次 delete→ 程序崩溃智能指针就是来彻底解决这些问题的。从 C11 开始标准库提供了三种智能指针unique_ptr、shared_ptr、weak_ptr。它们本质上是用 RAII 包装原始指针让指针在离开作用域时自动释放。本文作为智能指针系列第一篇聚焦于独占所有权的unique_ptr。第一部分为什么需要 unique_ptr一、原始指针的困境#include iostream using namespace std; class Resource { public: Resource() { cout 资源获取 endl; } ~Resource() { cout 资源释放 endl; } void work() { cout 工作中... endl; } }; void badExample() { Resource* p new Resource(); // 场景1提前 return忘记 delete if (rand() % 2 0) { return; // 内存泄漏p 没有被释放 } // 场景2抛异常delete 被跳过 p-work(); throw runtime_error(出错了); // 异常抛出 → delete 不执行 delete p; // 只有正常情况才能执行到这里 }三个场景只有一种情况能正确释放内存。这就是为什么大型 C 项目普遍禁止裸new/delete。二、unique_ptr 的解决方案#include memory void goodExample() { unique_ptrResource p make_uniqueResource(); // 场景1提前 return → p 的析构函数自动调用 → 资源释放 ✅ if (rand() % 2 0) return; // 场景2抛异常 → p 的析构函数仍然会被调用 → 资源释放 ✅ p-work(); throw runtime_error(出错了); // 不需要 delete离开作用域自动释放 ✅ }第二部分unique_ptr 的核心特性一、独占所有权unique_ptr独占它指向的对象不能被拷贝只能被移动。unique_ptrint p1 make_uniqueint(42); unique_ptrint p2 p1; // ❌ 编译错误不能拷贝 unique_ptrint p3 std::move(p1); // ✅ 移动后 p1 变成 nullptr二、三种创建方式#include memory using namespace std; int main() { // 方式1make_uniqueC14最推荐 auto p1 make_uniqueint(42); // 方式2用 newC11 兼容 unique_ptrint p2(new int(42)); // 方式3从已有 unique_ptr 移动 unique_ptrint p3 std::move(p1); // p1 变成 nullptr return 0; // p2 和 p3 自动释放p1 已经为空 }优先用make_unique原因代码更简洁不需要写两次类型名异常安全C14 起可用三、访问被管理的对象unique_ptrstring p make_uniquestring(hello); // 像指针一样使用 cout *p endl; // hello解引用 cout p-size() endl; // 5访问成员 cout p.get() endl; // 获取原始指针谨慎使用操作含义*p解引用获取对象的引用p-访问对象成员p.get()获取原始指针不转移所有权p nullptr判断是否为空if(p)判断是否为空四、释放与重置unique_ptrint p make_uniqueint(42); p.reset(); // 释放资源p 变成 nullptr p.reset(new int(100)); // 释放旧资源接管新资源 int* raw p.release(); // 放弃所有权返回原始指针 // p 变成 nullptrraw 需要手动 delete delete raw;操作含义p.reset()释放资源p 为空p.reset(newPtr)释放旧资源接管新资源p.release()放弃所有权返回裸指针需要手动 delete第三部分unique_ptr 的常用场景一、作为函数返回值最安全unique_ptrResource createResource() { return make_uniqueResource(); // 返回临时对象 → 自动移动 } int main() { auto p createResource(); // 接收移动 p-work(); }二、放入容器vectorunique_ptrResource resources; resources.push_back(make_uniqueResource()); // 移动进去 resources.emplace_back(make_uniqueResource()); // 遍历 for (const auto r : resources) { r-work(); } // 容器销毁时所有 unique_ptr 自动释放 → 不用手动删三、工厂模式class Product { public: virtual void use() 0; virtual ~Product() default; }; class ProductA : public Product { public: void use() override { cout 使用产品A endl; } }; class ProductB : public Product { public: void use() override { cout 使用产品B endl; } }; // 工厂函数返回 unique_ptr基类 unique_ptrProduct createProduct(char type) { if (type A) return make_uniqueProductA(); if (type B) return make_uniqueProductB(); return nullptr; } int main() { auto p createProduct(A); p-use(); // 使用产品A // 多态析构需要基类有虚析构函数 }第四部分自定义删除器一、什么时候需要自定义删除器默认的删除器是delete但有些资源不是用new分配的// 文件句柄 FILE* f fopen(test.txt, r); // 用完后需要 fclose(f)不是 delete f // Socket int sockfd socket(...); // 用完后需要 close(sockfd)不是 delete sockfd // 自定义内存池 void* mem my_pool_alloc(1024); // 用完后需要 my_pool_free(mem)不是 delete mem二、自定义删除器示例#include cstdio // 文件句柄管理 auto fileDeleter [](FILE* f) { if (f) { cout 关闭文件 endl; fclose(f); } }; int main() { FILE* f fopen(test.txt, w); unique_ptrFILE, decltype(fileDeleter) filePtr(f, fileDeleter); fprintf(filePtr.get(), hello\n); // 离开作用域 → 自动调用 fclose }// Socket 管理 auto socketDeleter [](int* fd) { if (fd *fd ! -1) { cout 关闭 socket endl; close(*fd); } delete fd; }; int main() { int* sockfd new int(socket(AF_INET, SOCK_STREAM, 0)); unique_ptrint, decltype(socketDeleter) sockPtr(sockfd, socketDeleter); // 离开作用域 → 自动关闭 socket }三、unique_ptr 管理数组// 管理动态数组C14 起 auto arr make_uniqueint[](10); // 10 个 int arr[0] 1; arr[1] 2; cout arr[3] endl; // 离开作用域 → 自动调用 delete[] // 注意shared_ptr 也支持数组C17 起第五部分unique_ptr vs 原始指针对比项原始指针unique_ptr内存释放手动 delete自动所有权不明确独占明确拷贝浅拷贝可能出问题禁止拷贝异常安全❌ 可能泄漏✅ 保证释放性能开销无零开销和原始指针一样大小8 字节8 字节无自定义删除器时unique_ptr 是零开销抽象在编译优化后unique_ptr的大小和性能与原始指针完全一样。第六部分常见错误// ❌ 错误1试图拷贝 unique_ptr unique_ptrint p1 make_uniqueint(42); unique_ptrint p2 p1; // 编译错误 // ❌ 错误2把同一个裸指针给多个 unique_ptr int* raw new int(42); unique_ptrint p3(raw); unique_ptrint p4(raw); // p3 和 p4 都会尝试 delete raw → 双重释放 // ❌ 错误3忘记 release 后要手动 delete unique_ptrint p5 make_uniqueint(42); int* raw2 p5.release(); // raw2 需要手动 delete否则泄漏 delete raw2; // ✅ 正确用 get 只是临时查看不转移所有权 unique_ptrint p6 make_uniqueint(42); int* raw3 p6.get(); // 只是查看所有权还是 p6 的 func(raw3); // 可以传给只读函数总结一、核心要点特性说明所有权独占不可拷贝只可移动创建make_uniqueT(args)释放自动离开作用域、reset()、release()访问*p、p-、p.get()自定义删除器非 new 分配的资源FILE*、socket 等性能零开销和原始指针相同头文件memory二、一句话记忆unique_ptr独占对象所有权禁止拷贝只允许移动。用make_unique创建离开作用域自动释放。需要管理非 new 资源时用自定义删除器。零开销和原始指针一样快。