shared_ptr 和 unique_ptr 深入探秘(CPP)

📅 2026/7/16 18:25:12
shared_ptr 和 unique_ptr 深入探秘(CPP)
shared_ptr 和 unique_ptr 深入探秘C 中shared_ptr和unique_ptr是 C11 之后被广泛使用的两个智能指针但是其实他们在使用上还是有一些“秘密”的我根据平时遇到的两个问题总结记录一些知识。为什么 unique_ptr 需要明确知道类型的析构函数这个问题是我写unique_ptr调试接口的时候才注意到的之前确实不知道。为什么会这样呢首先我们必须要知道unique_ptr到底封装了什么通常unique_ptr就是简单的对裸指针封装并且禁用拷贝和赋值templateclassT,classDeleterstd::default_deleteTclassunique_ptr;可以看到Deleter的类型是unique_ptr类型的一部分。在unique_ptr内部会保存类型为T*和Deleter的成员 分别表示保存的裸指针和删除器。假设内部是这么实现的 (一般会运用空基类优化把Deleter的空间优化掉libstdc里把他们放进了一个 tuple。这里是简化了)private:T*p;Deleter del;然后析构的时候就会这样~unique_ptr(){del(p);}当Deleter是默认的std::default_delete时del(p)就会delete pdelete会调用析构函数。而delete一个不完整类型的指针是 ub(undefined behavior)。在典型的实现中都会在delete前通过static_assert(sizeof(T) 0)做检查。sizeof对 incomplete type 求值会直接编译出错。incomplete type 是指当定义一个变量的时候不知道应该分配多少内存。C 声明和定义最大的区别就是是否发生内存分配当发生内存分配的时候必须知道要分配多少内存通常一个未定义的 struct未指定长度的数组类型都会引发 incomplete type 的问题。参考https://docs.microsoft.com/en-us/cpp/c-language/incomplete-types?viewmsvc-170structstudent;std::coutsizeof(student)std::endl;上述代码执行会报错prog.cc:17:18:error:invalid application ofsizeofto an incomplete typestudentstd::coutsizeof(student)std::endl;只声明了结构体student但是并没有定义所以是一个 incomplete type所以sizeof无法执行。回到unique_ptr现在我们知道unique_ptr的报错链路是unique_ptr-delete-sizoef也就是sizeof才是罪魁祸首。所以当Deleter非默认时就不一定需要知道类型的析构函数。比如下面这样// A is incomplete typeclassA;autoDel[](A*){};std::unique_ptrA,decltype(Del)ptr;因此可以对这个问题做定性并不是unique_ptr需要知道析构函数而是unique_ptr的默认删除器Deleter需要明确知道类型的析构函数。继续深挖一下这个问题会出现在shared_ptr吗答案是不会。这又引入了另一个问题shared_ptr 和 unique_ptr 的封装有什么不同下面这段代码将演示classTC;// 不提供析构函数voidfunc(){// unique_ptrTC up; // error: invalid application of sizeof to incomplete type detail4::TCshared_ptrTCtc;// shared_ptr可以正常运行}intmain(){func();}shared_ptr 的封装按理说shared_ptr.reset的时候需要deleteptr 就需要 ptr 的类型错了请指正而shared_ptr的 template type 可以是 incomplete type错误请指正。cppreference 是这么描述的std::shared_ptrmay be used with an incomplete typeT. However, the constructor from a raw pointer (template shared_ptr(Y*)) and the templatevoid reset(Y*) member function may only be called with a pointer to a complete type (note that std::unique_ptr may be constructed from a raw pointer to an incomplete type).reset的时候需要类型完整。默认构造的时候允许是不完整类型。为什么会这样呢shared_ptr怎么处理Deleter呢(还记得吧 Deleter 就是智能指针析构时候的删除操作)在常见编译器的实现里shared_ptr把Deleter包括默认情况下的 operator delete放进一个叫做control block的结构里相当于做了一次 type erasure把Deleter的类型从shared_ptr类型本身里面擦下去。Deleter的类型在 control block 的具体类型上shared_ptr本身只持有一个control block基类的指针通过虚函数来调用Deleter。而因为shared_ptr构造的时候要求必须是 complete typecontrol block已经知道怎么析构了shared_ptr析构的时候就调用个虚函数具体事情它不管的。这下我们明白了unique_ptr的封装太简单了没有 control blockDeleter包括默认的std::default_delete直接做在unique_ptr一起了这就导致unique_ptr的析构函数需要亲手析构被管理的类型因此析构函数必须看到 complete type。然而反过来因为构建的时候只需要保存下指针所以unique_ptr构造的时候不需要看到 complete type。这俩正好是反的。C 标准并没有规定这些实现细节但是规定函数签名和特性的时候是考虑着比较合理的实现方式来写标准的到最后标准落下来之后也差不多只能这么实现了。总结unique_ptr只保存了类型指针 ptr 和这个指针的析构方法调用 delete ptr就需要ptr的完整类型为了防止这个问题出现直接通过 assert sizeof 排除掉了这种风险。unique_ptr相当于在编译时绑定了删除器。shared_ptr保存的是一个控制块的指针。控制块包含的就是一个引用计数和一个原来对象的裸指针。控制块中初始化的指针是nullptr在运行时为其赋值也可以通过reset修改。类似于虚函数shared_ptr相当于在运行时绑定了删除器。虽然只是一个小小的知识点但是也帮助我深入理解了shared_ptr和unique_ptr在设计上的区别对于不同使用场景下选择不同智能指针的体会也更加深刻。