1. 项目概述:为什么我们要自己实现一个vector?

如果你写过C++,那你一定用过 std::vector 。它是标准库中最常用、最核心的容器,没有之一。无论是刷算法题、做项目,还是日常开发, vector 几乎无处不在。它就像一个“智能的动态数组”,帮你自动管理内存,让你可以方便地添加、删除、访问元素,而不用操心底层的内存分配和释放。

但用多了之后,你有没有想过,这个看似简单的“动态数组”内部到底是怎么工作的?为什么 push_back 操作在大多数情况下都很快(常数时间复杂度)?为什么说 vector 的元素在内存中是连续存储的? capacity() size() 到底有什么区别?当内存不够时,它是如何“偷偷”扩容的?

这些问题,仅仅通过调用 std::vector 的接口是无法得到深刻理解的。这就好比你会开车,但不一定懂发动机的原理。而“自己动手实现一个简易版的 vector ”,正是深入理解这些核心机制的最佳途径。这不是为了替代标准库(标准库的实现经过千锤百炼,远非我们一个练习所能及),而是为了**“知其然,更知其所以然”**。

通过这个“番外篇”项目,我们将从零开始,构建一个名为 MyVector 的模板类。我们将亲手处理内存分配、元素拷贝、迭代器失效、异常安全这些底层且关键的细节。这个过程会让你对C++的内存管理、模板编程、RAII(资源获取即初始化)原则有脱胎换骨的认识。当你再使用 std::vector 时,你看到的将不再是一个黑盒,而是一个由指针、容量、大小等构件清晰组成的精密机器。这对于应对技术面试中关于 vector 底层原理的“八股文”问题,或是进行高性能C++开发,都有着不可估量的价值。

2. 核心设计思路与架构拆解

在动手写代码之前,我们必须先想清楚 MyVector 需要哪些核心部件,以及它们之间如何协作。一个最简单的动态数组容器,其核心状态可以由三个指针(或与之等效的指针算术)来定义。

2.1 三大核心指针:理解容器的骨架

这是理解 vector 乃至许多连续存储容器的钥匙。我们通常用三个指针来标记内存块的状态:

  1. _start (或 begin ) : 指向已分配内存块的起始位置。这是整个数组的“头”。
  2. _finish (或 end ) : 指向当前已构造的最后一个元素的下一个位置。 _finish - _start 就等于 size() ,即容器中当前元素的数量。
  3. _end_of_storage (或 end_cap ) : 指向已分配内存块的末尾的下一个位置。 _end_of_storage - _start 就等于 capacity() ,即当前分配的总容量。

为什么是“下一个位置”?这是为了与C++标准库的“尾后迭代器”概念保持一致。 end() 迭代器指向的就是最后一个有效元素的下一个位置,这样表示空范围( begin() == end() )和遍历操作都非常方便。

这三个指针的关系定义了容器的所有状态:

  • 空容器 : _start == _finish == _end_of_storage (也可能都为 nullptr )。
  • 满容器 : _finish == _end_of_storage 。此时再添加元素就需要扩容。
  • 有空间 : _finish < _end_of_storage 。可以直接在尾部构造新元素。

这种设计将容量( capacity )、大小( size )和内存块清晰地分离开,是后续所有操作(增删改查、扩容)的基础。

2.2 内存管理策略:扩容的奥秘

vector 最精妙的设计之一就是其 摊销常数时间 push_back 操作。它并不是每次添加元素都重新分配内存,那样成本太高( O(n) )。它的策略是 预分配

当我们创建一个 vector 或调用 reserve(n) 时,它会一次性分配一块能容纳至少 n 个元素的内存。之后,在容量耗尽之前,所有的 push_back 操作都只是在已分配的内存上构造对象,速度极快。

_finish == _end_of_storage ,即容量已满,需要添加新元素时,就会触发扩容。常见的扩容策略是 倍增(Geometric Growth) ,例如每次扩容为当前容量的2倍(GCC、Clang的标准库实现)或1.5倍(MSVC的实现)。为什么是2倍?这是一个在时间(减少扩容次数)和空间(避免过多浪费)之间的经典权衡。

  • 倍增(2倍) : 扩容次数为对数级。假设我们从1开始,插入n个元素,最多需要约 log₂(n) 次扩容。每次扩容需要将旧元素拷贝到新内存,虽然单次成本是 O(n) ,但经过数学证明,n次插入的总时间复杂度摊销下来是 O(n) ,即单次 push_back 的摊销时间复杂度是 O(1) 。缺点是空间浪费可能略多(最多浪费接近当前容量的一倍)。
  • 1.5倍增长 : 同样能实现摊销 O(1) ,并且空间利用率更高一些,是现代许多实现的选择。

