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],非常别扭。

警告:C++17 之前,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 知识体系:智能指针与数组的核心逻辑

下面这张图,帮你把今天讲的内容串起来。

智能指针 + 数组 unique_ptr<T[]> 自动 delete[] 支持 p[i] 下标访问 零开销,轻量级 shared_ptr + 数组 默认 delete,非 delete[] 需自定义删除器 访问需 sp.get()[i] std::vector 自动管理内存 支持动态扩容 完整 STL 迭代器 推荐优先级:vector > unique_ptr<T[]> > shared_ptr + 数组

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