关于支持非平凡类型的通用可增长缓冲区/数组类型模板的实现问询
如何实现支持非平凡类型的通用可增长缓冲区/数组模板(不使用STL)
嘿,你现在的CGrowableBuffer实现确实有个核心问题——依赖memcpy、memmove、memset这类字节级操作,这些对int、char这种平凡类型没问题,但碰到带构造函数、析构函数,或者有自定义拷贝/移动逻辑的非平凡类型,直接字节拷贝就会出大问题:它完全不遵守C++的对象生命周期规则,既不会调用对象的构造/拷贝/移动构造函数,也不会在移动后析构旧对象,轻则导致对象状态混乱,重则引发内存泄漏、程序崩溃。
要解决这个问题,我们得把所有字节级的内存操作,替换成符合C++对象语义的构造、拷贝、移动、析构操作。下面是修改后的完整实现,我会逐个关键点给你解释:
#include <utility> // 用于std::move template<class T> class CGrowableBuffer { protected: T* m_pBuff; // 指向分配的原始内存(不是已构造的T数组) unsigned m_nSize; // 已分配内存可容纳的T对象总数 unsigned m_nCount; // 已构造完成的T对象数量 unsigned m_nGrowBy; // 每次扩容的步长(可改为倍数增长更高效) public: // 构造函数:初始化缓冲区,可选初始大小和扩容步长 CGrowableBuffer(unsigned initialSize = 0, unsigned growBy = 16) : m_pBuff(nullptr), m_nSize(0), m_nCount(0), m_nGrowBy(growBy) { if (initialSize > 0) { SetSize(initialSize); } } // 析构函数:必须先析构所有已构造的对象,再释放内存 ~CGrowableBuffer() { for (unsigned i = 0; i < m_nCount; ++i) { m_pBuff[i].~T(); // 显式调用析构函数 } operator delete(m_pBuff); // 释放原始内存,不调用析构 } // 禁用拷贝语义(如需支持,需手动实现深拷贝:逐个拷贝构造对象) CGrowableBuffer(const CGrowableBuffer&) = delete; CGrowableBuffer& operator=(const CGrowableBuffer&) = delete; // 实现移动语义:高效转移缓冲区资源 CGrowableBuffer(CGrowableBuffer&& other) noexcept : m_pBuff(other.m_pBuff), m_nSize(other.m_nSize), m_nCount(other.m_nCount), m_nGrowBy(other.m_nGrowBy) { // 清空原对象,避免双重释放 other.m_pBuff = nullptr; other.m_nSize = 0; other.m_nCount = 0; } CGrowableBuffer& operator=(CGrowableBuffer&& other) noexcept { if (this != &other) { // 先清理当前对象的资源 for (unsigned i = 0; i < m_nCount; ++i) { m_pBuff[i].~T(); } operator delete(m_pBuff); // 接管对方的资源 m_pBuff = other.m_pBuff; m_nSize = other.m_nSize; m_nCount = other.m_nCount; m_nGrowBy = other.m_nGrowBy; // 清空原对象 other.m_pBuff = nullptr; other.m_nSize = 0; other.m_nCount = 0; } return *this; } // 调整缓冲区的预分配大小 bool SetSize(unsigned newSize) { if (newSize <= m_nSize) { // 缩小缓冲区:析构多余的对象 for (unsigned i = newSize; i < m_nCount; ++i) { m_pBuff[i].~T(); } m_nCount = newSize; return true; } // 计算实际扩容后的尺寸(这里用固定步长,也可以改成按倍数增长,比如*2) unsigned actualNewSize = ((newSize + m_nGrowBy - 1) / m_nGrowBy) * m_nGrowBy; // 分配原始内存,不构造任何对象 T* newBuff = static_cast<T*>(operator new(actualNewSize * sizeof(T))); if (!newBuff) { return false; // 内存分配失败 } // 移动已构造的对象到新内存 for (unsigned i = 0; i < m_nCount; ++i) { new (newBuff + i) T(std::move(m_pBuff[i])); // 定位new:移动构造对象 m_pBuff[i].~T(); // 析构原位置的对象 } // 释放旧内存 operator delete(m_pBuff); m_pBuff = newBuff; m_nSize = actualNewSize; return true; } // 插入对象的核心函数 bool Insert(const T* p, unsigned index, unsigned count) { if (index > m_nCount || !SetSize(m_nCount + count)) { return false; // 索引越界或内存分配失败 } // 把index及之后的对象向后移动count个位置(从后往前移,避免覆盖) for (unsigned i = m_nCount; i > index; --i) { new (m_pBuff + i + count - 1) T(std::move(m_pBuff[i - 1])); m_pBuff[i - 1].~T(); } if (p == nullptr) { // 插入默认构造的对象 for (unsigned i = 0; i < count; ++i) { new (m_pBuff + index + i) T(); } } else { // 插入拷贝构造的对象 for (unsigned i = 0; i < count; ++i) { new (m_pBuff + index + i) T(p[i]); } } m_nCount += count; return true; } // 便捷方法:在末尾添加对象 bool PushBack(const T& value) { return Insert(&value, m_nCount, 1); } bool PushBack(T&& value) { if (!SetSize(m_nCount + 1)) { return false; } new (m_pBuff + m_nCount) T(std::move(value)); m_nCount++; return true; } // 元素访问 T& operator[](unsigned index) { return m_pBuff[index]; } const T& operator[](unsigned index) const { return m_pBuff[index]; } // 获取已构造对象数量和预分配大小 unsigned GetCount() const { return m_nCount; } unsigned GetSize() const { return m_nSize; } };
关键修改点说明:
- 内存分配方式:用
operator new分配原始内存,而不是new T[size]——后者会默认构造所有对象,我们只预分配空间,在需要的时候才构造对象,避免不必要的开销。 - 对象生命周期管理:所有对象的创建都用定位new(placement new),显式调用T的构造函数(默认、拷贝、移动);对象销毁必须显式调用
~T(),确保资源被正确清理。 - 替换字节操作:
- 原来的
memmove换成了从后往前的对象移动构造+原对象析构,避免覆盖未移动的对象 - 原来的
memcpy换成了拷贝构造函数调用 - 原来的
memset换成了默认构造函数调用
- 原来的
- 移动语义支持:实现了移动构造和移动赋值,避免大对象拷贝的性能开销,同时确保资源转移的安全性。
- 拷贝语义禁用:默认禁用了拷贝构造和赋值,因为深拷贝需要逐个构造对象,如果你需要支持,可以手动实现(遍历所有对象,拷贝构造到新缓冲区)。
额外优化建议:
- 扩容策略可以改成按倍数增长(比如每次扩容到原来的2倍),比固定步长扩容的平均性能更好,减少内存分配的次数。
- 如果你的场景需要异常安全,可以在SetSize和Insert函数中添加异常处理逻辑(比如内存分配失败时回滚操作)。
- 确保T的移动构造函数是
noexcept的,这样在移动对象时不会抛出异常,提升稳定性。
这样修改后,你的CGrowableBuffer就可以安全处理任何类型了——不管是内置类型、平凡类型,还是带复杂逻辑的非平凡类型,都能严格遵守C++的对象生命周期规则,不会再出现内存泄漏或对象状态异常的问题。
内容来源于stack exchange
相关产品推荐
相关产品推荐