在我们的 MyVector 实现中,为了简单和经典性,我们将采用 2倍扩容 策略。

2.3 迭代器设计:让容器“可遍历”

迭代器是STL算法的基石。对于 vector 这种连续存储的容器,它的迭代器可以简单地设计为原生指针的包装(或者直接就是原生指针)。因为元素在内存中是连续的, ++ 操作就是指针前进一个元素大小, * 操作就是解引用。

我们的 MyVector 需要提供以下几种迭代器类型:

  • iterator : 普通迭代器,指向 T
  • const_iterator : 常量迭代器,指向 const T
  • reverse_iterator const_reverse_iterator : 反向迭代器,可以通过适配器 std::reverse_iterator 从普通迭代器生成。

此外,还需要实现 begin() , end() , cbegin() , cend() , rbegin() , rend() 等成员函数来获取这些迭代器。这使得我们的容器可以无缝接入C++11的范围for循环以及 <algorithm> 中的各种算法,如 std::sort , std::find 等。

2.4 异常安全与RAII

这是实现一个健壮容器时必须考虑的问题。我们的代码必须保证在发生异常(比如元素类型的构造函数抛出异常)时,不会发生内存泄漏,并且容器保持在一个有效状态。

RAII原则 是我们的第一道防线。我们将资源(动态内存)的生命周期与对象的生命周期绑定。在 MyVector 的构造函数中分配内存,在析构函数中释放内存。这样,无论控制流如何离开作用域,内存都能被正确释放。

对于可能抛出异常的操作,如 push_back (可能涉及内存分配和元素构造),我们需要考虑 强异常保证 :要么操作成功,要么容器状态完全回滚到操作之前。实现这一点通常需要“先分配、后构造、再交换”的技巧。例如,在扩容时,我们先在新的内存块上成功构造所有必要元素(包括新元素),然后再安全地释放旧内存、交换指针。如果在新内存上构造元素时发生异常,旧内存及其数据依然完好无损。

3. MyVector类的具体实现

理论铺垫完毕,现在让我们进入实战环节,一步步实现 MyVector 。我们将遵循从简到繁的顺序,先搭建骨架,再填充血肉。

3.1 基础框架与成员变量

首先,我们定义类模板和核心成员变量。

#include <cstddef> // for std::size_t, std::ptrdiff_t
#include <algorithm> // for std::swap, std::copy, etc.
#include <initializer_list>
#include <stdexcept> // for std::out_of_range
#include <iterator>  // for std::reverse_iterator

namespace my {
    template <typename T>
    class vector {
    public:
        // 类型别名 (Member types)
        using value_type = T;
        using size_type = std::size_t;
        using difference_type = std::ptrdiff_t;
        using reference = value_type&;
        using const_reference = const value_type&;
        using pointer = value_type*;
        using const_pointer = const value_type*;
        using iterator = pointer; // 对于vector,迭代器就是原生指针
        using const_iterator = const_pointer;
        using reverse_iterator = std::reverse_iterator<iterator>;
        using const_reverse_iterator = std::reverse_iterator<const_iterator>;

    private:
        pointer _start = nullptr;        // 指向内存块开始
        pointer _finish = nullptr;       // 指向最后一个有效元素的下一个位置
        pointer _end_of_storage = nullptr; // 指向内存块末尾的下一个位置

        // 内部工具函数
        void _reallocate(size_type new_cap);
        void _range_check(size_type n) const;
        // ... 其他辅助函数

    public:
        // 构造函数、析构函数、成员函数将在下面实现
    };
}

注意 :我们将迭代器直接定义为指针类型。这在 vector 中是可行且高效的,因为它满足随机访问迭代器的所有要求(可递增、递减、加减整数、求距离、解引用等)。更复杂的容器(如 list )则需要单独设计迭代器类。

3.2 构造、拷贝与析构(Rule of Three/Five)

