【C++进阶】从零实现一个支持动态扩容的 Vector 容器(含移动语义与内存管理详解)

本文将带你一步步实现一个简化版的 std::vector,从最基础的动态扩容开始,到支持移动语义、原地构造(EmplaceBack)以及自定义内存管理(::operator new/delete)。

通过阅读本文,你将深入理解 C++ 中对象构造、析构、内存分配的底层机制。


一、Vector 数组简介

相较于 Array(静态数组),Vector 的最大特点就是动态扩容

我们无需在创建时指定固定容量,可以在运行时以 O(1) 的摊销复杂度不断地向尾部插入元素,同时仍然保持随机访问的 O(1) 时间复杂度。


二、动态扩容策略

1. 连续存储与随机访问

要在 O(1) 时间内访问任意元素,必须使用连续存储空间 。这意味着 Vector 内部仍然是通过普通数组(T*)实现的。

2. 扩容与均摊复杂度

当数组容量用尽时,我们需要重新分配更大的内存,并将原有数据拷贝过去。这一操作本身复杂度是 O(n)。

为了使多次插入操作的平均复杂度仍然保持 O(1),扩容时通常采用倍增策略 (如 2 倍扩容)。

这样,每个元素在整个生命周期内平均只会被移动常数次。

实际上,MSVC 使用 1.5 倍扩容,GCC 使用 2 倍扩容。本文采用后者。


三、基础版本实现

我们先实现一个最小可用的版本,包含以下 API:

  • PushBack(const T&)

  • operator[]

  • Size()

  • 内部的 ReAlloc() 实现动态扩容

cpp 复制代码
template <typename T>
class Vector {
public:
    Vector() { ReAlloc(2); }

    void PushBack(const T& value) {
        if (m_Size >= m_Capacity)
            ReAlloc(m_Size + m_Size);
        m_Data[m_Size++] = value;
    }

    T& operator[](size_t index) { return m_Data[index]; }
    const T& operator[](size_t index) const { return m_Data[index]; }

    size_t Size() const { return m_Size; }

private:
    void ReAlloc(size_t newCapacity) {
        T* newBlock = new T[newCapacity];

        if (newCapacity < m_Size)
            m_Size = newCapacity;

        for (size_t i = 0; i < m_Size; ++i)
            newBlock[i] = m_Data[i];

        delete[] m_Data;
        m_Data = newBlock;
        m_Capacity = newCapacity;
    }

private:
    T* m_Data = nullptr;
    size_t m_Size = 0;
    size_t m_Capacity = 0;
};

四、加入移动语义(Move 版本)

我们可以用以下类 Vector3 来观察对象复制与移动:

cpp 复制代码
class Vector3 {
public:
    Vector3() {}
    Vector3(float scalar)
        : x(scalar), y(scalar), z(scalar) {}
    Vector3(float x, float y, float z)
        : x(x), y(y), z(z) {}
 
    Vector3(const Vector3& other)
        : x(other.x), y(other.y), z(other.z) {
        std::cout << "Copy" << std::endl;
    }
    Vector3(const Vector3&& other)
        : x(other.x), y(other.y), z(other.z) {
        std::cout << "Move" << std::endl;
    }
    ~Vector3() {
        std::cout << "Destroy" << std::endl;
    }
 
    Vector3& operator=(const Vector3& other) {
        std::cout << "Copy" << std::endl;
        x = other.x;
        y = other.y;
        z = other.z;
        return *this;
    }
    Vector3& operator=(Vector3&& other) {
        std::cout << "Move" << std::endl;
        x = other.x;
        y = other.y;
        z = other.z;
        return *this;
    }
    friend std::ostream& operator<<(std::ostream&, const Vector3&);
private:
    float x = 0.0f, y = 0.0f, z = 0.0f;
};
 
std::ostream& operator<<(std::ostream& os, const Vector3& vec) {
    os << vec.x << ", " << vec.y << ", " << vec.z;
    return os;
}
 

测试代码:

cpp 复制代码
int main() {
    Vector<Vector3> vec;
    vec.PushBack(Vector3());
    vec.PushBack(Vector3(1.0f));
    vec.PushBack(Vector3(1.0f, 2.0f, 3.0f));
    PrintVector(vec);
 
    return 0;
}

输出(未优化前):

cpp 复制代码
Copy
Destroy
Copy
Destroy
Copy
Copy
Destroy
Destroy
Copy
Destroy
0, 0, 0
1, 1, 1
1, 2, 3
---------------------------

可以看到,每次插入都进行了多余的拷贝。


改进:支持右值引用

我们在 Vector 中新增:

cpp 复制代码
void PushBack(T&& value) {
    if (m_Size >= m_Capacity)
        ReAlloc(m_Size + m_Size);
    m_Data[m_Size++] = std::move(value);
}

同时在 ReAlloc 中也使用 std::move

cpp 复制代码
for (size_t i = 0; i < m_Size; ++i)
    newBlock[i] = std::move(m_Data[i]);

输出结果:

cpp 复制代码
Move
Destroy
Move
Destroy
Move
Move
Destroy
Destroy
Move
Destroy

现在已经没有 Copy,全是 Move!


五、原地构造(EmplaceBack + Placement New)

