14. 智能指针与数组:unique_ptr<T[]>、shared_ptr 管理数组的陷阱、std::vector 的对比
数组和智能指针,这俩东西凑在一起,坑特别多。
我见过不少同事,写 C++ 写了三五年,一碰到「智能指针 + 数组」就翻车。说白了,shared_ptr 默认的删除器是 delete,不是 delete[]。你拿它管数组,内存泄漏没跑。
今天咱们就把这块彻底捋清楚。我会结合自己踩过的坑,把 unique_ptr<T[]>、shared_ptr 管理数组的陷阱,以及为什么很多时候你根本不该用它们——直接用 std::vector 就完了——一次性讲透。
14.1 unique_ptr<T[]>:C++ 给你的「数组专用」智能指针
C++11 开始,unique_ptr 就有了针对数组的偏特化版本:unique_ptr<T[]>。
它和普通的 unique_ptr<T> 有什么区别?
- 删除器不同:
unique_ptr<T[]>默认调用delete[],而不是delete。 - 支持下标访问:你可以直接用
p[i]访问元素,而unique_ptr<T>只能解引用*p。 - 不支持
*和->:因为数组不是单个对象,解引用没有意义。
我个人习惯,只要需要动态分配数组,优先考虑 unique_ptr<T[]>。它轻量、零开销,而且语义明确——「我管的是一个数组,不是单个对象」。
#include <memory>
#include <iostream>
int main() {
// 分配一个包含 10 个 int 的数组
std::unique_ptr<int[]> arr(new int[10]{1, 2, 3, 4, 5});
// 直接下标访问
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << ' ';
}
// 自动调用 delete[],无需手动释放
return 0;
}
嗯,这里要注意:unique_ptr<T[]> 不支持自定义删除器。如果你需要特殊清理逻辑(比如释放 GPU 内存),那就得用 unique_ptr<T, Deleter> 自己包装了。
14.2 shared_ptr 管理数组:一个经典的陷阱
好,重点来了。shared_ptr 管理数组,是 C++ 里最容易踩的坑之一。
为什么?因为 shared_ptr 的默认删除器是 delete,不是 delete[]。
你想想看:
std::shared_ptr<int> sp(new int[10]); // 危险!未定义行为!
这段代码编译没问题,运行也不一定立刻崩溃。但 shared_ptr 析构时只会调用 delete,而不是 delete[]。对于内置类型 int,可能不会马上出事——但这是纯粹的运气,不是正确。
我曾经在一个线上服务里排查内存泄漏,查了整整两天。最后发现是某位同事用 shared_ptr 管了一个结构体数组,析构时只释放了第一个元素。嗯,从那以后,我对 shared_ptr 管数组这件事就特别敏感。
正确的做法是:提供自定义删除器。
std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
// 或者用 lambda
std::shared_ptr<int> sp(new int[10], [](int* p) { delete[] p; });
但这样写,你仍然不能用 sp[i] 访问元素——因为 shared_ptr<int> 的类型信息里不包含「这是一个数组」。你需要先 sp.get()[i],非常别扭。
shared_ptr<T[]> 是不合法的。C++17 开始,标准库提供了部分支持,但依然不推荐。如果你非要用 shared_ptr 管数组,请务必提供自定义删除器,并且用 sp.get()[i] 访问元素。
14.3 为什么我建议你优先用 std::vector
说实话,大部分场景下,你根本不需要用智能指针管数组。
你想想看,std::vector 本身就是动态数组,它自己管理内存,支持拷贝、移动、下标访问,还能用范围 for 循环。它不香吗?
| 特性 | unique_ptr<T[]> | shared_ptr + 数组 | std::vector |
|---|---|---|---|
| 内存管理 | 自动 delete[] | 需自定义删除器 | 自动管理 |
| 下标访问 | 支持 p[i] | 需 sp.get()[i] | 支持 v[i] |
| 拷贝语义 | 不支持拷贝 | 支持共享所有权 | 深拷贝 |
| 动态扩容 | 不支持 | 不支持 | 支持 |
| 迭代器支持 | 有限 | 无 | 完整 STL 迭代器 |
| 性能开销 | 零开销 | 引用计数 + 删除器 | 少量(可忽略) |
我个人习惯是:
- 如果数组大小固定,且不需要共享所有权 →
unique_ptr<T[]> - 如果数组需要共享所有权 → 用
std::shared_ptr<std::vector<T>>,而不是shared_ptr管原生数组 - 如果数组需要动态扩容、拷贝、遍历 → 无脑
std::vector
说白了,std::vector 是 C++ 标准库给你的「默认选择」。除非你有非常明确的理由(比如嵌入式环境禁止异常、需要极致性能),否则别自己折腾数组内存管理。
14.4 知识体系:智能指针与数组的核心逻辑
下面这张图,帮你把今天讲的内容串起来。
14.5 避坑指南:我踩过的那些坑
最后,分享几个我实际遇到过的教训。
坑一:shared_ptr 管数组,析构时只释放第一个元素
我曾经在某个模块里,用 shared_ptr<int> 管了一个图像像素数组。程序跑起来没问题,但内存占用持续增长。查了两天,才发现是删除器不对。修复后,内存曲线直接平了。
技巧:如果你非要用 shared_ptr 管数组,建议封装一个辅助函数:
template<typename T>
std::shared_ptr<T> make_shared_array(size_t size) {
return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
这样至少不会忘记删除器。
坑二:unique_ptr<T[]> 和 unique_ptr<T> 混用
有人写 unique_ptr<int> p(new int[10]),编译通过,运行时 delete 而不是 delete[]。对于 POD 类型可能不崩,但一旦有自定义析构函数,就是未定义行为。
建议:用 auto p = std::make_unique<int[]>(10),C++14 开始支持。这样类型和分配方式完全匹配,不会出错。
坑三:用智能指针管数组,却需要动态扩容
我见过有人用 unique_ptr<T[]> 然后手动 realloc——这完全违背了智能指针的初衷。如果你需要动态扩容,请直接用 std::vector。它内部就是动态数组,而且异常安全。
好了,关于智能指针与数组,核心就这些。记住一句话:能用 vector 就别折腾,用 unique_ptr<T[]> 要确认语义,用 shared_ptr 管数组一定要给删除器。
公众号:蓝海资料掘金营,微信deep3321