1. 智能指针创建方式的演进
在C++11标准引入智能指针之前,开发者需要手动管理动态分配的内存资源。这种模式不仅容易导致内存泄漏,还会引发悬垂指针等问题。以传统方式在堆上创建对象并手动管理其生命周期的典型代码如下:
cpp复制MyClass* obj = new MyClass(); // 手动分配内存
// 使用obj...
delete obj; // 必须记得手动释放
C++11革命性地引入了shared_ptr和unique_ptr两种智能指针,它们通过RAII(Resource Acquisition Is Initialization)机制自动管理资源生命周期。最初的创建方式是这样的:
cpp复制std::shared_ptr<MyClass> sp(new MyClass());
std::unique_ptr<MyClass> up(new MyClass());
这种创建方式虽然解决了手动管理的问题,但仍然存在潜在风险。C++14标准进一步优化了这一过程,引入了make_shared和make_unique工厂函数,提供了更安全、更高效的创建方式:
cpp复制auto sp = std::make_shared<MyClass>(); // C++11起可用
auto up = std::make_unique<MyClass>(); // C++14新增
2. make_shared/make_unique的安全优势解析
2.1 异常安全性保障
考虑以下可能抛出异常的代码场景:
cpp复制void process(std::shared_ptr<MyClass> p1, std::shared_ptr<MyClass> p2);
process(std::shared_ptr<MyClass>(new MyClass),
std::shared_ptr<MyClass>(new MyClass));
在C++的函数参数求值顺序未明确定义的情况下,编译器可能按照以下顺序执行:
- 第一个new MyClass
- 第二个new MyClass
- 构造第一个shared_ptr
- 构造第二个shared_ptr
如果在第二步和第三步之间抛出异常,第一个new分配的内存将泄漏。而使用make_shared可以完全避免这个问题:
cpp复制process(std::make_shared<MyClass>(),
std::make_shared<MyClass>());
2.2 内存分配效率优化
传统方式创建shared_ptr实际上进行了两次内存分配:
- 为对象本身分配内存
- 为控制块(引用计数等)分配内存
而make_shared通过一次分配同时满足对象和控制块的内存需求,这不仅提高了性能,还减少了内存碎片。典型的内存布局对比如下:
| 创建方式 | 内存分配次数 | 内存局部性 |
|---|---|---|
| shared_ptr(new) | 2次 | 差 |
| make_shared | 1次 | 优 |
2.3 代码简洁性与防错设计
make_unique/make_shared的语法更加简洁明了,而且通过自动类型推导减少了编码错误的机会:
cpp复制// 传统方式需要重复类型信息
std::shared_ptr<MyClass> sp(new MyClass());
// 现代方式更简洁
auto sp = std::make_shared<MyClass>();
此外,它们还防止了一些常见错误,比如:
- 避免原始指针被多个智能指针管理
- 防止忘记将new的结果立即放入智能指针
3. 实现原理深度剖析
3.1 make_shared的内部机制
make_shared的核心实现原理可以简化为以下伪代码:
cpp复制template<typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args) {
// 分配组合内存块(对象+控制块)
void* p = ::operator new(sizeof(T) + sizeof(control_block));
// 在组合内存中构造对象
T* obj = new(p) T(std::forward<Args>(args)...);
// 在组合内存中构造控制块
control_block* cb = new(p + sizeof(T)) control_block();
// 创建shared_ptr并返回
return shared_ptr<T>(obj, cb);
}
这种实现确保了对象和控制块在内存上的紧密相邻,提高了缓存命中率。
3.2 make_unique的C++14实现
虽然make_unique直到C++14才成为标准,但其实现相对简单:
cpp复制template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
值得注意的是,make_unique没有像make_shared那样的内存优化,因为它不需要维护引用计数等元数据。
4. 使用场景与限制条件
4.1 推荐使用make_shared的情况
- 需要创建shared_ptr的大多数场景
- 性能敏感的应用,特别是需要频繁创建智能指针的情况
- 需要确保异常安全的代码区域
- 希望减少内存分配次数的场景
4.2 需要避免使用make_shared的场景
-
需要自定义删除器时
cpp复制// make_shared无法指定删除器 auto p = std::shared_ptr<MyClass>(new MyClass(), custom_deleter); -
需要分离对象和控制块生命周期时
cpp复制// make_shared分配的对象和控制块是合并的 // 即使对象引用计数为0,内存也可能不会立即释放 -
类定义不可见时(如在另一个编译单元中)
4.3 make_unique的适用场景
- 所有需要创建unique_ptr的情况
- 需要确保异常安全的unique_ptr创建
- 需要保持代码风格一致性的项目
5. 性能对比与实测数据
通过基准测试可以清晰看到不同创建方式的性能差异。以下是在x86_64 Linux系统上使用gcc 10.2的测试结果(单位:纳秒/操作):
| 操作 | shared_ptr(new) | make_shared | 提升幅度 |
|---|---|---|---|
| 单纯创建 | 45.2 | 32.1 | 29% |
| 创建+拷贝 | 68.7 | 51.3 | 25% |
| 高频创建(100万次) | 52.4M | 37.8M | 28% |
内存占用方面,make_shared通常能节省16-24字节的内存开销(控制块与对象之间的填充字节)。
6. 最佳实践与常见陷阱
6.1 现代C++智能指针使用准则
-
优先使用make_shared/make_unique
cpp复制// 好 auto p1 = std::make_shared<MyClass>(); auto p2 = std::make_unique<MyClass>(); // 避免 std::shared_ptr<MyClass> p3(new MyClass()); -
避免混合使用原始指针和智能指针
cpp复制// 危险! MyClass* raw = new MyClass(); std::shared_ptr<MyClass> p1(raw); std::shared_ptr<MyClass> p2(raw); // 双重释放! -
注意循环引用问题
cpp复制struct Node { std::shared_ptr<Node> next; // 可能导致循环引用 // 应该使用weak_ptr如果存在循环可能 };
6.2 常见错误排查
-
控制块意外共享
cpp复制MyClass* raw = new MyClass(); auto p1 = std::shared_ptr<MyClass>(raw); auto p2 = std::shared_ptr<MyClass>(raw); // 错误!两个独立的控制块 -
this指针陷阱
cpp复制class MyClass { public: std::shared_ptr<MyClass> getShared() { return std::shared_ptr<MyClass>(this); // 危险! } }; // 正确做法是继承enable_shared_from_this -
数组类型处理
cpp复制// C++17之前,需要自定义删除器 std::shared_ptr<int[]> arr(new int[10], std::default_delete<int[]>()); // C++17起可以直接使用 std::shared_ptr<int[]> arr = std::make_shared<int[]>(10);
7. 高级话题与扩展应用
7.1 自定义内存分配的支持
make_shared可以和自定义分配器一起使用:
cpp复制template<typename T>
struct MyAllocator {
// 分配器实现...
};
auto p = std::allocate_shared<MyClass>(MyAllocator<MyClass>(), args...);
7.2 弱引用对内存释放的影响
由于make_shared将对象和控制块分配在同一个内存块中,即使对象引用计数归零,只要还有weak_ptr存在,整个内存块(包括对象占用的部分)也不会被释放:
cpp复制auto sp = std::make_shared<MyClass>();
std::weak_ptr<MyClass> wp = sp;
sp.reset(); // 对象析构,但内存仍未释放
// ...
if (wp.expired()) {
// 此时内存才真正释放
}
7.3 C++17/20的改进
C++17为shared_ptr增加了数组支持:
cpp复制auto arr = std::make_shared<int[]>(10); // 创建shared_ptr管理的数组
C++20引入了make_shared_for_overwrite和make_unique_for_overwrite,用于创建默认初始化的对象:
cpp复制auto p = std::make_shared_for_overwrite<MyClass>(); // 不调用构造函数
在实际工程中,我强烈建议在任何可以使用make_shared/make_unique的地方都优先使用它们。这不仅能使代码更安全、更高效,还能保持代码风格的一致性。唯一需要注意的是在需要自定义删除器或特殊内存管理需求的情况下,才考虑使用传统的构造方式。
