C++如何实现动态数组?(类似std::vector的简易版)

2次阅读

手动管理内存实现动态数组需用new/delete[]配合裸指针,维护m_data、m_size、m_capacity三个成员,扩容时用2倍策略、移动构造、显式析构,注意对齐与noexcept。

C++如何实现动态数组?(类似std::vector的简易版)

怎么手动管理内存实现动态数组

直接用 newdelete[] 配合裸指针是最底层的方式,但必须自己管容量、大小、扩容逻辑。不是不能写,而是稍有疏忽就会内存泄漏或越界——比如忘了在拷贝构造里深拷贝,或者 resize 时没正确释放旧内存。

实操建议:

  • 用两个成员变量m_dataT* 指针)和 m_size(当前元素个数),再加一个 m_capacity(已分配空间大小)
  • 首次分配建议设容量为 1 或 2,别从 0 开始;否则每次 push_back 都要 realloc,性能极差
  • push_back 前必须检查 m_size == m_capacity,触发扩容:通常用 m_capacity * 2,别用 +1(摊还成本爆炸)
  • 扩容时先 new T[new_capacity],逐个调用 new (&dest[i]) T(std::move(src[i])) 移动构造(c++11+),再显式调用旧对象析构函数,最后 delete[] m_data

为什么不能直接用 std::vector 的部分源码来简化

因为 std::vector 依赖 std::allocatorstd::uninitialized_copy 等设施,还做了异常安全、SFINAE、迭代器类别等大量适配。抄几行 data_size_ 看似简单,但一旦涉及异常(比如构造函数抛出)、移动语义或自定义类型,裸指针方案立刻崩。

常见错误现象:

立即学习C++免费学习笔记(深入)”;

  • 插入含资源的对象(如 std::String)时,只做位拷贝(memcpy),导致多个对象指向同一内存,析构时报 double-free
  • 没声明移动构造/移动赋值,对象在 vector 扩容时被反复拷贝,性能陡降且可能失败
  • 没处理 noexcept,导致 std::vector 在某些优化路径下拒绝移动而改用拷贝

简易版该暴露哪些接口才够用

够用 ≠ 全,重点是让使用者不掉坑。比如 operator[] 不做边界检查(和 std::vector::operator[] 一致),但必须提供 at() 做带异常的访问——否则调试时根本不知道哪越界了。

实操建议:

  • 必有:构造函数(空、带初始容量)、push_back(const T&)push_back(T&&)size()capacity()empty()operator[]at()pop_back()、析构函数
  • 可选但推荐:reserve()(预分配,避免多次扩容)、clear()(只清 size,不释放内存)、移动构造/赋值(用 noexcept 标记)
  • 别急着加 insert / erase —— 它们需要迭代器支持,一写就牵扯到 traits、tag dispatch、std::distance,复杂度指数上升

容易被忽略的细节:对齐与 noexcept

如果存的是 alignas(16) 类型(比如 SIMD 结构体),仅用 new char[capacity * sizeof(T)] 分配内存会导致地址不对齐,触发硬件异常。而 std::allocator 内部会调用 std::align 修正。

另一个隐形炸弹是 noexcept:哪怕你所有操作都不抛异常,只要没显式写 noexcept,编译器就默认函数可能抛,导致 std::vector 在 move-aware 容器中退化成拷贝。

所以:

  • 手动分配内存时,用 operator new(size, std::align_val_t{alignof(T)}) 替代裸 new char[]
  • 所有不抛异常的函数(构造、移动、push_back 等)末尾加上 noexcept
  • 移动操作里,确保 T 的移动构造/赋值也是 noexcept,否则你的移动函数会被视为可能抛异常

这些点不写代码时看不出问题,一跑 ASan 或上生产环境就卡住半天——尤其对齐问题,在 x86 可能只是慢,在 ARM 直接 crash。

text=ZqhQzanResources