12. 智能指针与数组:管理动态数组的正确姿势

说实话,智能指针管理单个对象,大家基本都会用。但一碰到数组,很多人就开始犯迷糊了。我见过不少项目里,有人直接用 std::unique_ptr<int> 去管一个 new 出来的数组,结果析构的时候只调了 delete 而不是 delete[]——嗯,内存泄漏就这么悄无声息地来了。

这一章,咱们就把智能指针和数组那点事彻底说清楚。

12.1 默认行为:一个常见的坑

先看一段代码:

std::unique_ptr<int> p(new int[10]);  // 编译通过,但行为未定义!

这段代码能编译,但运行时大概率会出问题。为什么?因为 std::unique_ptr<int> 的默认删除器调用的是 delete,而不是 delete[]。你想想看,new 出来的数组,析构时只释放了第一个元素的内存,后面 9 个元素的内存就永远漏在那了。

我在项目中排查过一个诡异的内存泄漏,查了两天才发现是某位同事用 shared_ptr 管理数组时踩了这个坑。从那以后,我养成了一个习惯:只要看到智能指针和数组搭配,一定先检查删除器。

⚠️ 警告: 永远不要用 std::unique_ptr<T>std::shared_ptr<T> 直接管理 new T[N] 分配的数组。默认删除器不匹配,会导致未定义行为。

12.2 正确姿势一:使用数组特化版本

C++11 标准库为 std::unique_ptr 提供了数组特化版本。写法很简单:

std::unique_ptr<int[]> p(new int[10]);  // 正确!删除器自动使用 delete[]

注意这里的模板参数是 int[] 而不是 int。这个特化版本做了两件事:

  • 重载了 operator[],让你能像普通数组一样访问元素
  • 默认删除器使用 delete[],保证正确释放

举个例子:

std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
for (int i = 0; i < 5; ++i) {
    std::cout << arr[i] << " ";  // 直接使用下标访问
}
// 离开作用域时自动调用 delete[]

我个人习惯,只要涉及动态数组,一律用 unique_ptr<T[]>。这样既安全,语义也清晰。

12.3 正确姿势二:自定义删除器

std::shared_ptr 呢?它没有数组特化版本。怎么办?

答案是:自定义删除器。

std::shared_ptr<int> p(new int[10], [](int* p) {
    delete[] p;
});

这里我们传了一个 lambda 作为删除器,明确告诉 shared_ptr:析构时请调用 delete[]

不过要注意,shared_ptr 不提供 operator[],所以访问元素得用 p.get()[i]。说实话,这写法有点丑。所以我一般建议:

  • 优先用 unique_ptr<T[]>,简单直接
  • 实在需要共享所有权,再用 shared_ptr + 自定义删除器
💡 小技巧: 如果你经常需要 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[]>());
}
这样每次调用 make_shared_array<int>(10) 就行了,省得重复写删除器。

12.4 为什么不推荐用 vector 替代?

你可能会问:既然数组这么麻烦,为什么不直接用 std::vector

嗯,这个问题问得好。大多数情况下,std::vector 确实是更好的选择。但有些场景,智能指针数组有它不可替代的优势:

场景 推荐方案 原因
需要动态大小,且频繁增删 std::vector 自动管理内存,接口丰富
固定大小,且需要与 C 接口交互 unique_ptr<T[]> 直接持有原始指针,零开销
需要共享数组所有权 shared_ptr<T> + 删除器 引用计数管理生命周期
需要多态数组(如基类指针数组) unique_ptr<Base[]> 正确调用虚析构函数

我在做音视频编解码库的时候,就遇到过必须用 unique_ptr<uint8_t[]> 的场景。因为要直接把数据 buffer 传给 C 语言的编码器 API,vector 的连续内存保证虽然没问题,但接口上多了一层包装,反而不如原始数组指针来得直接。

12.5 知识体系总览

下面这张图,把智能指针管理数组的核心逻辑串起来了:

智能指针管理动态数组:决策流程图 需要动态数组 共享所有权? (多个所有者) unique_ptr<T[]> 自动 delete[] 支持 operator[] 零开销,与C接口兼容 shared_ptr<T> + 自定义删除器 需 lambda 或 std::default_delete<T[]> 用 p.get()[i] 访问元素 核心原则:删除器必须与分配方式匹配

12.6 避坑指南

最后,分享几个我踩过的坑:

坑1:make_unique<int[]>(10) 创建数组?C++14 确实支持,但注意它返回的是 unique_ptr<int[]>,不是 unique_ptr<int>。类型别搞混了。

坑2: shared_ptr 的别名构造函数。有人会写 shared_ptr<int> p(new int[10], default_delete<int[]>()),但如果你用 p.get() 拿到指针后传给另一个 shared_ptr,两个智能指针会各自管理同一块内存——double free 等着你。

坑3: 多态数组。如果数组元素是基类指针,记得基类要有虚析构函数。否则 delete[] 只会调用基类的析构,派生类的资源就漏了。我曾经在一个插件系统里吃过这个亏,排查了一整个下午。

嗯,关于智能指针和数组,核心就这些。记住一句话:分配方式决定删除方式。new 出来的数组,就用 delete[] 去释放。智能指针只是帮你自动调用,但调用什么,你得告诉它。


公众号:蓝海资料掘金营,微信deep3321