C++ 中 unique ptr 详解:从原理到实践 C 中 unique_ptr 详解从原理到实践一、引言独占所有权的智能指针std::unique_ptr是 C11 引入的智能指针它实现了独占所有权(Unique Ownership)语义——同一时间只有一个unique_ptr可以拥有并管理一个堆上分配的对象。当unique_ptr被销毁离开作用域、被重置、被移动走它所管理的对象会被自动delete。与裸指针相比unique_ptr提供了自动释放RAII 保证资源不泄漏所有权明确独占语义让代码意图清晰零开销大小与裸指针相同性能与裸指针相当不可拷贝防止意外的共享所有权二、核心概念速览| 维度 | 说明 || --- | --- || 所有权模型 | 独占同一时间只有一个 unique_ptr 拥有对象 || 拷贝语义 | 禁止拷贝 delete || 移动语义 | 支持移动转移所有权 || 默认删除器 | std::default_deleteT调用 delete || 自定义删除器 | 支持用于 fclose、fclose、自定义析构等 || 数组支持 | std::unique_ptrT[]C14 起有 make_uniqueT[] || 内存开销 | 零与裸指针大小相同无引用计数 || 性能开销 | 零解引用与裸指针相同构造/析构通常可内联 |三、unique_ptr 的基本原理3.1 内部结构简化实现cpp复制下载templatetypename T, typename Deleter std::default_deleteT class unique_ptr { private: T* ptr_; // 裸指针存储管理的对象地址 Deleter deleter_; // 删除器通常为空基类不占空间 public: // 构造函数接管裸指针的所有权 explicit unique_ptr(T* p nullptr) noexcept : ptr_(p) { } // 析构函数释放管理的对象 ~unique_ptr() { if (ptr_) { deleter_(ptr_); // 调用删除器默认是 delete } } // 禁止拷贝 unique_ptr(const unique_ptr) delete; unique_ptr operator(const unique_ptr) delete; // 移动构造函数转移所有权 unique_ptr(unique_ptr other) noexcept : ptr_(other.ptr_) { other.ptr_ nullptr; // 源指针置空保证独占 } // 移动赋值运算符 unique_ptr operator(unique_ptr other) noexcept { if (this ! other) { reset(other.release()); // 释放当前对象接管新对象 } return *this; } // 核心操作 T* get() const noexcept { return ptr_; } T* operator-() const noexcept { return ptr_; } T operator*() const noexcept { return *ptr_; } // 释放所有权不删除对象 T* release() noexcept { T* old ptr_; ptr_ nullptr; return old; } // 替换管理的对象删除旧对象 void reset(T* p nullptr) noexcept { T* old ptr_; ptr_ p; if (old) { deleter_(old); } } explicit operator bool() const noexcept { return ptr_ ! nullptr; } };3.2 所有权转移模型图表代码下载全屏渲染失败四、unique_ptr 的核心操作4.1 创建和初始化cpp复制下载#include memory #include iostream class Resource { public: Resource() { std::cout Resource acquired std::endl; } ~Resource() { std::cout Resource released std::endl; } void use() { std::cout Using resource std::endl; } }; int main() { // 方式一从裸指针构造不推荐 std::unique_ptrResource p1(new Resource()); // 方式二使用 make_unique推荐C14 auto p2 std::make_uniqueResource(); // 方式三C11 等价写法 std::unique_ptrResource p3(new Resource()); // 方式四从 nullptr 构造 std::unique_ptrResource p4; // 空指针 std::unique_ptrResource p5 nullptr; // 空指针 }4.2 所有权转移cpp复制下载void transferOwnership() { auto p1 std::make_uniqueint(42); // 移动构造转移所有权 std::unique_ptrint p2 std::move(p1); // p1 现在为空nullptr // p2 拥有 int(42) std::cout p1 is (p1 ? not null : null) std::endl; // null std::cout p2 is (p2 ? not null : null) std::endl; // not null // 移动赋值 std::unique_ptrint p3; p3 std::move(p2); // 所有权从 p2 转移到 p3 // 函数返回自动移动 auto p4 createResource(); // 从函数接收 unique_ptr } std::unique_ptrint createResource() { auto p std::make_uniqueint(100); return p; // 自动移动RVO 或移动语义 }4.3 释放和重置cpp复制下载void releaseAndReset() { auto p std::make_uniqueint(42); // release()放弃所有权但不删除对象 int* raw p.release(); // p 变为 nullptrraw 指向 int(42) // 现在需要手动管理 raw delete raw; // 必须手动释放 // reset()替换管理的对象删除旧对象 auto p2 std::make_uniqueint(100); p2.reset(new int(200)); // 删除 int(100)开始管理 int(200) p2.reset(); // 删除 int(200)p2 变为 nullptr // 检查是否为空 if (p2) { std::cout p2 is not null std::endl; } else { std::cout p2 is null std::endl; } }五、自定义删除器5.1 用于 C 资源管理cpp复制下载#include memory #include cstdio // C 文件资源的 RAII 包装 struct FileDeleter { void operator()(FILE* fp) const { if (fp) { std::cout Closing file std::endl; fclose(fp); } } }; using FilePtr std::unique_ptrFILE, FileDeleter; FilePtr openFile(const char* path) { FILE* fp fopen(path, r); if (!fp) { throw std::runtime_error(Cannot open file); } return FilePtr(fp); } int main() { auto file openFile(data.txt); // 使用 file.get() 获取裸指针 char buffer[256]; fgets(buffer, sizeof(buffer), file.get()); // file 析构时自动调用 fclose }5.2 使用 Lambda 作为删除器cpp复制下载#include memory #include cstdlib #include iostream // malloc/free 的 RAII 包装 templatetypename T auto makeUniqueMalloc(size_t count 1) { T* raw static_castT*(malloc(sizeof(T) * count)); if (!raw) throw std::bad_alloc(); return std::unique_ptrT, decltype(free)(raw, free); // 或者使用 Lambda // return std::unique_ptrT, void(*)(void*)(raw, [](void* p) { free(p); }); } int main() { auto buffer makeUniqueMallocint(1000); buffer.get()[0] 42; // buffer 析构时自动调用 free }5.3 删除器的类型对 unique_ptr 大小的影响cpp复制下载#include memory #include iostream void rawDeleter(int* p) { delete p; } int main() { // 默认删除器大小等于裸指针 auto p1 std::make_uniqueint(42); std::cout Default deleter: sizeof(p1) std::endl; // 8 (64位) // 无捕获 Lambda大小等于裸指针空基类优化 auto lambdaDeleter [](int* p) { delete p; }; std::unique_ptrint, decltype(lambdaDeleter) p2(new int(42), lambdaDeleter); std::cout Lambda deleter: sizeof(p2) std::endl; // 8 // 函数指针删除器大小 指针 函数指针 std::unique_ptrint, void(*)(int*) p3(new int(42), rawDeleter); std::cout Func ptr deleter: sizeof(p3) std::endl; // 16 // 有捕获 Lambda大小 指针 Lambda 捕获 int cleanupCount 0; auto capturingLambda [cleanupCount](int* p) { cleanupCount; delete p; }; std::unique_ptrint, decltype(capturingLambda) p4(new int(42), capturingLambda); std::cout Capturing lambda: sizeof(p4) std::endl; // 16 }六、unique_ptr 与数组cpp复制下载#include memory #include iostream int main() { // 管理动态数组C14 起有 make_uniqueT[] auto arr std::make_uniqueint[](10); for (int i 0; i 10; i) { arr[i] i * 2; // 可以使用 operator[] } for (int i 0; i 10; i) { std::cout arr[i] ; // 0 2 4 6 8 10 12 14 16 18 } std::cout std::endl; // arr 析构时调用 delete[] // 注意unique_ptrT[] 没有 operator* 和 operator- // 它只有 operator[] }七、unique_ptr 的常见使用模式7.1 工厂函数cpp复制下载#include memory #include string class Product { public: virtual ~Product() default; virtual void use() 0; }; class ConcreteProductA : public Product { public: void use() override { std::cout Using A std::endl; } }; class ConcreteProductB : public Product { public: void use() override { std::cout Using B std::endl; } }; // 工厂函数返回 unique_ptr 明确转移所有权 std::unique_ptrProduct createProduct(const std::string type) { if (type A) { return std::make_uniqueConcreteProductA(); } else if (type B) { return std::make_uniqueConcreteProductB(); } return nullptr; } int main() { auto product createProduct(A); product-use(); // product 自动销毁 }7.2 PIMPL 惯用法cpp复制下载// widget.h #include memory class Widget { public: Widget(); ~Widget(); // 必须在 .cpp 中定义析构时需要完整类型 Widget(Widget) noexcept; // 移动语义 Widget operator(Widget) noexcept; void doSomething(); private: class Impl; // 前置声明 std::unique_ptrImpl pImpl_; }; // widget.cpp #include widget.h #include iostream class Widget::Impl { public: void doSomething() { std::cout Implementation std::endl; } }; Widget::Widget() : pImpl_(std::make_uniqueImpl()) { } Widget::~Widget() default; // 必须在 Impl 完整定义后 Widget::Widget(Widget) noexcept default; Widget Widget::operator(Widget) noexcept default; void Widget::doSomething() { pImpl_-doSomething(); }7.3 容器中的 unique_ptrcpp复制下载#include vector #include memory class Task { public: virtual ~Task() default; virtual void execute() 0; }; class PrintTask : public Task { std::string msg_; public: PrintTask(std::string msg) : msg_(std::move(msg)) { } void execute() override { std::cout msg_ std::endl; } }; int main() { std::vectorstd::unique_ptrTask tasks; // 向容器中添加多态对象 tasks.push_back(std::make_uniquePrintTask(Task 1)); tasks.push_back(std::make_uniquePrintTask(Task 2)); tasks.emplace_back(std::make_uniquePrintTask(Task 3)); // 遍历执行 for (auto task : tasks) { task-execute(); } // 容器析构时所有 unique_ptr 自动释放各自的对象 }八、unique_ptr 与 shared_ptr 的选择cpp复制下载#include memory // 使用 unique_ptr 的场景 void uniquePtrScenarios() { // 1. 工厂函数返回所有权转移 auto obj createProduct(A); // 2. PIMPL 惯用法独占实现 Widget w; // 3. 容器中存储多态对象独占所有权 std::vectorstd::unique_ptrTask tasks; // 4. 作为类成员独占子对象 class Engine { }; class Car { std::unique_ptrEngine engine_; // Car 独占 Engine }; } // 使用 shared_ptr 的场景 void sharedPtrScenarios() { // 1. 多个所有者共享同一个对象 auto sp1 std::make_sharedint(42); auto sp2 sp1; // 共享所有权 // 2. 观察者模式配合 weak_ptr std::weak_ptrint weak sp1; }九、性能与内存开销总结| 特性 | unique_ptr | 裸指针 | shared_ptr || --- | --- | --- | --- || 大小默认删除器 | 8 字节 | 8 字节 | 16 字节 || 引用计数 | 无 | 无 | 有原子操作 || 解引用开销 | 零 | 零 | 零 || 拷贝开销 | 禁止 | 浅拷贝 | 原子递增引用计数 || 移动开销 | 极低指针赋值 | 极低 | 低指针赋值 || 析构开销 | delete | 无手动 | 原子递减 可能 delete |十、总结std::unique_ptr是现代 C 中管理独占所有权的首选工具其核心原理可以归纳为独占所有权模型同一时间只有一个unique_ptr拥有对象。通过禁止拷贝、支持移动来保证这一语义。移动后源指针变为nullptr目标指针获得所有权。零开销抽象默认情况下无状态删除器unique_ptr的大小与裸指针完全相同8 字节64 位系统。解引用操作与裸指针相同无额外开销。析构时的delete调用通常可被内联。RAII 自动化管理unique_ptr在析构时自动删除管理的对象。即使函数抛出异常也能保证资源被正确释放。这是异常安全的基础保障。自定义删除器可以指定删除器来处理特殊资源FILE*、malloc分配的内存、自定义资源。删除器类型会影响unique_ptr的大小无状态删除器零开销有状态删除器会增加大小。现代 C 使用建议用std::make_uniqueT()创建C14避免直接使用new工厂函数返回unique_ptr明确转移所有权用unique_ptr实现 PIMPL 惯用法容器中存储多态对象用unique_ptrBase默认选择unique_ptr只有确实需要共享所有权时才用shared_ptrunique_ptr是 C 类型系统和 RAII 哲学的完美体现——通过类型系统在编译期保证资源的正确管理无需运行时开销。它是现代 C 中管理动态资源的基础工具。