为什么std::shared_ptr控制块需要持有正确类型的托管对象指针
核心原因非常直接:shared_ptr实例自身存储的指针,和控制块里存储的指针,类型、指向的地址、用途完全不相同,根本不是“重复存储”。
两个指针的分工差异
- shared_ptr类内部存储的
T*:是对外提供给用户使用的指针,类型和当前shared_ptr的模板参数T完全一致,会随着shared_ptr的类型转换(比如基类指针转换、多继承下的类型偏移)改变地址值,用于支撑get()、operator->、operator*等用户接口,保证用户拿到的指针可以直接访问对应类型的成员。 - 控制块中存储的
U*:是不对外暴露的内部指针,类型固定为最初构造shared_ptr时托管的真实对象类型U,地址永远是当初分配托管对象时返回的原始首地址,仅用于引用计数归0时执行删除操作(不管是默认delete还是自定义删除器),保证删除操作的地址是合法的。
具体场景示例
我们可以用多继承场景直观看到两个指针的地址差:
struct A { int a; virtual ~A() = default; }; struct B { int b; virtual ~B() = default; }; // 多继承下C的内存布局依次为:A子对象 -> B子对象 -> C自身成员 struct C : A, B {}; int main() { std::shared_ptr<C> pc = std::make_shared<C>(); // 转换为指向B子对象的shared_ptr std::shared_ptr<B> pb = pc; // 打印两个指针的地址值 // 输出示例:pc.get() = 0x55f8b7d2eeb0 printf("pc.get() = %p\n", pc.get()); // 输出示例:pb.get() = 0x55f8b7d2eeb8 // pb里存的B*地址,比原始C对象首地址偏移了8字节(刚好是A子对象的大小) printf("pb.get() = %p\n", pb.get()); }
如果控制块里没有存储原始的C*指针,而是依赖每个shared_ptr实例自身存储的指针做删除操作,那么当pb的引用计数归0时,就会拿着偏移后的B*地址去delete,这个地址不是当初new C返回的原始首地址,会直接触发未定义行为,轻则内存泄漏,重则程序崩溃。
原作者注释提到的“派生类存在多个基类T子对象”的场景,本质也是同一个逻辑:
struct T {}; struct U1 : T {}; struct U2 : T {}; // Derived内存里同时存在两个独立的T子对象,分别来自U1和U2的继承 struct Derived : U1, U2 {}; int main() { std::shared_ptr<Derived> d = std::make_shared<Derived>(); // 分别转换为指向两个不同T子对象的shared_ptr std::shared_ptr<T> t1 = std::static_pointer_cast<U1>(d); std::shared_ptr<T> t2 = std::static_pointer_cast<U2>(d); // t1.get()和t2.get()指向两个完全不同的T子对象,地址值不一样 // 如果没有控制块里存的原始Derived*,无论用哪个T*做删除操作都是错误的 }
除了多继承场景,这个设计还能覆盖更多边界情况:
- 当shared_ptr被转换为
shared_ptr<void>时,内部存储的是void*类型指针,根本无法直接用于delete(delete void*属于标准未定义行为),必须依赖控制块中存储的原始类型指针完成删除。 - 当使用自定义删除器管理非new分配的资源(比如文件句柄、套接字)时,控制块中存储的原始指针可以保证传入删除器的参数是最初注册的正确资源地址,不会因为类型转换出现偏移错误。
内容的提问来源于stack exchange,提问作者Grigor Aleksanyan
相关产品推荐
相关产品推荐