这是管理资源类的核心。我们需要实现构造函数(默认构造、带数量构造、范围构造、初始化列表构造)、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符(C++11)以及析构函数。这被称为“三/五法则”。

public:
    // 1. 默认构造函数
    vector() noexcept = default;

    // 2. 构造包含n个默认值元素的vector
    explicit vector(size_type n) {
        _start = _allocate(n); // 辅助函数,分配未初始化的内存
        _finish = _start + n;
        _end_of_storage = _finish;
        _construct_range(_start, _finish); // 辅助函数,在内存上构造n个默认对象
    }

    // 3. 构造包含n个value副本的vector
    vector(size_type n, const T& value) {
        _start = _allocate(n);
        _finish = _start + n;
        _end_of_storage = _finish;
        _construct_range(_start, _finish, value); // 构造n个value的副本
    }

    // 4. 范围构造函数 [first, last)
    template <typename InputIt>
    vector(InputIt first, InputIt last) {
        // 计算距离,对于InputIterator可能需要遍历一次
        size_type count = std::distance(first, last);
        _start = _allocate(count);
        _finish = _start + count;
        _end_of_storage = _finish;
        _construct_range(_start, _finish, first); // 从迭代器范围拷贝构造
    }

    // 5. 初始化列表构造函数 (C++11)
    vector(std::initializer_list<T> init) : vector(init.begin(), init.end()) {}

    // 6. 拷贝构造函数 (深拷贝)
    vector(const vector& other) {
        size_type count = other.size();
        _start = _allocate(count);
        _finish = _start + count;
        _end_of_storage = _finish;
        _construct_range(_start, _finish, other._start); // 拷贝other的元素
    }

    // 7. 移动构造函数 (C++11)
    vector(vector&& other) noexcept
        : _start(other._start), _finish(other._finish), _end_of_storage(other._end_of_storage) {
        // 窃取资源,并将other置于有效但空的状态
        other._start = other._finish = other._end_of_storage = nullptr;
    }

    // 8. 析构函数
    ~vector() {
        _destroy_range(_start, _finish); // 析构所有已构造的元素
        _deallocate(_start); // 释放内存
    }

    // 9. 拷贝赋值运算符 (copy-and-swap idiom)
    vector& operator=(vector other) { // 注意:按值传参!
        swap(other); // 交换当前对象和临时对象other的内容
        return *this; // other离开作用域,自动析构旧资源
    }

    // 交换函数
    void swap(vector& other) noexcept {
        std::swap(_start, other._start);
        std::swap(_finish, other._finish);
        std::swap(_end_of_storage, other._end_of_storage);
    }

关键点解析:

  1. 拷贝赋值运算符的“拷贝并交换”惯用法 :这是实现异常安全赋值操作的优雅方式。 operator= 的参数是 vector other ,这会导致一次拷贝构造(如果传左值)或移动构造(如果传右值)。然后我们交换 *this other 的内容。函数返回时,临时对象 other 被析构,从而释放了 *this 原来的资源。这个写法自动提供了强异常保证,并且同时处理了拷贝赋值和移动赋值(通过参数类型的重载决议)。
  2. 移动构造函数的 noexcept :标记为 noexcept 非常重要,这允许标准库容器(如 std::vector<vector<int>> )在扩容等操作时使用更高效的移动而非拷贝。
  3. 辅助函数 _allocate , _deallocate , _construct_range , _destroy_range 这些函数封装了底层的内存分配和对象构造/析构,通常使用 std::allocator_traits 或直接使用 new/delete placement new 来实现,使主逻辑更清晰。为了简化,下文示例中可能会直接使用 new[] delete[] ,但在生产级代码中应使用分配器。

3.3 容量相关操作

容量操作关注的是内存,而非元素。

public:
    size_type size() const noexcept { return _finish - _start; }
    size_type capacity() const noexcept { return _end_of_storage - _start; }
    bool empty() const noexcept { return _start == _finish; }

    void reserve(size_type new_cap) {
        if (new_cap > capacity()) {
            _reallocate(new_cap);
        }
    }

    void shrink_to_fit() {
        if (size() < capacity()) {
            _reallocate(size()); // 重新分配一块刚好容纳当前元素的内存
        }
    }

