资讯动态

C++中智能指针unique_ptr的实现详解

发布时间:2026/10/20 20:46:26 来源:尧图企业网站定制
前言std::unique_ptr是 C11 引入的独占所有权智能指针。它表达一个非常明确的契约同一时刻只有一个unique_ptr拥有某个对象所有权只能通过移动转移不能复制。对象在持有者析构时被自动释放因此它既能替代裸new/delete又不会引入引用计数的开销。常见的误解是“unique_ptr内部一定只有一个指针所以它和裸指针完全一样”。在删除器为空类无状态的常见实现中空基类优化empty base optimization, EBO确实能让unique_ptr的大小等于一个指针但标准并不保证这一点带状态删除器或函数指针删除器时对象会更大。另一个误解是“只要用了unique_ptr就不会有悬垂”——get()返回的裸指针、release()交出的指针都不再受智能指针管理用错照样会泄漏或重复释放。本文先给出一个简化版UniquePtr的实现用它说明移动语义、删除器与reset/release的职责再对照标准库版本说明数组特化、自定义删除器、所有权转换等特性最后列出实际使用中的坑点。一、unique_ptr 的所有权契约一个unique_ptr在任意时刻要么持有指针要么为空。它不可拷贝如果允许拷贝两个对象都认为自己拥有同一块内存析构时就会重复释放。它可移动移动构造把指针从源对象“偷”走并把源置空所有权始终只有一个主人。围绕这个契约有几个必须记住的操作语义操作行为get()返回裸指针不转移所有权不要用它构造另一个智能指针release()放弃所有权并返回裸指针不释放对象调用方负责后续释放reset(p)先释放当前对象再接管preset()无参时只释放并置空swap(other)交换两个指针与删除器operator bool判断是否持有对象explicit只能用于条件上下文operator*/operator-访问对象空指针时解引用是未定义行为reset的顺序值得注意标准语义是先把新指针存入再对旧指针调用删除器。这样即使删除器抛异常虽然标准要求删除器不抛对象也已经处于可析构的状态。自己实现时不要先删旧再存新否则删除器访问到自身状态时容易出问题。二、简化版实现下面的实现覆盖构造、移动、析构、get、release、reset、swap与解引用。它不处理数组特化与类型转换目的是把核心机制讲清楚。// 适用于 C17#include utilitytemplate class Tstruct DefaultDelete {void operator()(T* p) const noexcept {delete p;}};template class T, class D DefaultDeleteTclass UniquePtr {public:using pointer T*;UniquePtr() noexcept default;explicit UniquePtr(pointer p) noexcept : ptr_(p) {}UniquePtr(pointer p, D d) noexcept : ptr_(p), deleter_(std::move(d)) {}UniquePtr(const UniquePtr) delete;UniquePtr operator(const UniquePtr) delete;UniquePtr(UniquePtr other) noexcept: ptr_(other.release()), deleter_(std::move(other.deleter_)) {}UniquePtr operator(UniquePtr other) noexcept {if (this ! other) {reset(other.release());deleter_ std::move(other.deleter_);}return *this;}~UniquePtr() {if (ptr_) {deleter_(ptr_);}}pointer get() const noexcept { return ptr_; }pointer release() noexcept {pointer p ptr_;ptr_ nullptr;return p;}void reset(pointer p nullptr) noexcept {pointer old ptr_;ptr_ p;if (old) {deleter_(old);}}void swap(UniquePtr other) noexcept {std::swap(ptr_, other.ptr_);std::swap(deleter_, other.deleter_);}explicit operator bool() const noexcept { return ptr_ ! nullptr; }T operator*() const { return *ptr_; }pointer operator-() const noexcept { return ptr_; }private:pointer ptr_ nullptr;D deleter_{};};int main() {UniquePtrint a(new int(42));UniquePtrint b std::move(a); // 移动构造a 变为空b.reset(new int(7)); // 释放旧的 42接管 7UniquePtrint c;c std::move(b); // 移动赋值int value *c;return value 7 ? 0 : 1;}按标准语义推演a构造后持有一个值为 42 的intb std::move(a)调用移动构造b接管指针a的ptr_被release()置空。b.reset(new int(7))先保存旧指针接管新指针再删除旧对象。c std::move(b)走移动赋值源对象b变为空。main的返回值只是示意用于说明最终*c应当是 7。这个版本有几点省略没有实现从UniquePtrU到UniquePtrT的转换没有为数组提供operator[]与delete[]删除器的赋值要求D可移动赋值。标准库版本对这些都有明确规定。三、标准库版本的关键特性std::unique_ptr定义在memory中签名是templateclass T, class Deleter std::default_deleteT class unique_ptr;。与简化版相比它多了以下能力。数组特化。std::unique_ptrT[]是偏特化删除器调用delete[]并提供operator[]。因此动态数组必须写成std::unique_ptrint[]而不是std::unique_ptrint后者析构时用delete释放new[]分配的内存是未定义行为。自定义删除器。删除器可以是函数对象、函数指针或 lambda只要能用D::operator()(T*)的形式调用。无状态删除器通常被空基类优化掉带状态删除器会增加对象大小。get_deleter()可以取回删除器的引用。所有权转换。当U*能隐式转换为T*、且删除器也能转换时std::unique_ptrU, E可以移动构造std::unique_ptrT, D。最常见的用法是把unique_ptrDerived转成unique_ptrBase。这里有一个硬性前提Base的析构函数必须是virtual否则通过基类指针删除派生对象是未定义行为。工厂函数。std::make_uniqueT(args...)与std::make_uniqueT[](n)从 C14 起可用。它把new与unique_ptr的构造放在同一个表达式里求值避免“先 new 成功、随后构造智能指针时抛异常”造成的泄漏同时省去显式写new。不可拷贝、可移动。unique_ptr可以放进std::vector因为容器扩容时用的是移动但按值传给函数必须std::move否则编译失败。完整示例自定义删除器与文件句柄下面的示例用std::unique_ptr管理std::FILE演示删除器如何把任意“获取—释放”资源纳入 RAII。// 适用于 C17#include cstddef#include cstdio#include memory#include string#include vectorstruct FileCloser {void operator()(std::FILE* f) const noexcept {if (f) {std::fclose(f);}}};using FilePtr std::unique_ptrstd::FILE, FileCloser;FilePtr open_file(const char* path) {return FilePtr(std::fopen(path, rb));}int main() {FilePtr file open_file(example.invalid);if (file) {char buffer[256];std::size_t n std::fread(buffer, 1, sizeof(buffer), file.get());(void)n;}auto number std::make_uniqueint(42);auto array std::make_uniqueint[](5);array[0] *number;std::vectorstd::unique_ptrint items;items.push_back(std::move(number));items.push_back(std::make_uniqueint(7));return 0;}按标准语义推演open_file返回一个持有FILE*的FilePtr若fopen失败则指针为空if (file)为假不执行读取离开作用域时FileCloser调用fclose。file.get()只借用裸指针不转移所有权。make_uniqueint[](5)创建 5 个值初始化的intoperator[]可用。items.push_back(std::move(number))把独占所有权移入容器之后number为空不能再解引用。这段代码不会真的去打开一个存在的文件路径使用了保留的example.invalid域名形式实际使用时替换为你的文件路径并检查fopen的返回值。四、与裸指针的边界unique_ptr解决的是“所有权与释放时机”不解决“对象是否还活着”。以下几种情况仍然会出问题。从get()得到的裸指针在unique_ptr被销毁、reset或移动之后就失效了继续使用是悬垂指针。release()交出的指针必须由调用方负责释放忘记释放就泄漏。把同一个裸指针交给两个unique_ptr会构造两个独立的控制逻辑这里是两个独立的删除器析构时重复释放。用unique_ptr管理并非new分配的内存例如malloc的结果、栈地址、new[]的结果配错删除器同样是未定义行为。还有一类问题是类型不完整。unique_ptr的析构函数需要完整类型才能调用删除器如果类的前向声明处就定义了持有unique_ptr的类的析构函数编译器无法生成删除代码。常见做法是把析构函数声明在头文件、定义在源文件pimpl 惯用法让完整类型在定义处可见。常见坑点❌std::unique_ptrT p(new T); std::unique_ptrT q(p.get());✅ 用std::move(p)转移所有权或用std::make_unique直接创建。❌void take(std::unique_ptrT p); take(p);✅take(std::move(p))若只是观察参数改为T*或const T。❌std::unique_ptrBase b(new Derived);而Base没有虚析构。✅ 给Base加virtual ~Base() default;否则删除是未定义行为。❌ 用std::unique_ptrint管理new int[10]。✅ 用std::unique_ptrint[]它会调用delete[]并提供operator[]。❌ 调用p.release()后不保存返回值以为对象被释放。✅release()只放弃所有权需要释放用reset()需要交出指针就接收返回值。❌p.reset(p.get());✅ 这是自赋值式的重置先存同址再删旧对象指针随即悬垂不要这样写。❌ 用std::unique_ptr管理malloc分配的内存。✅ 提供调用std::free的自定义删除器或改用new/make_unique。❌ 返回return std::move(local);以“帮助”移动。✅ 直接return local;局部对象返回时本身按移动处理显式std::move可能阻止返回值优化。总结主题关键结论所有权独占、不可拷贝、只能移动移动后源对象为空核心操作get借用、release放弃、reset释放并接管删除器默认delete数组特化用delete[]可自定义创建方式优先std::make_uniqueC14避免裸new多态基类析构必须virtual否则通过基类删除是 UB大小常见实现可做到一个指针但标准不保证unique_ptr的实现难点不在代码量而在所有权语义的严格性拷贝被删除、移动要置空、释放与放弃必须区分清楚。理解这三点之后再去看标准库版本提供的数组特化、删除器与转换构造就只是在同一套规则上增加细节。日常代码中优先使用make_unique把裸new限制在极少数需要自定义删除器的场合资源泄漏的空间会被压缩得很小。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