C++17 std::shared_ptr数组支持详解:原理、应用与性能优化 1. 项目概述为什么C17的std::shared_ptr对数组如此重要如果你和我一样是从C98/03甚至更早的版本一路写过来的那么对智能指针管理数组这件事多半会有点“心理阴影”。在C11/14的时代我们有了std::shared_ptr和std::unique_ptr但它们在处理数组时待遇是天差地别的。std::unique_ptr从设计之初就优雅地支持了数组特化比如std::unique_ptrT[]拥有完整的operator[]析构时也能正确调用delete[]。但std::shared_ptr呢它像个“瘸腿”的巨人标准库明确告诉你别用std::shared_ptr去直接管理通过new T[N]分配的数组因为它的默认删除器是delete而不是delete[]强行使用会导致未定义行为——通常是内存泄漏或者更糟的程序崩溃。于是在C17之前我们不得不使用各种“土办法”要么自己写一个调用delete[]的定制删除器比如一个简单的lambda[](T* p){ delete[] p; }然后把这个删除器作为第二个参数传给std::shared_ptr的构造函数要么就干脆放弃std::shared_ptr改用std::vectorstd::shared_ptrT这种“套娃”结构虽然安全但间接访问和内存开销都增加了。这些方法都能用但总让人觉得不够优雅不够“标准”像是在用胶水粘合一个本应浑然一体的功能。C17的到来终于为std::shared_ptr的数组支持“正名”了。它通过引入std::shared_ptrT[]这个特化版本以及配套的构造函数和operator[]让共享所有权的动态数组管理变得和std::unique_ptrT[]一样自然、安全。这不仅仅是语法糖它意味着类型安全std::shared_ptrT[]的类型系统明确表达了“这是一个数组”编译器能在更多环节帮你检查错误。行为正确其默认删除器就是delete[]无需手动指定从根本上杜绝了误用delete的风险。接口统一提供了operator[]使得数组元素的访问方式和原生指针、std::vector一样直观。生态融合能更好地与现代C的其他组件如标准算法库协同工作。这个改进看似不大但对于需要共享生命周期、且规模动态变化的数组场景比如一个图像处理管道中多个模块共享一大块像素数据或者一个资源管理器持有多个同类型资源句柄它极大地简化了代码提升了安全性和可读性。接下来我们就深入这个“新武器”的每一个细节。2.std::shared_ptrT[]的核心特性与底层原理2.1 特化类型的语义与构造函数std::shared_ptrT[]是一个完全独立的模板特化。当你写下这个类型时你就是在告诉编译器和代码的阅读者“这个智能指针管理的是一个T类型的数组其大小在运行时确定。”C17为它新增了几个关键的构造函数默认构造函数创建一个空的、不持有任何数组的shared_ptr。std::shared_ptrint[] emptyArr; // 空的数组shared_ptr接管裸指针数组这是最常用的方式。但这里有一个至关重要的陷阱你必须使用new T[N]分配的数组并且只能将这样的指针传递给std::shared_ptrT[]的构造函数。如果你传递一个new T分配的单个对象的指针或者一个malloc分配的内存行为是未定义的。// 正确用法 std::shared_ptrint[] arr(new int[10]); // 正确管理一个包含10个int的数组 // 编译器会确保使用 delete[] 进行析构 // 错误且危险的用法 // std::shared_ptrint[] badPtr(new int); // 错误类型不匹配可能导致 delete[] 作用于单个对象 // std::shared_ptrint[] anotherBadPtr((int*)malloc(10 * sizeof(int))); // 错误删除器不匹配别名构造Aliasing Constructor这是shared_ptr一个强大但容易被忽略的特性对数组同样适用。它允许你创建一个shared_ptr其“所有权”与另一个shared_ptr共享即控制块相同但存储的指针get()返回的指针可以指向另一个地址通常是所拥有对象的一个成员或数组中的一个元素。这对于返回数组切片或内部结构的指针非常有用同时保持底层数组的完整生命周期。std::shared_ptrint[] originalArr(new int[100]); // 创建一个新的shared_ptr它与originalArr共享数组的所有权 // 但“指向”数组的第10个元素originalArr.get() 10 std::shared_ptrint elementPtr(originalArr, originalArr.get() 10); // 现在即使originalArr被销毁只要elementPtr还存在整个100个元素的数组就不会被释放。 // elementPtr.get() 返回的是 originalArr[10]。2.2operator[]与访问安全std::shared_ptrT[]提供了下标运算符operator[]其行为和原生数组指针或std::vector::operator[]类似它不进行边界检查。访问越界是未定义行为。std::shared_ptrdouble[] data(new double[5]); data[0] 3.14; // 正确 data[4] 2.71; // 正确访问最后一个元素 // data[5] 1.41; // 未定义行为越界访问。这里有一个重要的注意事项std::shared_ptrT[]的operator[]返回的是T引用。这意味着你可以通过它修改数组元素。但这也意味着如果你有一个指向const T类型的数组的shared_ptr即std::shared_ptrconst T[]那么operator[]返回的就是const T元素是不可修改的。std::shared_ptrconst int[] constArr(new int[3]{1, 2, 3}); int value constArr[1]; // 正确读取 // constArr[1] 4; // 编译错误不能给常量赋值。实操心得由于没有边界检查在复杂的循环或计算中要非常小心下标计算。如果安全性是首要考虑并且不需要共享所有权std::vector通常是更好的选择。如果必须用shared_ptr且需要边界检查可以考虑将它包装在一个提供at()方法的自定义类中或者使用std::spanC20来提供边界安全的视图。2.3 删除器与自定义分配/释放虽然std::shared_ptrT[]的默认删除器是delete[]但你仍然可以像普通的shared_ptr一样提供自定义的删除器。这在管理非标准方式分配的数组时是必需的例如使用malloc/free的C库或者使用特定内存池分配的数组。// 使用C库函数分配和释放的数组 struct CArrayDeleter { void operator()(int* p) const { std::cout Custom deleter freeing array.\n; free(p); // 使用 free 释放 } }; std::shared_ptrint[] cStyleArray((int*)malloc(100 * sizeof(int)), CArrayDeleter()); // 或者使用lambda表达式更简洁 auto lambdaDeleter [](int* p) { free(p); }; std::shared_ptrint[] anotherArray((int*)malloc(50 * sizeof(int)), lambdaDeleter);关键点当你提供自定义删除器时shared_ptr的类型仍然是std::shared_ptrT[]这保持了接口的一致性你仍然可以使用operator[]。删除器的存在不影响shared_ptr的模板类型。2.4 与std::make_shared的遗憾与替代方案一个显著的缺失是C17没有为std::shared_ptrT[]提供对应的std::make_shared重载。也就是说你不能这样写// auto arr std::make_sharedint[](10); // 在C17/20中这是错误的这是因为std::make_shared的设计需要一次性分配对象内存和控制块内存以优化性能。而为可变长度数组实现这种优化更为复杂标准委员会在C17时没有就此达成一致直到C20std::make_shared才支持了T[N]但T[]的动态数组仍然不行。那么如何优雅地构造呢你有以下几种选择直接使用new最直接但可能导致两次内存分配对象数组一次控制块一次性能稍差。std::shared_ptrMyClass[] objects(new MyClass[10]);使用std::make_shared创建单个对象然后手动管理数组不行这违背了设计初衷。不要这么做。使用std::vector或std::unique_ptr转换如果所有权模型允许std::vectorstd::shared_ptrMyClass vec; vec.reserve(10); for (int i 0; i 10; i) { vec.push_back(std::make_sharedMyClass()); } // 现在vec管理着10个独立的shared_ptrMyClass // 如果需要“数组”视图可以考虑使用std::spanconst std::shared_ptrMyClass (C20)我的建议是如果性能不是极端敏感且数组大小在运行时确定直接使用new配合std::shared_ptrT[]构造函数是清晰且正确的做法。明确性比微小的性能差异更重要。3. 实战从旧模式迁移到C17新语法让我们通过一个具体的例子看看如何将C14及之前的“手工删除器”模式升级到C17的“一等公民”模式。场景一个简单的图像数据缓冲区宽度和高度动态确定需要在多个滤镜处理函数之间传递和共享。C14及之前使用自定义删除器class ImageBuffer { private: struct ArrayDeleter { void operator()(uint8_t* p) const { delete[] p; } }; std::shared_ptruint8_t data_; // 注意类型是uint8_t不是uint8_t[] size_t width_; size_t height_; public: ImageBuffer(size_t w, size_t h) : width_(w), height_(h), data_(new uint8_t[w * h], ArrayDeleter()) // 必须显式指定删除器 {} uint8_t* get() const { return data_.get(); } uint8_t at(size_t x, size_t y) { // 手动计算偏移没有 operator[] 语法糖 return data_.get()[y * width_ x]; } // ... 其他方法 };C17及之后使用std::shared_ptrT[]class ImageBuffer { private: std::shared_ptruint8_t[] data_; // 类型明确是数组 size_t width_; size_t height_; public: ImageBuffer(size_t w, size_t h) : width_(w), height_(h), data_(new uint8_t[w * h]) // 无需指定删除器默认就是delete[] {} uint8_t* get() const { return data_.get(); } // 访问方式一使用 get() 和原生指针算术 uint8_t at(size_t x, size_t y) { return data_.get()[y * width_ x]; } // 访问方式二更安全封装更好提供一个返回“行指针”或“元素引用”的方法 // 或者如果我们改变设计让data_直接是二维的shared_ptruint8_t[]的数组那会更复杂。 // 但至少类型本身表达了它是数组。 // 新增可以直接使用 operator[] 进行一维索引如果按行优先存储 uint8_t operator[](size_t index) { return data_[index]; // 看直接使用 shared_ptr 的 operator[] } const uint8_t operator[](size_t index) const { return data_[index]; } };升级带来的好处代码更简洁移除了自定义删除器类ArrayDeleter。意图更清晰std::shared_ptruint8_t[]这个类型声明本身就是一个文档明确表示管理的是数组。安全性提升编译器能进行更多的类型检查。如果你不小心把new uint8_t单个对象的指针传给它会得到一个类型不匹配的错误或警告。访问更直观可以直接使用data_[index]代码更像在使用标准容器。4. 高级用法、陷阱与性能考量4.1 与标准库算法协同工作std::shared_ptrT[]存储的指针可以像普通指针一样用于迭代器范畴。你可以用std::shared_ptrT[]::get()获取起始指针然后将其传递给标准库算法。std::shared_ptrint[] values(new int[1000]); // 使用标准算法填充数组 std::iota(values.get(), values.get() 1000, 0); // 填充 0, 1, 2, ..., 999 // 查找元素 auto it std::find(values.get(), values.get() 1000, 42); if (it ! values.get() 1000) { std::cout Found value at index: (it - values.get()) std::endl; } // 排序 std::sort(values.get(), values.get() 1000, std::greaterint());注意算法接收的是裸指针迭代器这意味着算法内部对元素的任何修改都会直接影响shared_ptr管理的数组。同时你需要自己确保传递的指针范围[begin, end)是有效的。4.2std::shared_ptrT[]与std::unique_ptrT[]的抉择这是设计中常见的选择题。它们的核心区别在于所有权语义std::unique_ptrT[]独占所有权。更轻量通常无需控制块开销移动效率高但无法共享。std::shared_ptrT[]共享所有权。有引用计数开销但允许多个上下文安全地共享同一数组数据。选择指南特性std::unique_ptrT[]std::shared_ptrT[]所有权独占移动共享拷贝/移动开销很小通常仅一个指针较大指针控制块含引用计数等线程安全对象本身非线程安全移动需同步引用计数操作原子性线程安全但数据访问仍需同步C版本C11C17推荐场景数组在单一作用域或单一对象生命周期内明确使用数组需要在多个未知生命周期的对象或线程间共享make_函数std::make_uniqueT[](N)(C14)无直接make_shared(C17/20)经验法则默认使用std::unique_ptrT[]除非你确凿地需要共享所有权。共享所有权会增加架构的复杂性和循环引用的风险尽管数组本身不包含指针但持有它的shared_ptr可能形成环。4.3 循环引用与std::weak_ptrstd::shared_ptr的经典陷阱——循环引用——在管理数组时同样存在。假设你有一个树形结构每个节点持有一个shared_ptr数组指向其子节点而子节点又想持有指向父节点的shared_ptr这就构成了循环引用导致内存泄漏。解决方案依然是std::weak_ptr。weak_ptr可以观测一个由shared_ptr管理的对象或数组但不增加其引用计数。它不能直接访问资源必须通过lock()方法尝试提升为shared_ptr。struct TreeNode { std::string name; std::weak_ptrTreeNode parent; // 使用 weak_ptr 避免循环引用 std::vectorstd::shared_ptrTreeNode children; // 子节点数组这里用vector更合适 // 如果非要演示 shared_ptrT[]假设每个节点有固定大小的数据块 std::shared_ptrfloat[] nodeData; TreeNode(const std::string n, size_t dataSize) : name(n), nodeData(new float[dataSize]) {} }; void processTree() { auto root std::make_sharedTreeNode(root, 10); auto child std::make_sharedTreeNode(child, 5); child-parent root; // 弱引用不会增加root的引用计数 root-children.push_back(child); // 当root和child超出作用域它们会被正确释放因为parent是weak_ptr没有形成强引用环。 }对于数组std::weak_ptr同样可以特化为std::weak_ptrT[]用于观测一个由std::shared_ptrT[]管理的数组。用法完全一致。4.4 性能开销与优化点使用std::shared_ptrT[]需要意识到其性能开销内存开销除了管理的数组本身还有一个控制块control block包含引用计数、弱引用计数、删除器等。这通常比std::unique_ptr或裸指针多出几十字节的开销。时间开销引用计数的增减是原子操作除非使用std::shared_ptr的非线程安全别名即使在单线程环境下也有一定成本。频繁地拷贝shared_ptr例如在循环中传递会影响性能。缓存不友好控制块和数组数据通常是分开分配的可能位于不同的内存页对CPU缓存不友好。优化建议传递引用在函数中如果不需要取得所有权或延长生命周期优先传递const std::shared_ptrT[]或std::shared_ptrT[]避免不必要的引用计数操作。使用std::move当需要转移所有权时使用移动语义。考虑std::unique_ptr再次强调如果不需要共享就用unique_ptr。批量操作如果可能设计接口时考虑批量处理整个数组而不是逐个元素地传递shared_ptr这通常不现实但值得思考架构。5. 常见问题排查与调试技巧在实际项目中使用std::shared_ptrT[]可能会遇到一些典型问题。这里记录几个我踩过的坑和解决方法。5.1 问题运行时崩溃错误信息指向delete或free症状程序在析构shared_ptr管理的数组时崩溃错误可能是malloc: *** error for object 0x...: pointer being freed was not allocated或类似的delete/free错误。根本原因这是最经典的问题——删除器不匹配。你用new T[N]分配了数组但shared_ptr的删除器是默认的delete如果你用的是非数组特化的shared_ptrT。或者你用了自定义分配如malloc、aligned_alloc但删除器是delete[]。在C17后如果你正确使用了std::shared_ptrT[]并且用new T[N]初始化那么这个问题应该被杜绝。出现这个问题很可能是因为你混用了旧的代码模式或者错误地传递了指针。排查步骤检查shared_ptr的模板参数。确保管理数组的是std::shared_ptrT[]而不是std::shared_ptrT。检查构造函数的参数。确保传递给构造函数的指针确实是new T[N]的结果。如果使用了自定义删除器确保删除器中的释放函数与分配函数匹配malloc/free,new/delete,new[]/delete[],VirtualAlloc/VirtualFree等。5.2 问题内存泄漏引用计数未清零症状程序运行一段时间后内存持续增长。使用内存检测工具如Valgrind、AddressSanitizer或IDE内置的调试器发现数组内存没有被释放。根本原因循环引用或者某个地方意外地延长了shared_ptr的生命周期例如将其存储在一个全局容器中却忘了移除。排查步骤检查循环引用审视所有持有该数组shared_ptr的对象看是否存在“你中有我我中有你”的强引用关系。将其中非所有权的引用改为std::weak_ptr。检查生命周期使用调试器或在关键点打印shared_ptr的use_count()观察引用计数的变化。找到计数意外增加的地方。检查全局或长生命周期存储查看是否有将shared_ptr存入static变量、单例、长期存活的线程局部存储或全局容器中。5.3 问题访问越界导致数据损坏或崩溃症状程序行为不稳定某些数据莫名被修改或者随机崩溃崩溃点可能在数组操作之后很远的地方。根本原因通过operator[]或get()得到的指针进行越界访问破坏了堆内存结构。排查步骤使用调试工具AddressSanitizer (-fsanitizeaddress) 是检测越界访问的神器能在发生越界时立即报错并定位代码行。代码审查仔细检查所有使用下标index的地方确保其计算在[0, size)范围内。特别注意循环的终止条件、来自外部输入的下标值。防御性编程在调试版本中可以实现一个包装类在operator[]中加入断言assert(index size_)。或者如果条件允许直接使用std::vector它提供了可选的边界检查at()方法。5.4 问题多线程下的数据竞争症状程序在多线程环境下运行结果不确定有时正确有时错误。根本原因std::shared_ptr的引用计数操作是线程安全的但这并不意味着它管理的数组数据访问是线程安全的。多个线程同时修改同一个数组元素或者一个线程读另一个线程写而没有同步就会导致数据竞争。解决方案使用互斥锁std::mutex在访问数组数据前加锁。如果整个数组被频繁访问这可能成为性能瓶颈。使用读写锁std::shared_mutex如果读操作远多于写操作读写锁可以提高并发性。分区处理如果算法允许将数组分成若干段每个线程处理互不重叠的一段从根本上避免竞争。使用原子操作如果数组元素是简单的标量类型如int,bool并且操作是原子的如简单的赋值、增减可以考虑使用std::atomic_refC20或直接使用std::atomic类型的数组但std::shared_ptrstd::atomicT[]又是一种组合需要谨慎设计。一个简单的加锁示例class ThreadSafeArray { private: std::shared_ptrint[] data_; size_t size_; mutable std::shared_mutex mutex_; // 使用读写锁 public: ThreadSafeArray(size_t n) : data_(new int[n]), size_(n) {} void set(size_t index, int value) { std::unique_lock lock(mutex_); // 写锁 if (index size_) data_[index] value; } int get(size_t index) const { std::shared_lock lock(mutex_); // 读锁 return (index size_) ? data_[index] : 0; } // 批量读取只读可以共享锁效率高 int calculateSum() const { std::shared_lock lock(mutex_); return std::accumulate(data_.get(), data_.get() size_, 0); } };5.5 调试技巧可视化与日志在复杂的系统中跟踪shared_ptr的生命周期可能很困难。我常用的几个小技巧自定义删除器添加日志在自定义删除器中加入打印语句可以清晰地看到数组何时被释放。auto loggingDeleter [name std::string(MyArray)](int* p) { std::cout [ name ] Deleting array at static_castvoid*(p) std::endl; delete[] p; }; std::shared_ptrint[] trackedArray(new int[100], loggingDeleter);在关键点输出use_count()在怀疑有生命周期问题的地方打印shared_ptr的引用计数。但注意在多线程环境下use_count()通常用于调试其值可能瞬间变化且调用它本身可能带来性能开销尽管不大。std::cout Ref count: mySharedArray.use_count() std::endl;使用IDE的调试器监视现代IDE如Visual Studio、CLion、VS Code with C插件可以很好地显示shared_ptr的内部状态包括其指向的对象和引用计数。学会使用这些工具能极大提升调试效率。6. 设计模式与架构中的应用思考std::shared_ptrT[]不仅仅是一个语法改进它影响着我们对资源管理和模块边界的思考。以下是一些它在实际设计中的应用场景。6.1 工厂模式返回大型数据块假设有一个图像解码器工厂它根据文件内容返回解码后的像素数据。由于数据量可能很大且后续可能有多个模块如显示、保存、滤镜需要访问使用std::shared_ptruint8_t[]作为返回类型非常合适。class ImageDecoder { public: struct ImageData { std::shared_ptruint8_t[] pixels; int width; int height; int channels; }; virtual ImageData decode(const std::string filePath) 0; virtual ~ImageDecoder() default; }; class PNGDecoder : public ImageDecoder { public: ImageData decode(const std::string filePath) override { // ... 解析PNG文件头获取宽高通道数 int w 1024, h 768, c 4; size_t dataSize w * h * c; auto pixelArray std::shared_ptruint8_t[](new uint8_t[dataSize]); // ... 将解码后的数据填充到 pixelArray 中 return {std::move(pixelArray), w, h, c}; // 移动语义避免拷贝大数据 } }; // 使用方 auto decoder std::make_uniquePNGDecoder(); auto image decoder-decode(picture.png); // 现在image.pixels 可以被安全地传递给多个消费者直到最后一个引用消失内存才释放。6.2 观察者模式中的共享数据状态在观察者模式中主题Subject状态发生变化时需要通知所有观察者Observer。如果状态包含一个大型数组使用shared_ptr可以避免每个观察者都拷贝一份数据。class SensorDataSubject { private: std::shared_ptrfloat[] latestData_; // 最新的传感器数据数组 std::vectorstd::weak_ptrIObserver observers_; // 使用weak_ptr避免主题持有观察者导致泄漏 std::mutex mutex_; public: void updateData(std::shared_ptrfloat[] newData) { std::lock_guard lock(mutex_); latestData_ std::move(newData); notifyObservers(); } std::shared_ptrconst float[] getCurrentData() const { std::lock_guard lock(mutex_); return latestData_; // 返回一个副本shared_ptr增加引用计数 } void notifyObservers() { for (auto it observers_.begin(); it ! observers_.end(); ) { if (auto obs it-lock()) { obs-onDataUpdated(latestData_); it; } else { // 观察者对象已失效移除弱引用 it observers_.erase(it); } } } }; class DisplayObserver : public IObserver { public: void onDataUpdated(std::shared_ptrconst float[] data) override { // 直接使用共享的数据无需拷贝 visualize(data.get(), dataSize); } };6.3 池化资源管理器在游戏或高性能计算中经常需要池化大量同类型的资源如纹理、缓冲区。资源管理器可以持有这些资源的std::shared_ptrT[]当某个资源被“借出”时返回一个shared_ptr给使用者。当所有使用者都归还shared_ptr析构后该资源槽位标志为空闲可以复用。虽然资源管理器内部可能用std::vector或链表管理但对外接口使用shared_ptr可以自动处理生命周期。templatetypename T class ResourcePool { struct PoolItem { std::shared_ptrT[] resource; bool inUse false; }; std::vectorPoolItem pool_; std::mutex poolMutex_; public: std::shared_ptrT[] acquireResource(size_t size) { std::lock_guard lock(poolMutex_); // 1. 查找空闲且大小匹配的资源... (这里简化假设每次都创建新的) // 2. 如果找到标记inUse返回resource的shared_ptr。 // 3. 如果没找到创建新的。 auto newRes std::shared_ptrT[](new T[size]); pool_.push_back({newRes, true}); // 4. 关键我们返回一个带有自定义删除器的shared_ptr。 // 当这个“借出”的shared_ptr析构时并不真正delete数组而是通知池子该资源空闲了。 auto customDeleter [this, rawPtr newRes.get()](T*) { std::lock_guard innerLock(poolMutex_); for (auto item : pool_) { if (item.resource.get() rawPtr) { item.inUse false; break; } } // 注意这里不执行 delete[]内存由pool_中的原始shared_ptr管理。 }; return std::shared_ptrT[](newRes.get(), customDeleter); } };这个例子展示了shared_ptr自定义删除器的强大之处它可以用于实现复杂的生命周期管理策略而不仅仅是调用delete。7. 向C20/23的展望与替代方案虽然C17的std::shared_ptrT[]解决了基本问题但C20和即将到来的C23引入了更多现代工具可以与它结合使用或作为替代。std::span(C20)这是一个非拥有类型的视图可以表示一个连续的对象序列如数组、vector、array。如果你需要的是一个“只读”或“临时访问”的数组视图并且不想涉及所有权std::span是比shared_ptr更好的选择。它更轻量并且提供了边界检查的选项通过at()或在编译时检查。void processChunk(std::spanconst float data) { // 接受任何连续float序列的视图 for (auto val : data) { /* ... */ } } std::shared_ptrfloat[] bigArray(new float[1000]); processChunk({bigArray.get(), 100}); // 处理前100个元素 processChunk({bigArray.get() 100, 200}); // 处理接下来的200个元素std::make_sharedforT[N](C20)C20允许std::make_shared创建固定大小的数组例如auto p std::make_sharedint[10]();。但这仍然是编译时已知大小的数组T[N]而不是运行时动态大小的数组T[]。对于动态数组目前标准库仍然没有提供make_shared。std::vectorwith custom allocator如果你需要共享所有权但又想要vector的丰富接口迭代器、size()、push_back等可以考虑使用std::vectorT, CustomAllocator并让这个自定义分配器从某个共享的内存池中分配。然后用shared_ptr包装这个vector。这比直接使用shared_ptrT[]更重量级但功能也更完整。第三方库像Boost库中的boost::shared_array在C11之前就提供了类似功能。如今在成熟的C17/20项目中应优先使用标准库方案。在我个人的项目经验中std::shared_ptrT[]是一个“恰到好处”的工具。它填补了标准库智能指针家族的一个关键空白使得共享所有权的动态数组管理变得规范而安全。它的引入让我在设计和重构相关代码时少了许多“拧巴”的感觉多了一份“本该如此”的顺畅。当然工具虽好也需慎用。时刻问自己这里真的需要共享所有权吗如果答案是否定的那么std::unique_ptrT[]或std::vector可能是更简洁高效的选择。

本月热点