private:
    void _reallocate(size_type new_cap) {
        if (new_cap == 0) {
            // 处理请求容量为0的情况,通常释放所有内存
            _destroy_range(_start, _finish);
            _deallocate(_start);
            _start = _finish = _end_of_storage = nullptr;
            return;
        }
        // 1. 分配新内存
        pointer new_start = _allocate(new_cap);
        // 2. 将旧元素移动或拷贝到新内存 (优先移动)
        pointer new_finish = new_start;
        try {
            for (pointer it = _start; it != _finish; ++it) {
                _construct_at(new_finish, std::move_if_noexcept(*it)); // 构造新元素
                ++new_finish;
            }
        } catch (...) {
            // 如果构造过程中发生异常,析构已构造的新元素并释放新内存
            _destroy_range(new_start, new_finish);
            _deallocate(new_start);
            throw; // 重新抛出异常
        }
        // 3. 析构并释放旧内存
        _destroy_range(_start, _finish);
        _deallocate(_start);
        // 4. 更新指针
        _start = new_start;
        _finish = new_finish;
        _end_of_storage = new_start + new_cap;
    }

关键点解析:

  1. reserve shrink_to_fit reserve 用于预分配内存,避免后续 push_back 时频繁扩容。 shrink_to_fit 是一个非强制性的请求,希望释放未使用的内存。注意,标准不保证调用后 capacity() == size() ,但我们的实现可以做到。
  2. _reallocate 中的异常安全 :这是实现强异常保证的关键。我们遵循“先分配新资源,再操作,最后交换”的模式。在将旧元素转移到新内存时,我们使用 std::move_if_noexcept ,这是一个C++11的设施,它会在元素类型的移动构造函数被声明为 noexcept 时选择移动,否则选择拷贝,以确保在转移过程中如果发生异常,旧数据不会丢失。如果转移过程抛出异常,我们会清理新分配的内存,然后重新抛出异常,旧内存和数据保持不变。

3.4 元素访问

提供安全和不安全的元素访问方式。

public:
    reference operator[](size_type n) {
        return _start[n]; // 不检查边界,性能高
    }
    const_reference operator[](size_type n) const {
        return _start[n];
    }

    reference at(size_type n) {
        _range_check(n); // 检查边界,越界则抛出std::out_of_range
        return (*this)[n];
    }
    const_reference at(size_type n) const {
        _range_check(n);
        return (*this)[n];
    }

    reference front() { return *_start; }
    const_reference front() const { return *_start; }
    reference back() { return *(_finish - 1); }
    const_reference back() const { return *(_finish - 1); }

    pointer data() noexcept { return _start; }
    const_pointer data() const noexcept { return _start; }

private:
    void _range_check(size_type n) const {
        if (n >= size()) {
            throw std::out_of_range("vector::_range_check: n >= size()");
        }
    }

3.5 修改器:增删改

这是 vector 逻辑最复杂的部分,涉及迭代器失效和异常安全。

3.5.1 push_back 与 emplace_back
public:
    void push_back(const T& value) {
        if (_finish == _end_of_storage) { // 容量已满,需要扩容
            size_type new_cap = capacity() == 0 ? 1 : capacity() * 2; // 2倍扩容策略
            reserve(new_cap);
        }
        _construct_at(_finish, value); // 在_finish位置构造value的副本
        ++_finish;
    }

    void push_back(T&& value) { // 移动版本的push_back
        if (_finish == _end_of_storage) {
            size_type new_cap = capacity() == 0 ? 1 : capacity() * 2;
            reserve(new_cap);
        }
        _construct_at(_finish, std::move(value)); // 移动构造
        ++_finish;
    }

    template <typename... Args>
    reference emplace_back(Args&&... args) { // C++11 变参模板,完美转发
        if (_finish == _end_of_storage) {
            size_type new_cap = capacity() == 0 ? 1 : capacity() * 2;
            reserve(new_cap);
        }
        _construct_at(_finish, std::forward<Args>(args)...); // 原地构造
        ++_finish;
        return *(_finish - 1); // 返回新元素的引用
    }

