深度剖析 C++ 之 vector(下)篇
前言
在 深度剖析 C++ 之 vector(上)篇-CSDN博客 中,我们系统地讲解了
std::vector
的常用接口、遍历方式、容量机制、增删查改操作、迭代器失效问题以及 OJ 实战技巧,掌握了它在使用层面的全部核心内容。👉 但仅仅“会用”还不够。
要想真正理解 vector 的精髓,我们得“走进它的身体”,看看它到底是怎么工作的,然后——亲手实现一个属于自己的 vector 👇
在本篇文章中,我们将:
深入剖析
vector
的底层内存布局讲清楚扩容、拷贝、插入删除的原理
从零实现一个属于你自己的
zgx::vector
剖析常见的实现 Bug 与改进点
讲解迭代器与运算符的进阶实现
本篇文章比上篇更“底层”、更硬核💪
建议你准备一杯咖啡 ☕,慢慢往下看,一步步跟着实现👇
一、底层结构与内存模型
vector
本质上就是一个动态顺序表,它底层的核心就是 三根指针👇
T* _start; // 指向已分配空间的起始位置
T* _finish; // 指向当前有效元素的尾后位置
T* _endOfStorage; // 指向已分配空间的尾后位置
可以用下图来形象地表示它👇
[start]------[finish]~~~~~~~~~~~~[end_of_storage]↑ ↑ ↑|<-- 已使用 -->|<------ 预留空间 ---->|
举个例子,比如你有一个 vector<int>
:
zgx::vector<int> v;
v.reserve(10);
此时,假设你只放了 3 个元素👇
_start --------------------> [0][1][2][ ][ ][ ][ ][ ][ ][ ]↑ ↑ ↑_start _finish _endOfStorage
各指针的意义:
-
_start
:底层动态数组的首地址 -
_finish
:当前元素的“尾后”,即 下一个可以插入元素的位置 -
_endOfStorage
:当前这块空间的“尾后”,表示分配到哪了
这套结构正是 vector 能实现高效随机访问 + 自动扩容的根本。
1.1 capacity / size 的本质
在这个结构里:
size() == _finish - _start
capacity() == _endOfStorage - _start
这就是为什么 capacity 通常比 size 大的原因——它提前分配了内存来减少多次扩容的开销。
1.2 扩容机制的核心逻辑
当插入元素时,如果 _finish == _endOfStorage
,说明空间满了:
-
计算新的容量(VS 1.5 倍,g++ 2 倍)
-
申请一块更大的内存
-
将旧元素拷贝到新空间
-
释放旧空间
-
更新三根指针
你写 push_back
的时候,其实就是在不断重复这个过程:
if (_finish == _endOfStorage) {reserve(capacity == 0 ? 1 : capacity * 2);
}
*_finish = value;
++_finish;
二、构造与析构函数实现
有了三指针模型,我们就可以开始实现自己的 zgx::vector
啦
你在 list2.0.h
里的实现非常规范,我们这里挑重点来讲:
2.1 默认构造
vector(): _start(nullptr), _finish(nullptr), _endOfStorage(nullptr)
{}
很简单,三根指针都置空。此时:
-
size = 0
-
capacity = 0
2.2 指定大小构造
vector(size_t n, const T& val = T())
{_start = new T[n];_finish = _start + n;_endOfStorage = _start + n;for (size_t i = 0; i < n; ++i) {_start[i] = val;}
}
这里我们申请 n 个元素的空间,并全部初始化为 val
这对应标准 vector 的 vector(n, val)
构造函数。
2.3 拷贝构造
vector(const vector<T>& v)
{size_t n = v.size();_start = new T[n];_finish = _start + n;_endOfStorage = _start + n;// 拷贝元素for (size_t i = 0; i < n; ++i) {_start[i] = v._start[i];}
}
这是一种“深拷贝”,每个元素都重新分配、复制一遍。
不能只拷贝指针,否则两个对象共享一块内存,析构时就会 double free
2.4 赋值运算符(经典写法:copy-swap)
vector<T>& operator=(vector<T> v)
{swap(_start, v._start);swap(_finish, v._finish);swap(_endOfStorage, v._endOfStorage);return *this;
}
这就是非常优雅的 拷贝-交换(copy-swap) 写法:
-
先用传值参数生成一个副本(拷贝构造)
-
然后交换指针
-
离开函数时 v 析构,释放旧空间
这样写不仅简洁,还能自动处理自赋值,异常安全性也不错。
2.5 析构函数
~vector()
{delete[] _start;_start = _finish = _endOfStorage = nullptr;
}
析构时释放内存即可,vector 不需要调用元素的析构函数(因为这里只支持 T 是普通类型的情况,如果 T 是类对象,还要手动调用析构,属于进阶话题)。
小结
到目前为止,我们完成了:
三指针结构的搭建
各种构造方式 + 析构的实现
赋值运算符的优雅写法
这是实现 vector 的“骨架”部分,也是很多同学最容易忽略掉的内存管理基础。
三、容量管理函数实现
在上篇中我们讲过,std::vector
的容量管理函数主要有:
-
size()
/capacity()
-
empty()
-
reserve(n)
-
resize(n)
它们的底层本质其实就是围绕 _start
、_finish
、_endOfStorage
三根指针做指针运算和内存重分配。
3.1 size()
/ capacity()
/ empty()
这三个函数的实现非常简单,本质就是指针差:
size_t size() const {return _finish - _start;
}size_t capacity() const {return _endOfStorage - _start;
}bool empty() const {return _start == _finish;
}
-
size
:返回当前有效元素个数 -
capacity
:返回底层已分配空间大小 -
empty
:判断 size 是否为 0
这些函数都是 O(1) 的,几乎没有开销。
3.2 reserve(n)
—— 提前预留空间
reserve
是 vector 中最容易踩坑但也最重要的函数之一:
-
如果 n <= 当前 capacity → 啥都不做
-
如果 n > 当前 capacity → 重新分配更大的空间,拷贝原数据
来看实现:
void reserve(size_t n) {size_t oldCapacity = capacity();if (n > oldCapacity) {size_t oldSize = size();T* newStart = new T[n];// 拷贝旧数据for (size_t i = 0; i < oldSize; ++i) {newStart[i] = _start[i];}// 释放旧空间delete[] _start;// 更新三根指针_start = newStart;_finish = _start + oldSize;_endOfStorage = _start + n;}
}
核心步骤:
-
申请一块更大的空间
-
拷贝旧数据
-
释放旧空间
-
更新指针
这就是 vector 扩容的本质。
小示例
zgx::vector<int> v;
v.reserve(10);cout << v.size() << endl; // 0
cout << v.capacity() << endl; // 10
注意:reserve 不会改变 size,只是分配空间。
很多初学者以为 reserve(10)
后就能直接 v[9] = xxx
,这是错误的。
3.3 resize(n)
—— 改变元素个数
resize
是对 size 的调整,而不是 capacity :
-
如果 n < size:缩小 size,删掉多余元素
-
如果 n > size:
-
如果 n <= capacity:直接补默认值
-
如果 n > capacity:先 reserve,再补
-
void resize(size_t n, const T& val = T()) {size_t oldSize = size();if (n <= oldSize) {_finish = _start + n; // 相当于“砍掉”多余部分} else {if (n > capacity()) {reserve(n);}for (size_t i = oldSize; i < n; ++i) {_start[i] = val;}_finish = _start + n;}
}
-
当 n 变小,直接移动
_finish
指针,相当于逻辑上“删掉”尾部元素 -
当 n 变大,可能会触发扩容,然后再补元素
这个行为和标准 vector 一致:
zgx::vector<int> v(3, 1);
v.resize(5, 9);
// 结果:1 1 1 9 9v.resize(2);
// 结果:1 1
3.4 reserve
vs resize
的区别
函数 | 是否改变 size | 是否可能分配空间 | 是否初始化新元素 | 使用场景 |
---|---|---|---|---|
reserve | 否 | 是 | 否 | 预留容量,避免频繁扩容 |
resize | 是 | 都可能 | 是 | 改变逻辑长度 |
面试高频考点:
“
reserve(100)
后能访问v[99]
吗?”
不能,因为 size 仍然是 0!
3.5 扩容策略(g++ VS VS)
虽然你手写的 zgx::vector 没有自动扩容倍数的策略(是根据需求直接 reserve 的),但标准库一般是:
平台 | 扩容倍数 |
---|---|
g++ | 2 倍 |
VS | 1.5 倍(+1) |
你在实现 push_back 时完全可以自己定策略,比如:
if (_finish == _endOfStorage) {size_t newCapacity = capacity() == 0 ? 1 : capacity() * 2;reserve(newCapacity);
}
这样你的 push_back
就和标准 vector 一致了。
小结
通过实现
size
、capacity
、reserve
、resize
,我们掌握了 vector 最核心的内存管理机制:
size/capacity = 三根指针的差
reserve = 提前扩容,避免重复分配
resize = 调整逻辑长度,可能触发扩容
扩容 = 重新申请空间 + 拷贝 + 更新指针
这也是你在实现 insert、push_back、erase 这些函数时的“基石”,后面几章都会用到这套逻辑。
四、访问与修改接口实现
在上篇我们已经详细讲过 std::vector
的常用访问与修改接口:
-
operator[]
、at()
、front()
、back()
、data()
-
push_back()
/pop_back()
在你自己的 zgx::vector
里,核心其实就三个函数:
-
operator[]
—— 随机访问 -
push_back
—— 尾插元素 -
pop_back
—— 尾删元素
4.1 operator[]
—— 下标访问
下标访问本质就是指针偏移
T& operator[](size_t i) {return _start[i];
}const T& operator[](size_t i) const {return _start[i];
}
和标准 vector 一样:
-
访问时间复杂度 O(1)
-
不做越界检查(标准 vector 的 at() 才会检查)
示例:
zgx::vector<int> v(3, 10);
cout << v[0] << endl; // 10v[1] = 99;
cout << v[1] << endl; // 99
下标访问是你在刷题、写算法时最常用的接口,效率和数组几乎完全一样(因为底层就是数组)。
4.2 push_back
—— 尾插元素(扩容核心)
push_back
的实现是 zgx::vector
的关键逻辑之一:
void push_back(const T& x) {if (_finish == _endOfStorage) {// 空间不够,先扩容size_t newCapacity = capacity() == 0 ? 1 : capacity() * 2;reserve(newCapacity);}*_finish = x;++_finish;
}
核心步骤:
-
判断是否有空间,没有就扩容(倍增策略)
-
在
_finish
所在位置写入元素 -
_finish++
,表示元素数 +1
示例:连续 push_back 的扩容过程
zgx::vector<int> v;
for (int i = 0; i < 10; ++i) {v.push_back(i);cout << "size=" << v.size() << " capacity=" << v.capacity() << endl;
}
可能的输出(g++ 逻辑)
size=1 capacity=1
size=2 capacity=2
size=3 capacity=4
size=4 capacity=4
size=5 capacity=8
size=6 capacity=8
size=7 capacity=8
size=8 capacity=8
size=9 capacity=16
size=10 capacity=16
你能清楚看到 容量倍增扩容 的过程,每次扩容都会重新分配内存、拷贝旧元素,这正是标准 vector 的行为。
4.3 pop_back
—— 尾删元素
void pop_back() {if (!empty()) {--_finish;}
}
非常简单,只要把 _finish
向前移一位,相当于逻辑上“删掉”最后一个元素
zgx::vector<int> v(3, 5);
v.pop_back();
// 结果:只剩两个元素
和标准 vector 一样,pop_back 不会收缩 capacity,也不会调用 delete,只是改变尾指针。
4.4 访问函数(补充)
你也可以加上 front
/ back
/ data
,这在调试和算法题中很常用:
T& front() { return *_start; }
T& back() { return *(_finish - 1); }
T* data() { return _start; }
和标准 vector 完全一致,没什么额外开销。
小结
函数 作用 特点 operator[]
下标访问元素 O(1),不检查越界 push_back
尾插 空间不足时自动扩容 pop_back
尾删 不释放空间,时间 O(1) front/back
访问首尾元素 O(1),不检查空 data
获取底层数组首地址 与 C 接口兼容 这几乎就是所有 vector 操作的“最小集合”,剩下的 insert / erase 都是在这个基础上做元素移动的。
五、插入与删除操作实现
除了 push_back
/ pop_back
这类尾部操作,vector
还提供了更通用的接口:
-
insert(iterator pos, const T& val)
—— 在 pos 前插入一个元素 -
erase(iterator pos)
—— 删除 pos 位置的元素
它们的实现都需要手动搬移数据(从 pos 往后移/往前移),这也是你实现的 zgx::vector
的重点部分。
5.1 insert
的基本逻辑
插入元素的核心步骤如下:
-
保存插入位置相对偏移
因为可能会触发扩容,扩容后旧的迭代器会失效,所以必须先用pos - _start
保存下标位置。 -
扩容检查
如果_finish == _endOfStorage
,就按倍增策略 reserve。 -
确定新插入位置指针
扩容后重新计算:pos = _start + offset
-
从尾部开始向后搬移元素
为新元素腾出空间,注意要从尾往前搬,不然会覆盖数据。 -
插入元素,_finish++
5.2 insert
代码实现
iterator insert(iterator pos, const T& x) {assert(pos >= _start && pos <= _finish);// 1 保存偏移size_t offset = pos - _start;// 2 扩容if (_finish == _endOfStorage) {size_t newCapacity = capacity() == 0 ? 1 : capacity() * 2;reserve(newCapacity);}// 3 重新计算 pospos = _start + offset;// 4 从尾部向后搬移元素iterator end = _finish;while (end != pos) {*(end) = *(end - 1);--end;}// 5 插入新元素*pos = x;++_finish;return pos;
}
这里的关键点是:
-
使用 offset 来避免扩容导致迭代器失效;
-
搬移时一定要从尾往前;
-
返回新插入位置的迭代器,和标准 vector 一致。
示例
zgx::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);auto it = v.begin() + 1;
v.insert(it, 99);
// 结果:1 99 2 3
插入 99 后,原本的 [1,2,3] 变成 [1,99,2,3],容量自动扩容(如有需要),尾部整体后移一格。
5.3 erase
的基本逻辑
删除元素的步骤:
-
检查位置合法
-
从 pos+1 到尾部的元素整体往前搬一位
-
_finish--
不需要释放空间,也不需要扩容检查,删除只是逻辑层面的移动。
5.4 erase
代码实现
iterator erase(iterator pos) {assert(pos >= _start && pos < _finish);iterator next = pos + 1;while (next != _finish) {*(next - 1) = *next;++next;}--_finish;return pos;
}
返回值与标准 vector 一致:
返回删除位置的迭代器(即“新元素”所处的位置)
示例
zgx::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);auto it = v.begin() + 1;
v.erase(it);
// 结果:1 3 4
删除 2 后,3 和 4 整体前移,size 减一,capacity 不变。
5.5 插入删除与迭代器失效
虽然我们在这篇里不单独展开迭代器失效(那是上篇的内容),但这里要提醒两点:
-
扩容导致迭代器整体失效
insert 时如果发生扩容,之前所有迭代器、指针、引用都会失效。 -
erase 导致 [pos, end) 范围内迭代器失效
例如你删除中间元素后,后面元素位置改变,指向它们的迭代器不再有效。
这也是为什么标准 vector 的 insert/erase 都会返回一个迭代器,方便你继续操作。
小结
函数 | 功能 | 核心步骤 |
---|---|---|
insert | 在 pos 前插元素 | 保存 offset → 扩容 → 搬移 → 插入 |
erase | 删除 pos 元素 | 搬移后续元素 → finish-- |
插入删除操作本质上就是偏移 + 扩容 + 搬移,逻辑上并不复杂,但实现时很容易出错,尤其是偏移保存这一步,很多初学者直接在扩容后用旧迭代器,结果就炸了。
六、实现难点与常见 Bug 剖析
虽然我们已经完整实现了 zgx::vector
的核心功能,但在实际写的过程中,初学者常常会遇到一些“玄学崩溃”、段错误、内存错误:
❌ 有时是析构时 double free
❌ 有时是 erase 迭代器崩溃
❌ 有时是插入后数据错乱
❌ 有时是拷贝构造浅拷贝
这一章我们就来总结并剖析几个最典型的 5 个易错点。
Bug 1:拷贝构造 / 赋值时浅拷贝导致 double free
错误示例
vector(const vector<T>& v): _start(v._start), _finish(v._finish), _endOfStorage(v._endOfStorage)
{}
这段代码直接把对方的指针“抄”过来了,没有重新分配空间,也没有复制元素。
结果:
-
两个对象共享同一块内存;
-
当它们先后析构时,就会对同一块内存
delete[]
两次,直接段错误。
修正写法
vector(const vector<T>& v) {size_t n = v.size();_start = new T[n];_finish = _start + n;_endOfStorage = _start + n;for (size_t i = 0; i < n; ++i) {_start[i] = v._start[i];}
}
必须“深拷贝”,申请新空间,复制元素。
这是最经典的浅拷贝陷阱,面试常考。
Bug 2:扩容后未重新计算 pos,insert 崩溃
错误示例
iterator insert(iterator pos, const T& x) {if (_finish == _endOfStorage) {reserve(capacity() * 2);}// 这里直接用 pos 了,pos 已经失效iterator end = _finish;while (end != pos) { /* ... */ }
}
一旦扩容发生,原来的 pos(本质是指向旧空间的指针)立刻变成野指针,继续使用就炸了。
修正写法
size_t offset = pos - _start;
if (_finish == _endOfStorage) {reserve(capacity() == 0 ? 1 : capacity() * 2);
}
pos = _start + offset;
保存 offset 是 insert 的关键一环。
这是很多初学者最容易忽略的细节之一。
Bug 3:erase 返回值没用,迭代器悬空
错误示例
auto it = v.begin();
while (it != v.end()) {if (*it == 3) {v.erase(it);++it; // 悬空了!} else {++it;}
}
erase 删除元素时,会把后续元素整体前移,导致删除位置后的所有迭代器全部失效。
上面这段代码在 erase 后 it
就成了悬空迭代器,++it
直接炸掉。
修正写法
auto it = v.begin();
while (it != v.end()) {if (*it == 3) {it = v.erase(it); // 正确!使用返回值} else {++it;}
}
erase 会返回“删除元素后,新的该位置迭代器”,标准 vector 也是这样设计的。
忽略返回值是非常常见的 bug!
Bug 4:reserve 忘了更新 _finish / _endOfStorage
错误示例
void reserve(size_t n) {if (n > capacity()) {T* newStart = new T[n];memcpy(newStart, _start, size()*sizeof(T));delete[] _start;_start = newStart;// 忘了更新 _finish 和 _endOfStorage}
}
这样扩容后 _finish
仍然指向旧空间,插入新元素时就直接写进了野地址,爆炸。
修正写法
void reserve(size_t n) {if (n > capacity()) {size_t oldSize = size();T* newStart = new T[n];for (size_t i = 0; i < oldSize; ++i) {newStart[i] = _start[i];}delete[] _start;_start = newStart;_finish = _start + oldSize; // 正确!_endOfStorage = _start + n; // 正确!}
}
扩容时三根指针都要正确更新,这是 vector 的根基。
Bug 5:operator= 写法错误,内存泄漏 or 自赋值崩溃
错误示例
vector<T>& operator=(const vector<T>& v) {if (this != &v) {delete[] _start;_start = new T[v.size()];// ...}return *this;
}
表面没问题,但一旦自己给自己赋值,就 delete 掉自己正在读的数据 → 直接炸掉。
此外,如果中途 new 抛异常,原空间已经被 delete → 泄漏 + 崩溃。
修正写法(copy-swap)
vector<T>& operator=(vector<T> v) {swap(_start, v._start);swap(_finish, v._finish);swap(_endOfStorage, v._endOfStorage);return *this;
}
非常优雅、健壮的写法:
-
参数传值 → 自动调用拷贝构造,生成副本
-
swap → 交换指针
-
函数结束 → v 析构,释放旧资源
这也是现代 C++ 实现容器时的常用套路。
小结
Bug 类型 | 原因 | 修正关键点 |
---|---|---|
浅拷贝 double free | 拷贝构造/赋值只复制指针 | 深拷贝 |
扩容后迭代器失效 | insert 扩容未保存 offset | 先保存 offset 再扩容 |
erase 悬空迭代器 | 忽略返回值 | 使用 erase 的返回值 |
reserve 忘更新指针 | 扩容后没同步 _finish / _endOfStorage | 三指针都要更新 |
operator= 自赋值崩溃 / 泄漏 | delete 后再读 / 异常安全问题 | copy-swap 写法 |
这五个问题几乎覆盖了手写 vector 的 80% 踩坑点。
很多面试喜欢让你实现 vector,然后故意让你在这些地方“踩雷”,掌握它们,就已经非常接近标准实现了。
七、迭代器与运算符重载进阶
在前面我们已经用指针来实现了 begin()
、end()
,这其实已经足够完成大部分功能:
iterator begin() { return _start; }
iterator end() { return _finish; }
但如果想让 zgx::vector
更加贴近标准 vector,提升泛用性和可读性,我们还可以加上一些进阶接口:
-
const 版本的迭代器
-
一些比较运算符(==、!=、<)
-
输出流运算符(方便打印)
这些看起来不复杂,但非常实用!
7.1 const 迭代器
标准 vector 提供了 const 版本的迭代器,比如:
const vector<int> v = {1,2,3};
auto it = v.begin(); // const_iterator
这可以保证使用者不能通过迭代器修改容器内容。
在你目前的实现中,迭代器本质就是指针,因此只要加上 const 修饰就行:
const_iterator cbegin() const {return _start;
}const_iterator cend() const {return _finish;
}
当你的 zgx::vector
被声明为 const 时,调用 cbegin()
/cend()
,返回的是 const T*
,不能通过迭代器修改元素:
const zgx::vector<int> v(3, 100);
for (auto it = v.cbegin(); it != v.cend(); ++it) {cout << *it << endl; // 可以访问// *it = 10; 编译报错
}
这就是 const_iterator 的价值所在,很多 STL 算法都会优先调用 cbegin/cend 来确保容器内容不被修改。
7.2 比较运算符(==、!=、<)
标准 vector 支持直接用 ==
、!=
、<
来比较两个 vector:
vector<int> a = {1,2,3};
vector<int> b = {1,2,3};
vector<int> c = {1,3};cout << (a == b) << endl; // true
cout << (a != c) << endl; // true
cout << (a < c) << endl; // true (按字典序)
你也可以给 zgx::vector
加上类似的运算符:
7.2.1 operator==
/ operator!=
bool operator==(const vector<T>& v) const {if (size() != v.size()) return false;for (size_t i = 0; i < size(); ++i) {if (_start[i] != v._start[i]) return false;}return true;
}bool operator!=(const vector<T>& v) const {return !(*this == v);
}
这两者非常直接,按元素逐一比较即可。
7.2.2 operator<
(字典序比较)
标准库 vector 的 <
是按字典序比较的:
vector<int> a = {1, 2, 3};
vector<int> b = {1, 3};
cout << (a < b) << endl; // true,因为 2 < 3
你可以仿写:
bool operator<(const vector<T>& v) const {size_t minSize = size() < v.size() ? size() : v.size();for (size_t i = 0; i < minSize; ++i) {if (_start[i] < v._start[i]) return true;else if (_start[i] > v._start[i]) return false;}return size() < v.size();
}
-
从头开始比较;
-
第一个不同的元素决定大小;
-
如果前面都相同,长度短的更小。
这让你的容器在 std::sort
、std::map
等泛型算法中也能直接比较,非常有用。
7.3 输出流运算符 operator<<
(调试神器)
手写容器调试最大的痛点就是看不到内容。
你可以加上一个 friend
运算符,让 cout << v
能直接输出。
friend std::ostream& operator<<(std::ostream& out, const vector<T>& v) {out << "[";for (size_t i = 0; i < v.size(); ++i) {out << v._start[i];if (i + 1 < v.size()) out << ", ";}out << "]";return out;
}
使用示例:
zgx::vector<int> v;
for (int i = 0; i < 5; ++i) v.push_back(i);
cout << v << endl;
// 输出:[0, 1, 2, 3, 4]
写算法时这功能非常香,直接看容器内容而不需要写 for 循环。
小结
功能 | 作用 |
---|---|
const_iterator | 保证 const 容器的只读访问,兼容 STL 算法 |
== / != / < 运算符 | 支持元素级别比较与字典序比较,方便泛型算法 |
输出流运算符 << | 快速调试容器内容,提高开发效率 |
这些接口虽然不是“必需的”,但能让你的 zgx::vector
真的像一个“正规军”,而不是只能在 main 函数里 for 循环的玩具容器。
八、实现功能回顾与总结
到这里,我们的 zgx::vector
已经从零实现完毕。
从最底层的内存结构到 insert / erase,再到迭代器与运算符重载,基本已经涵盖了 std::vector
的主体功能:
namespace zgx {template<class T>class vector {public:// 构造 / 析构 / 拷贝 / 赋值vector();vector(size_t n, const T& val = T());vector(const vector<T>& v);vector<T>& operator=(vector<T> v);~vector();// 迭代器iterator begin();iterator end();const_iterator cbegin() const;const_iterator cend() const;// 容量管理size_t size() const;size_t capacity() const;bool empty() const;void reserve(size_t n);void resize(size_t n, const T& val = T());// 访问与修改T& operator[](size_t i);void push_back(const T& x);void pop_back();T& front();T& back();T* data();// 插入与删除iterator insert(iterator pos, const T& x);iterator erase(iterator pos);// 运算符bool operator==(const vector<T>& v) const;bool operator!=(const vector<T>& v) const;bool operator<(const vector<T>& v) const;friend std::ostream& operator<<(std::ostream& out, const vector<T>& v) { ... }private:T* _start;T* _finish;T* _endOfStorage;};
}
8.1 我们已经实现了哪些?
1. 内存模型:
-
三指针
_start
/_finish
/_endOfStorage
-
扩容逻辑、拷贝搬移机制
2. 常用接口:
-
构造、拷贝、赋值、析构
-
size、capacity、reserve、resize
-
push_back、pop_back、insert、erase
3. 迭代器与语法糖:
-
iterator / const_iterator
-
==、!=、< 比较
-
输出流打印
4. Bug 剖析:
-
浅拷贝、迭代器失效、erase 返回值、reserve 忘更新指针、自赋值问题等
这些内容已经足够支撑大多数实际场景、OJ 题、甚至是一些面试笔试的“手写容器题”。
8.2 和标准 vector 相比,还有哪些没实现?
虽然我们的 zgx::vector
已经很完整,但标准库 vector 还有一些更复杂的部分:
特性 | 标准 vector | zgx::vector |
---|---|---|
异常安全 | 强保证(通过 RAII) | 无,new 失败可能内存泄漏 |
分配器(Allocator) | 可自定义内存分配策略 | 固定使用 new/delete |
移动语义(C++11) | 完整支持,避免多余拷贝 | 未实现 |
emplace 系列(C++11) | 原地构造,性能更好 | 未实现 |
assign / swap 等接口 | 支持多种赋值与交换 | 部分支持(copy-swap) |
异常抛出规范 | 明确 noexcept / strong | 无声明 |
iterator_traits / Reverse Iterator | 完整支持 | 迭代器只是裸指针 |
换句话说,我们实现的是一个“教学版本”的 vector,清晰、好理解、便于踩坑与讲解,但离真正的工业级实现还有距离!!
8.3 写在最后
实现一个
vector
的过程,其实就是完整走了一遍“容器设计”的底层逻辑:
内存管理
构造/析构与拷贝语义
容量管理策略
元素访问与增删改
迭代器与算法兼容
常见 Bug 与异常安全问题
这个过程非常锻炼功底,也是 C++ 面试中最有含金量的部分之一。
当你能自己手撕一个 vector
,你不仅仅是“学会了 vector”:
你真正掌握了:
动态数组的实现原理
C++ 对象生命周期管理
STL 的接口设计思想
内存与迭代器失效问题
总结
至此,我们完成了《深度剖析 C++ 之 vector》系列的下篇内容:
底层内存结构剖析
构造/析构、容量管理、增删改查接口实现
插入删除的偏移与扩容机制
典型 Bug 剖析与修正
迭代器与运算符进阶
全面回顾与设计取舍
这篇文章不只是“实现一个容器”,更重要的是通过实现,真正理解了标准 vector 的设计精髓。
希望这一系列文章,能帮助你在 C++ 的道路上打下更扎实的基础
如果你坚持到这里,恭喜你完成了一个非常硬核的主题!