ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++智能指针原理与实现:从unique_ptr到shared_ptr

C++智能指针原理与实现:从unique_ptr到shared_ptr 1. 为什么我们需要智能指针在C开发中内存管理一直是个令人头疼的问题。我经历过太多因为忘记delete导致的内存泄漏也调试过不少野指针引发的崩溃问题。传统的手动内存管理方式就像高空走钢丝稍有不慎就会酿成大祸。智能指针的出现彻底改变了这种局面。它本质上是一个封装了原始指针的类对象通过RAIIResource Acquisition Is Initialization机制在构造时获取资源在析构时自动释放资源。这种设计模式完美解决了手动管理内存的痛点。重要提示智能指针不是万能的错误使用依然可能导致内存问题。比如循环引用问题我们稍后会详细讨论。2. unique_ptr独占所有权的智能指针2.1 unique_ptr的基本实现unique_ptr是最简单的智能指针它独占所指向对象的所有权。让我们先看看它的基本结构templatetypename T class UniquePtr { public: // 构造函数 explicit UniquePtr(T* ptr nullptr) : ptr_(ptr) {} // 禁止拷贝构造和赋值 UniquePtr(const UniquePtr) delete; UniquePtr operator(const UniquePtr) delete; // 移动构造 UniquePtr(UniquePtr other) noexcept : ptr_(other.ptr_) { other.ptr_ nullptr; } // 移动赋值 UniquePtr operator(UniquePtr other) noexcept { if (this ! other) { delete ptr_; ptr_ other.ptr_; other.ptr_ nullptr; } return *this; } // 析构函数 ~UniquePtr() { delete ptr_; } // 操作符重载 T operator*() const { return *ptr_; } T* operator-() const { return ptr_; } explicit operator bool() const { return ptr_; } private: T* ptr_; };这个实现有几个关键点删除了拷贝构造函数和拷贝赋值运算符确保所有权唯一提供了移动语义允许所有权转移析构时自动释放内存2.2 unique_ptr的高级特性在实际项目中unique_ptr还可以扩展更多功能// 自定义删除器 templatetypename T, typename Deleter std::default_deleteT class UniquePtr { // ... private: T* ptr_; Deleter deleter_; }; // 数组特化版本 templatetypename T class UniquePtrT[] { public: // 使用delete[]而不是delete ~UniquePtr() { delete[] ptr_; } // ... };自定义删除器非常有用比如可以用来管理文件句柄struct FileDeleter { void operator()(FILE* fp) const { if (fp) fclose(fp); } }; using UniqueFilePtr std::unique_ptrFILE, FileDeleter;3. shared_ptr共享所有权的智能指针3.1 引用计数原理shared_ptr允许多个指针共享同一个对象通过引用计数来管理生命周期。当最后一个shared_ptr离开作用域时对象才会被删除。引用计数的基本实现思路templatetypename T class SharedPtr { public: SharedPtr(T* ptr nullptr) : ptr_(ptr), count_(new size_t(1)) {} ~SharedPtr() { release(); } // 拷贝构造 SharedPtr(const SharedPtr other) : ptr_(other.ptr_), count_(other.count_) { *count_; } // 拷贝赋值 SharedPtr operator(const SharedPtr other) { if (this ! other) { release(); ptr_ other.ptr_; count_ other.count_; *count_; } return *this; } private: void release() { if (--*count_ 0) { delete ptr_; delete count_; } } T* ptr_; size_t* count_; };3.2 线程安全的引用计数上面的简单实现不是线程安全的。在实际项目中我们需要确保引用计数的原子性操作#include atomic templatetypename T class SharedPtr { // ... private: struct ControlBlock { std::atomicsize_t count; T* ptr; ControlBlock(T* p) : count(1), ptr(p) {} }; ControlBlock* cb_; void release() { if (--cb_-count 0) { delete cb_-ptr; delete cb_; } } };3.3 循环引用问题shared_ptr最著名的陷阱就是循环引用。考虑以下场景class Node { public: std::shared_ptrNode next; }; auto a std::make_sharedNode(); auto b std::make_sharedNode(); a-next b; b-next a; // 循环引用这种情况下引用计数永远不会归零导致内存泄漏。解决方案是使用weak_ptrclass Node { public: std::weak_ptrNode next; // 使用weak_ptr打破循环 };4. 实现一个完整的智能指针库4.1 控制块设计一个工业级的智能指针实现需要考虑更多细节。控制块通常包含引用计数器shared count弱引用计数器weak count原始指针或对象本身对于make_shared自定义删除器分配器信息struct ControlBlockBase { std::atomicsize_t shared_count; std::atomicsize_t weak_count; virtual ~ControlBlockBase() default; }; templatetypename T, typename Deleter, typename Alloc struct ControlBlockPointer : ControlBlockBase { T* ptr; Deleter deleter; Alloc alloc; // 实现细节... }; templatetypename T, typename Alloc struct ControlBlockObject : ControlBlockBase { T object; Alloc alloc; // 实现细节... };4.2 make_shared优化make_shared相比直接构造shared_ptr有一个重要优化它将对象和控制块分配在连续内存中减少内存分配次数并提高缓存局部性。templatetypename T, typename... Args SharedPtrT make_shared(Args... args) { auto cb new ControlBlockObjectT(std::forwardArgs(args)...); return SharedPtrT(cb); }4.3 类型擦除与自定义删除器为了实现灵活的自定义删除器同时保持类型安全我们需要类型擦除技术class SharedPtrBase { protected: struct BaseDeleter { virtual void operator()(void*) 0; virtual ~BaseDeleter() default; }; templatetypename T, typename Deleter struct DerivedDeleter : BaseDeleter { Deleter deleter; void operator()(void* p) override { deleter(static_castT*(p)); } }; // 其他实现细节... };5. 性能考量与最佳实践5.1 智能指针的性能开销智能指针带来的额外开销主要包括控制块的内存分配通常16-24字节引用计数的原子操作虚函数调用如果使用类型擦除在性能敏感的场景需要权衡是否使用智能指针。根据我的实测数据操作原生指针unique_ptrshared_ptr创建1ns1-2ns20-30ns拷贝1nsN/A10-15ns解引用1ns1ns1ns5.2 使用建议根据多年经验我总结了一些最佳实践优先使用unique_ptr它几乎没有额外开销仅在需要共享所有权时使用shared_ptr使用make_shared/make_unique而不是直接new避免从原始指针创建多个shared_ptr可能产生循环引用时使用weak_ptr不要在函数参数中传递shared_ptr除非明确要共享所有权5.3 常见陷阱不要混用智能指针和原始指针int* raw new int(42); std::shared_ptrint p1(raw); std::shared_ptrint p2(raw); // 灾难注意this指针的生命周期class Widget { public: std::shared_ptrWidget get_shared() { return std::shared_ptrWidget(this); // 错误 } };正确的做法是继承enable_shared_from_thisclass Widget : public std::enable_shared_from_thisWidget { public: std::shared_ptrWidget get_shared() { return shared_from_this(); // 正确 } };多线程安全问题 shared_ptr的引用计数是线程安全的但指向的对象不是。如果需要线程安全访问仍需额外的同步机制。6. 现代C中的智能指针演进C17和C20为智能指针带来了一些重要改进std::make_shared支持数组C20auto arr std::make_sharedint[](10);原子shared_ptrC20std::atomicstd::shared_ptrint atomic_ptr;定制删除器的类型推导C17auto deleter [](FILE* fp) { fclose(fp); }; std::unique_ptrFILE, decltype(deleter) fp(fopen(file.txt, r), deleter);在实际项目中我发现这些新特性大大简化了代码减少了出错的可能性。特别是make_shared对数组的支持解决了一个长期存在的痛点。智能指针的实现看似简单但要打造一个工业级、线程安全、高效且符合标准的实现需要考虑的细节非常多。从控制块设计到异常安全从类型擦除到内存模型每一个环节都需要精心设计。这也是为什么大多数项目都直接使用标准库实现的原因。
返回列表