析构函数非虚时shared_ptr与unique_ptr行为差异原因探究
已知多态类未将析构函数设为虚函数会导致未定义行为,正确做法是将其设为虚函数。但在RHEL 8的gcc/g++环境下,使用shared_ptr销毁对象时能正常调用派生类析构函数,而unique_ptr却不行?
代码示例:
class Base { public: ~Base() { std::cout << "Base dtor" << std::endl; } }; class Derrived : public Base { public: ~Derrived() { std::cout << "Derrived dtor" << std::endl; } }; int main() { // Derrived dtor NOT called! (I understand why!) { std::unique_ptr<Base> b = std::make_unique<Derrived>(); } // Derrived and Base dtor called! WHY??? { std::shared_ptr<Base> b = std::make_shared<Derrived>(); } }
工作中遇到shared_ptr隐藏内存泄漏的情况,换成unique_ptr后问题显现,修复虽简单但易遗漏。好奇两者行为差异的原因,shared_ptr是否会追踪对象的动态类型?曾将shared_ptr改为unique_ptr,预期行为一致但结果不同。
核心差异:删除器的绑定时机与存储方式
- unique_ptr:编译期绑定删除器
默认的std::unique_ptr<T>使用std::default_delete<T>作为删除器,这个删除器的类型是编译阶段确定的。当声明unique_ptr<Base>时,删除器就固定为default_delete<Base>,它只会调用Base的析构函数——不管实际指向的是派生类对象,编译时已经绑定了删除逻辑,运行时不会动态调整。 - shared_ptr:运行时存储删除器
shared_ptr在构造(比如通过make_shared<Derrived>)时,会捕获对象的动态类型,生成适配Derrived的删除器,并将其存储在内部的控制块中。当引用计数归零时,会调用这个运行时绑定的删除器,从而正确触发Derrived的析构,再自动调用基类析构。
为什么shared_ptr能“追踪”动态类型?
make_shared创建对象时明确知道实际构造的是Derrived类型,因此会生成对应类型的删除逻辑。当shared_ptr<Base>接收该对象时,控制块里已经保存了正确的删除器,和shared_ptr的模板参数Base无关——这是一种基于控制块的运行时多态实现,绕过了基类非虚析构的限制。
关于内存泄漏的隐藏
shared_ptr这种“侥幸正确”的行为属于未定义行为的特例生效,C++标准并不保证这种行为在所有编译器或环境下都成立。换成unique_ptr后,编译期绑定的删除器直接暴露了非虚析构的问题,反而能帮你及时发现代码隐患。
内容的提问来源于stack exchange,提问作者josh chatham
相关产品推荐
相关产品推荐

