从零实现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 乃至许多连续存储容器的钥匙。我们通常用三个指针来标记内存块的状态:
-
_start(或begin) : 指向已分配内存块的起始位置。这是整个数组的“头”。 -
_finish(或end) : 指向当前已构造的最后一个元素的下一个位置。_finish - _start就等于size(),即容器中当前元素的数量。 -
_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);
}
关键点解析:
- 拷贝赋值运算符的“拷贝并交换”惯用法 :这是实现异常安全赋值操作的优雅方式。
operator=的参数是vector other,这会导致一次拷贝构造(如果传左值)或移动构造(如果传右值)。然后我们交换*this和other的内容。函数返回时,临时对象other被析构,从而释放了*this原来的资源。这个写法自动提供了强异常保证,并且同时处理了拷贝赋值和移动赋值(通过参数类型的重载决议)。 - 移动构造函数的
noexcept:标记为noexcept非常重要,这允许标准库容器(如std::vector<vector<int>>)在扩容等操作时使用更高效的移动而非拷贝。 - 辅助函数 :
_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;
}
关键点解析:
-
reserve与shrink_to_fit:reserve用于预分配内存,避免后续push_back时频繁扩容。shrink_to_fit是一个非强制性的请求,希望释放未使用的内存。注意,标准不保证调用后capacity() == size(),但我们的实现可以做到。 -
_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); // 返回新元素的引用
}
关键点解析:
- 重载决议 :我们提供了
push_back的常量左值引用和右值引用版本。当传入临时对象(右值)时,编译器会选择移动版本,避免不必要的拷贝。 -
emplace_back的优势 :它使用变参模板和完美转发,可以直接在容器尾部构造元素,无需创建临时对象。例如vec.emplace_back(10, 'a')可以直接调用T(10, 'a')构造函数,而push_back(T(10, 'a'))则需要先构造一个临时T对象,再移动(或拷贝)进去。emplace_back通常更高效。 - 扩容时机 :在插入前检查容量。扩容操作
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;
}
关键点解析:
-
insert的迭代器失效 :insert可能导致扩容,一旦扩容,所有迭代器都失效。即使不扩容,插入点及其之后的迭代器也失效。我们的实现返回了一个新的迭代器指向新插入的元素。 - 元素移动 :我们使用了
_move和_move_backward这样的辅助函数。在C++11之后,应该使用std::move和std::move_backward算法配合移动语义,以提升性能(对于可移动的类型)。对于平凡类型(POD),移动就是拷贝。 -
erase的返回值 :标准规定erase返回指向被删除元素之后位置的迭代器。这非常有用,可以配合循环安全地删除元素,例如it = vec.erase(it);。 -
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 )提供强异常保证:要么操作完全成功,要么容器状态保持不变。
实现要点:
- 资源分配与对象构造分离 :在
_reallocate中,我们先分配原始内存(_allocate),然后在新的内存上构造新元素(包括从旧内存移动/拷贝过来的元素和新插入的元素)。只有在新内存上的所有构造都成功后,我们才析构旧元素并释放旧内存。如果构造过程抛出异常,新内存被清理,旧数据完好无损。 - 使用
std::move_if_noexcept:在移动旧元素到新内存时,我们不确定元素的移动构造函数是否会抛出异常。如果会,移动过程中抛出异常会导致旧数据被部分移动,状态破坏。std::move_if_noexcept是一个类型特性,它在移动构造函数被标记为noexcept时返回右值引用(触发移动),否则返回常量左值引用(触发拷贝)。拷贝构造函数通常保证不抛出异常(或提供强异常保证)。这确保了转移操作本身的异常安全。 - 拷贝并交换惯用法 :在拷贝赋值运算符中,我们通过按值传参和交换,自然地获得了强异常保证。因为参数的构造(拷贝或移动)是第一步,如果失败,
*this完全不受影响。
4.3 模板与泛型编程的细节
-
typename的使用 :在模板中,当引用一个依赖于模板参数的嵌套类型时,必须使用typename关键字前缀,告诉编译器这是一个类型。例如,在泛型代码中,T::iterator需要写成typename T::iterator。 - 完美转发 :
emplace_back和emplace使用了变参模板和std::forward来实现完美转发,将参数原封不动地传递给元素的构造函数,保持其值类别(左值/右值)。 - SFINAE与概念(C++20) :更健壮的实现可能会使用SFINAE或C++20的Concepts来约束模板参数,确保传入的迭代器类型是合法的输入迭代器。我们的简易版省略了这部分。
4.4 性能优化考量
- 移动语义 :为你的
MyVector实现移动构造函数和移动赋值运算符是至关重要的,这可以避免在传递临时容器时发生不必要的深拷贝。 -
reserve的明智使用 :如果你事先知道要插入大量元素,提前调用reserve预留足够空间,可以避免多次扩容和数据拷贝,极大提升性能。 - 元素类型的移动操作 :确保你存储在
vector中的类型有高效且noexcept的移动构造函数和移动赋值运算符。对于像std::string、std::vector这样的管理资源的类,移动操作通常很快。 -
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 时,你脑海中将清晰地浮现出那三个指针如何舞动,内存如何增长,元素如何移动。这份理解,是阅读任何文档都无法替代的。
更多推荐




所有评论(0)