8. 智能指针高级技巧:enable_shared_from_this、别名构造函数、指针类型转换、性能对比
智能指针用久了,你会发现有些场景光靠shared_ptr和unique_ptr的基本用法根本搞不定。比如在类内部怎么安全地返回自己的shared_ptr?怎么让两个shared_ptr共享同一个对象但指向不同的成员?这些坑我当年都踩过,今天咱们一次性说清楚。
8.1 enable_shared_from_this:解决“自己给自己”的难题
先看一个经典场景。你写了一个网络连接类,内部有个回调函数需要把当前对象传给外部。你可能会这么写:
class Connection {
public:
void start() {
// 假设有个异步回调
do_something([this]() {
// 这里用 this 安全吗?
process();
});
}
private:
void process() { /* ... */ }
};
问题在哪?如果外部已经没人持有这个Connection对象了,回调执行时this已经变成野指针。你想想看,这多危险。
有人会说:那我传shared_ptr<Connection>进去不就行了?但问题是,Connection内部怎么拿到自己的shared_ptr?直接shared_ptr<Connection>(this)?千万别!这会导致两个独立的控制块管理同一个对象,double free 等着你。
正确的做法是继承enable_shared_from_this:
class Connection : public std::enable_shared_from_this<Connection> {
public:
void start() {
do_something([self = shared_from_this()]() {
self->process();
});
}
private:
void process() { /* ... */ }
};
这里有个关键点:必须在对象已经被shared_ptr管理之后才能调用shared_from_this()。我在项目中遇到过有人直接在构造函数里调shared_from_this(),结果抛了std::bad_weak_ptr异常。为什么会这样?因为构造函数执行时,shared_ptr还没创建完成,内部的weak_ptr还没初始化。
shared_from_this()只能在对象已经被shared_ptr管理后调用。如果必须在构造时获取自身指针,考虑用工厂模式。
8.2 别名构造函数:指向成员,共享所有权
这个技巧说实话,用的人不多,但用对了非常优雅。别名构造函数允许你创建一个shared_ptr,它指向某个对象的成员,但共享的是原对象的控制块。
struct BigObject {
int data;
std::string name;
};
auto big = std::make_shared<BigObject>();
// 别名构造:指向 name,但共享 big 的所有权
std::shared_ptr<std::string> name_ptr(big, &big->name);
现在name_ptr指向name成员,但它持有的是big的控制块。只要name_ptr还活着,big就不会被销毁。这在处理大型对象的子组件时特别有用——你不需要复制整个对象,只需要延长它的生命周期。
我个人习惯在实现观察者模式时用这个技巧。比如一个UIWidget对象,它的某个子属性需要被外部监听,我就可以创建一个指向该属性的别名shared_ptr,既安全又轻量。
8.3 智能指针的类型转换
原始指针有static_cast、dynamic_cast、const_cast、reinterpret_cast,智能指针也有对应的版本。不过要注意,它们不是对指针本身做转换,而是创建新的智能指针。
| 函数 | 对应原始指针转换 | 说明 |
|---|---|---|
static_pointer_cast |
static_cast |
编译期类型转换,不检查运行时类型 |
dynamic_pointer_cast |
dynamic_cast |
运行时类型检查,失败返回空指针 |
const_pointer_cast |
const_cast |
移除或添加 const 属性 |
reinterpret_pointer_cast |
reinterpret_cast |
位模式重解释,慎用 |
举个例子,假设你有一个基类指针的shared_ptr,想转成派生类:
class Base { virtual ~Base() = default; };
class Derived : public Base { void specific() {} };
std::shared_ptr<Base> base = std::make_shared<Derived>();
auto derived = std::dynamic_pointer_cast<Derived>(base);
if (derived) {
derived->specific();
}
这里要注意:dynamic_pointer_cast失败时返回空shared_ptr,不会抛异常。嗯,这点和dynamic_cast对指针的行为一致。
dynamic_pointer_cast,除非你能百分之百确定类型。曾经有个同事用static_pointer_cast做向下转换,结果类型不对,程序跑飞了。调试了三天才发现是类型转换的问题。
8.4 性能对比:别让智能指针成为瓶颈
智能指针不是零开销的。咱们来做个对比,看看它们到底慢在哪。
| 操作 | 原始指针 | unique_ptr | shared_ptr |
|---|---|---|---|
| 构造 | 几乎零开销 | 几乎零开销 | 一次堆分配(控制块) |
| 拷贝 | 复制地址 | 不允许拷贝 | 原子操作递增引用计数 |
| 移动 | 复制地址 | 转移所有权 | 转移所有权,引用计数不变 |
| 析构 | 无操作 | 删除对象 | 原子操作递减引用计数,可能触发删除 |
你看,shared_ptr的拷贝和析构都涉及原子操作。在高并发场景下,这个开销会被放大。我做过一个测试:在8核机器上,大量shared_ptr拷贝操作比原始指针慢了5-10倍。
那怎么办?我的建议是:
- 能用
unique_ptr就别用shared_ptr。所有权明确时,unique_ptr几乎和原始指针一样快。 - 传引用而不是拷贝。函数参数用
const shared_ptr<T>&,避免不必要的引用计数增减。 - 考虑
make_shared的优化。它把对象和控制块分配在同一个内存块上,减少一次堆分配,还能提高缓存局部性。 - 用
weak_ptr打破循环引用。循环引用会导致内存泄漏,这个坑我踩过不止一次。
unique_ptr替代shared_ptr。
8.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到,enable_shared_from_this解决的是对象内部获取自身shared_ptr的问题,别名构造函数解决的是共享所有权但指向不同成员的问题,类型转换解决的是多态场景下的指针转换问题,而性能对比则是提醒我们不要滥用。
说实话,这些高级技巧不是每天都能用到。但一旦遇到合适的场景,它们能帮你写出更安全、更高效的代码。我个人建议你先把enable_shared_from_this和性能对比这两块吃透,因为实际项目中遇到的最多。别名构造函数和类型转换嘛,遇到了再查也来得及。
shared_ptr。后来被性能问题教育了,才学会根据场景选择。记住:unique_ptr是默认选项,shared_ptr是权衡后的选择。
公众号:蓝海资料掘金营,微信deep3321