从零实现C++ vector:深入理解动态数组与内存管理

从零实现C++ vector:深入理解动态数组与内存管理

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_iteratorconst_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,这会导致一次拷贝构造(如果传左值)或移动构造(如果传右值)。然后我们交换*thisother的内容。函数返回时,临时对象other被析构,从而释放了*this原来的资源。这个写法自动提供了强异常保证,并且同时处理了拷贝赋值和移动赋值(通过参数类型的重载决议)。
  2. 移动构造函数的noexcept:标记为noexcept非常重要,这允许标准库容器(如std::vector<vector<int>>)在扩容等操作时使用更高效的移动而非拷贝。
  3. 辅助函数_allocate,_deallocate,_construct_range,_destroy_range这些函数封装了底层的内存分配和对象构造/析构,通常使用std::allocator_traits或直接使用new/deleteplacement 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. reserveshrink_to_fitreserve用于预分配内存,避免后续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

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

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::movestd::move_backward算法配合移动语义,以提升性能(对于可移动的类型)。对于平凡类型(POD),移动就是拷贝。
  3. erase的返回值:标准规定erase返回指向被删除元素之后位置的迭代器。这非常有用,可以配合循环安全地删除元素,例如it = vec.erase(it);
  4. const_cast的使用:为了修改元素,我们需要将const_iterator转换回普通的iterator。这在实现上是可行的,因为const_iteratoriterator本质都是指针,并且我们知道这个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时最容易出错的地方。任何可能引起内存重新分配(如insertpush_back导致扩容)或元素位置移动(如eraseinsert)的操作,都会使指向容器元素的迭代器、指针和引用失效。

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

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

避坑技巧:

  • 在循环中修改容器:这是经典陷阱。例如,你想删除所有值为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_backemplace使用了变参模板和std::forward来实现完美转发,将参数原封不动地传递给元素的构造函数,保持其值类别(左值/右值)。
  3. SFINAE与概念(C++20):更健壮的实现可能会使用SFINAE或C++20的Concepts来约束模板参数,确保传入的迭代器类型是合法的输入迭代器。我们的简易版省略了这部分。

4.4 性能优化考量

  1. 移动语义:为你的MyVector实现移动构造函数和移动赋值运算符是至关重要的,这可以避免在传递临时容器时发生不必要的深拷贝。
  2. reserve的明智使用:如果你事先知道要插入大量元素,提前调用reserve预留足够空间,可以避免多次扩容和数据拷贝,极大提升性能。
  3. 元素类型的移动操作:确保你存储在vector中的类型有高效且noexcept的移动构造函数和移动赋值运算符。对于像std::stringstd::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时,你脑海中将清晰地浮现出那三个指针如何舞动,内存如何增长,元素如何移动。这份理解,是阅读任何文档都无法替代的。