关键点解析:

  1. 重载决议 :我们提供了 push_back 的常量左值引用和右值引用版本。当传入临时对象(右值)时,编译器会选择移动版本,避免不必要的拷贝。
  2. emplace_back 的优势 :它使用变参模板和完美转发,可以直接在容器尾部构造元素,无需创建临时对象。例如 vec.emplace_back(10, 'a') 可以直接调用 T(10, 'a') 构造函数,而 push_back(T(10, 'a')) 则需要先构造一个临时 T 对象,再移动(或拷贝)进去。 emplace_back 通常更高效。
  3. 扩容时机 :在插入前检查容量。扩容操作 reserve 可能会使所有迭代器、指针、引用失效。
3.5.2 insert 与 erase

insert erase 是更通用的操作,可以在任意位置插入或删除元素,但会导致插入点/删除点之后的所有迭代器失效。

public:
    iterator insert(const_iterator pos, const T& value) {
        // 计算插入位置的索引
        size_type index = pos - cbegin();
        if (_finish == _end_of_storage) { // 需要扩容
            size_type new_cap = capacity() == 0 ? 1 : capacity() * 2;
            reserve(new_cap);
        }
        // 插入位置之后的所有元素向后移动一位
        pointer p = _start + index;
        _move_backward(p, _finish, _finish + 1); // 从后往前移动,避免覆盖
        _construct_at(p, value); // 在插入点构造新元素
        ++_finish;
        return iterator(p); // 返回指向新元素的迭代器
    }

    iterator erase(const_iterator pos) {
        if (pos == cend()) return end();
        pointer p = const_cast<pointer>(pos); // 注意:const_cast需要谨慎,我们知道pos来自我们的容器
        // 将pos+1之后的元素向前移动一位,覆盖pos位置的元素
        _move(p + 1, _finish, p);
        --_finish;
        _destroy_at(_finish); // 析构最后一个冗余的元素(原_finish-1位置的元素已被前移)
        return iterator(p); // 返回指向被删除元素之后位置的迭代器
    }

    iterator erase(const_iterator first, const_iterator last) {
        if (first == last) return iterator(const_cast<pointer>(first));
        pointer p_first = const_cast<pointer>(first);
        pointer p_last = const_cast<pointer>(last);
        size_type n = p_last - p_first;
        // 将[last, _finish)区间的元素移动到first处
        _move(p_last, _finish, p_first);
        pointer new_finish = _finish - n;
        // 析构尾部冗余的元素
        _destroy_range(new_finish, _finish);
        _finish = new_finish;
        return iterator(p_first);
    }

    void pop_back() {
        if (!empty()) {
            --_finish;
            _destroy_at(_finish);
        }
    }

    void clear() noexcept {
        _destroy_range(_start, _finish);
        _finish = _start;
    }

关键点解析:

  1. insert 的迭代器失效 insert 可能导致扩容,一旦扩容,所有迭代器都失效。即使不扩容,插入点及其之后的迭代器也失效。我们的实现返回了一个新的迭代器指向新插入的元素。
  2. 元素移动 :我们使用了 _move _move_backward 这样的辅助函数。在C++11之后,应该使用 std::move std::move_backward 算法配合移动语义,以提升性能(对于可移动的类型)。对于平凡类型(POD),移动就是拷贝。
  3. erase 的返回值 :标准规定 erase 返回指向被删除元素之后位置的迭代器。这非常有用,可以配合循环安全地删除元素,例如 it = vec.erase(it);
  4. const_cast 的使用 :为了修改元素,我们需要将 const_iterator 转换回普通的 iterator 。这在实现上是可行的,因为 const_iterator iterator 本质都是指针,并且我们知道这个 const_iterator 来自我们自己的容器。但在通用代码中需要更复杂的类型萃取。

3.6 迭代器与相关操作

提供完整的迭代器支持,使其行为与标准库一致。

public:
    // 迭代器
    iterator begin() noexcept { return _start; }
    const_iterator begin() const noexcept { return _start; }
    const_iterator cbegin() const noexcept { return _start; }

    iterator end() noexcept { return _finish; }
    const_iterator end() const noexcept { return _finish; }
    const_iterator cend() const noexcept { return _finish; }

    // 反向迭代器 (通过适配器生成)
    reverse_iterator rbegin() noexcept { return reverse_iterator(end()); }
    const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); }
    const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(cend()); }

    reverse_iterator rend() noexcept { return reverse_iterator(begin()); }
    const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); }
    const_reverse_iterator crend() const noexcept { return const_reverse_iterator(cbegin()); }