即便如此,每次 PushBack 仍需先在外部构造一个临时对象再移动进去。

我们希望能直接在目标内存地址上构造对象 ,这就是 Placement new 的用法。

实现 EmplaceBack

cpp 复制代码
template<typename... Args>
T& EmplaceBack(Args&&... args) {
    if (m_Size >= m_Capacity)
        ReAlloc(m_Size + m_Size);

    new (&m_Data[m_Size]) T(std::forward<Args>(args)...); // 原地构造
    return m_Data[m_Size++];
}

测试:

cpp 复制代码
int main() {
    Vector<Vector3> vec;
    vec.EmplaceBack();
    vec.EmplaceBack(1.0f);
    vec.EmplaceBack(1.0f, 2.0f, 3.0f);
    return 0;
}

输出:

cpp 复制代码
Move
Move
Destroy
Destroy

效率大幅提升!


六、添加 PopBack 与析构函数

cpp 复制代码
void PopBack() {
    if (m_Size > 0) {
        --m_Size;
        m_Data[m_Size].~T();
    }
}

~Vector() { delete[] m_Data; }

但此实现有风险:如果 T 的析构函数中释放资源(如 delete[]),可能导致双重析构问题。

因此我们还需分离"分配内存"和"对象构造"两个步骤。


七、正确的内存管理:::operator new / delete

在 C++ 中:

  • new 做两件事:分配内存 + 调用构造函数。

  • delete 做两件事:调用析构函数 + 释放内存。

我们希望自行控制这两步,于是使用 ::operator new::operator delete

完整版本实现

cpp 复制代码
template<typename T>
class Vector {
public:
    Vector() { ReAlloc(2); }

    ~Vector() {
        Clear();
        ::operator delete(m_Data, m_Capacity * sizeof(T));
    }

    void PushBack(T&& value) {
        if (m_Size >= m_Capacity)
            ReAlloc(m_Size + m_Size);
        new (&m_Data[m_Size]) T(std::move(value));
        ++m_Size;
    }

    template<typename... Args>
    T& EmplaceBack(Args&&... args) {
        if (m_Size >= m_Capacity)
            ReAlloc(m_Size + m_Size);
        new (&m_Data[m_Size]) T(std::forward<Args>(args)...);
        return m_Data[m_Size++];
    }

    void PopBack() {
        if (m_Size > 0)
            m_Data[--m_Size].~T();
    }

    void Clear() {
        for (size_t i = 0; i < m_Size; ++i)
            m_Data[i].~T();
        m_Size = 0;
    }

private:
    void ReAlloc(size_t newCapacity) {
        T* newBlock = (T*)::operator new(newCapacity * sizeof(T));

        if (newCapacity < m_Size)
            m_Size = newCapacity;

        for (size_t i = 0; i < m_Size; ++i)
            new (&newBlock[i]) T(std::move(m_Data[i]));

        Clear();
        ::operator delete(m_Data, m_Capacity * sizeof(T));
        m_Data = newBlock;
        m_Capacity = newCapacity;
    }

private:
    T* m_Data = nullptr;
    size_t m_Size = 0;
    size_t m_Capacity = 0;
};

编译需使用 -std=c++14 或以上(因为 C++14 起支持带大小参数的 ::operator delete)。

输出验证

cpp 复制代码
Move
Move
Destroy
Destroy
1, 2, 3
---------------------------
Destroy
---------------------------
hello

程序正常退出,无内存泄漏,无双重析构!


八、总结

阶段 功能 关键技术
基础版 动态扩容 普通 new[]
Move版 高效移动 移动语义 std::move
Emplace版 原地构造 Placement new
最终版 完整内存控制 ::operator new/delete + 手动析构

至此,我们完成了一个功能齐全的简易版 std::vector


九、思考与拓展

  • 可实现迭代器支持,兼容 for(auto& e : vec)

  • 可增加容量控制函数 Reserve()Resize()

  • 可引入异常安全机制(RAII + strong exception guarantee)。

  • 深入理解 std::allocator 与 STL 的内存分配策略。

相关推荐
努力努力再努力wz2 小时前
【C++进阶系列】:万字详解特殊类以及设计模式
java·linux·运维·开发语言·数据结构·c++·设计模式
bkspiderx2 小时前
C++设计模式之行为型模式:策略模式(Strategy)
c++·设计模式·策略模式
Mcband2 小时前
Apache Commons IO:文件流处理利器,让Java IO操作更简单
java·开发语言·apache
泽虞3 小时前
《Qt应用开发》笔记p4
linux·开发语言·数据库·c++·笔记·qt·算法
ajassi20003 小时前
开源 C++ QT QML 开发(十三)多线程
c++·qt·开源
mahuifa3 小时前
C++(Qt)软件调试---binutils工具集详解(39)
linux·c++·软件调试·binutils
Qt程序员3 小时前
Qt C++ 教程:无边框窗体 + 自定义标题栏 + 圆角 + 拖拽拉升 + 阴影
c++·qt·qt编程·qt开发·qt教程·qt界面开发·qt界面
泽虞3 小时前
《Qt应用开发》笔记p5
linux·开发语言·c++·笔记·qt·算法
qq_433554543 小时前
C++ 完全背包时间优化、完全背包空间优化
开发语言·c++·动态规划