4. 实现过程中的核心挑战与避坑指南

自己实现 vector 绝非易事,你会遇到许多在单纯使用 std::vector 时不会考虑的细节和陷阱。下面是我在实现过程中总结的一些关键挑战和解决方案。

4.1 迭代器失效问题:理解并管理失效规则

这是使用和实现 vector 时最容易出错的地方。任何可能引起内存重新分配(如 insert push_back 导致扩容)或元素位置移动(如 erase insert )的操作,都会使指向容器元素的迭代器、指针和引用失效。

我们的 MyVector 失效规则模拟标准库:

操作 失效范围
所有只读操作 ( size , capacity , operator[] 访问等) 永不失效
swap , operator= 所有迭代器、指针、引用(指向原容器内容的)
clear , assign 所有迭代器、指针、引用
reserve , shrink_to_fit 如果容量改变,则全部失效;否则不失效。
push_back , emplace_back 如果导致扩容,则全部失效;否则仅 end() 失效。
insert , emplace 如果导致扩容,则全部失效;否则,插入点及其之后的所有迭代器、指针、引用失效。
erase 被删除元素及其之后的所有迭代器、指针、引用失效。
pop_back end() 和被删除元素的迭代器、指针、引用失效。

避坑技巧:

  • 在循环中修改容器 :这是经典陷阱。例如,你想删除所有值为3的元素:
    // 错误写法!erase后it失效,++it行为未定义
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        if (*it == 3) {
            vec.erase(it);
        }
    }
    // 正确写法:利用erase的返回值更新迭代器
    for (auto it = vec.begin(); it != vec.end(); ) {
        if (*it == 3) {
            it = vec.erase(it); // erase返回下一个有效迭代器
        } else {
            ++it;
        }
    }
    // C++20 更简洁的写法:
    std::erase_if(vec, [](int val){ return val == 3; });
    

4.2 异常安全保证:实现强异常保证

我们的目标是让关键操作(如 push_back , insert )提供强异常保证:要么操作完全成功,要么容器状态保持不变。

实现要点:

  1. 资源分配与对象构造分离 :在 _reallocate 中,我们先分配原始内存( _allocate ),然后在新的内存上构造新元素(包括从旧内存移动/拷贝过来的元素和新插入的元素)。只有在新内存上的所有构造都成功后,我们才析构旧元素并释放旧内存。如果构造过程抛出异常,新内存被清理,旧数据完好无损。
  2. 使用 std::move_if_noexcept :在移动旧元素到新内存时,我们不确定元素的移动构造函数是否会抛出异常。如果会,移动过程中抛出异常会导致旧数据被部分移动,状态破坏。 std::move_if_noexcept 是一个类型特性,它在移动构造函数被标记为 noexcept 时返回右值引用(触发移动),否则返回常量左值引用(触发拷贝)。拷贝构造函数通常保证不抛出异常(或提供强异常保证)。这确保了转移操作本身的异常安全。
  3. 拷贝并交换惯用法 :在拷贝赋值运算符中,我们通过按值传参和交换,自然地获得了强异常保证。因为参数的构造(拷贝或移动)是第一步,如果失败, *this 完全不受影响。

4.3 模板与泛型编程的细节

  1. typename 的使用 :在模板中,当引用一个依赖于模板参数的嵌套类型时,必须使用 typename 关键字前缀,告诉编译器这是一个类型。例如,在泛型代码中, T::iterator 需要写成 typename T::iterator
  2. 完美转发 emplace_back emplace 使用了变参模板和 std::forward 来实现完美转发,将参数原封不动地传递给元素的构造函数,保持其值类别(左值/右值)。
  3. SFINAE与概念(C++20) :更健壮的实现可能会使用SFINAE或C++20的Concepts来约束模板参数,确保传入的迭代器类型是合法的输入迭代器。我们的简易版省略了这部分。

4.4 性能优化考量

  1. 移动语义 :为你的 MyVector 实现移动构造函数和移动赋值运算符是至关重要的,这可以避免在传递临时容器时发生不必要的深拷贝。
  2. reserve 的明智使用 :如果你事先知道要插入大量元素,提前调用 reserve 预留足够空间,可以避免多次扩容和数据拷贝,极大提升性能。
  3. 元素类型的移动操作 :确保你存储在 vector 中的类型有高效且 noexcept 的移动构造函数和移动赋值运算符。对于像 std::string std::vector 这样的管理资源的类,移动操作通常很快。
  4. shrink_to_fit 的谨慎使用 :这是一个请求,非强制。频繁调用它可能导致内存碎片和性能下降。通常只在容器容量远大于其大小,且你确定未来不会需要那么多空间时才使用。

5. 测试与验证:确保MyVector行为正确

实现完成后,必须进行全面的测试。我们可以编写测试用例,与 std::vector 的行为进行对比。

#include <iostream>
#include <cassert>
#include "my_vector.h" // 我们的MyVector头文件
#include <vector>
#include <string>

void test_basic() {
    my::vector<int> v1;
    assert(v1.size() == 0 && v1.capacity() == 0 && v1.empty());

    my::vector<int> v2(5, 42);
    assert(v2.size() == 5);
    for (int i : v2) assert(i == 42);

    my::vector<int> v3 = {1, 2, 3, 4, 5};
    assert(v3.size() == 5);
    assert(v3[0] == 1 && v3[4] == 5);

    my::vector<int> v4(v3);
    assert(v4.size() == v3.size());
    for (size_t i = 0; i < v3.size(); ++i) assert(v4[i] == v3[i]);

    my::vector<int> v5(std::move(v4));
    assert(v5.size() == 5 && v4.empty()); // v4资源被移动

    v1 = v5;
    assert(v1.size() == 5);
    std::cout << "Basic tests passed.\n";
}

void test_modifiers() {
    my::vector<std::string> vec;
    vec.reserve(10);
    assert(vec.capacity() >= 10);

    vec.push_back("Hello");
    vec.emplace_back("World");
    assert(vec.size() == 2);
    assert(vec[0] == "Hello" && vec[1] == "World");

    vec.insert(vec.begin() + 1, "C++");
    assert(vec.size() == 3);
    assert(vec[0] == "Hello" && vec[1] == "C++" && vec[2] == "World");

    auto it = vec.erase(vec.begin());
    assert(*it == "C++");
    assert(vec.size() == 2);

    vec.pop_back();
    assert(vec.size() == 1 && vec.back() == "C++");

    vec.clear();
    assert(vec.empty());
    std::cout << "Modifier tests passed.\n";
}

void test_iterators() {
    my::vector<int> vec = {1, 2, 3, 4, 5};
    int sum = 0;
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        sum += *it;
    }
    assert(sum == 15);

    sum = 0;
    for (int val : vec) { // 范围for循环依赖begin()/end()
        sum += val;
    }
    assert(sum == 15);

    // 测试反向迭代器
    my::vector<int> reversed;
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        reversed.push_back(*rit);
    }
    assert((reversed == my::vector<int>{5, 4, 3, 2, 1}));
    std::cout << "Iterator tests passed.\n";
}

void test_exception_safety() {
    // 这是一个简化测试,实际需要更复杂的可抛异常类型
    my::vector<int> vec = {1, 2, 3};
    size_t old_cap = vec.capacity();
    try {
        // 尝试插入大量元素触发扩容,模拟可能的内存分配失败(这里用bad_alloc模拟较难)
        // 更实际的测试需要自定义一个在构造/拷贝时可能抛异常的类型
        vec.insert(vec.begin(), 100, 99); // 假设这里一切正常
    } catch (...) {
        // 如果发生异常,vec应保持原有状态(强异常保证)
        assert(vec.size() == 3);
        assert(vec[0] == 1 && vec[1] == 2 && vec[2] == 3);
        // 容量可能改变(因为reserve可能成功后才抛异常),但元素不变
    }
    std::cout << "Exception safety test considered.\n";
}

int main() {
    test_basic();
    test_modifiers();
    test_iterators();
    test_exception_safety();
    std::cout << "All tests passed for MyVector!\n";
    return 0;
}

通过自己动手实现 MyVector ,你不仅深入理解了 std::vector 的内部机制,更在实践中掌握了C++资源管理、模板编程、异常安全和迭代器设计等核心概念。下次当你再使用 vector 时,你脑海中将清晰地浮现出那三个指针如何舞动,内存如何增长,元素如何移动。这份理解,是阅读任何文档都无法替代的。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