C++ 入门笔记(十五):第15课 vector 的模拟实现 —— 三指针、reserve 深拷贝与迭代器失效
vector 的面试题,背出“三个指针”只是入门。真正容易挂的是三个坑:
reserve图省事用memcpy,vector<string>直接 double free;insert扩容后还在用旧迭代器;bit::vector<size_t> v(10, 1)被范围构造函数抢走,编译都过不了。
开篇
学习方式不变:听课 + 自己敲 + AI 辅助。这篇对应第 15 课(vector),代码在 VS 2026(MSVC 14.51)下编译运行;5.22 课发的 stl30.tar.gz 里正好有 SGI STL 的 stl_vector.h,我把它和老师这版放在一起对照。
一、三指针:vector 到底怎么管一块连续空间

iterator _start; // 第一个元素
iterator _finish; // 最后一个有效元素的下一个位置
iterator _endofstorage; // 容量末尾的下一个位置
size() = _finish - _startcapacity() = _endofstorage - _startbegin() = _startend() = _finish
end() 不是 _endofstorage。[begin, end) 是左闭右开,end() 指向有效元素的末尾;_endofstorage 后面是“备用空间”,不能遍历。
SGI STL 里叫 start、finish、end_of_storage,和老师这三指针一一对应。
二、构造与赋值:copy-and-swap 和“被模板抢走”的构造函数

老师这版有:
vector() = default;
vector(initializer_list<T> il);
vector(const vector<T>& v);
template<class InputIterator>
vector(InputIterator first, InputIterator last);
vector(int n, T val = T());
vector(size_t n, T val = T());
vector<T>& operator=(vector<T> v); // 传值 + swap
operator=(vector<T> v) 还是 copy-and-swap:传值拷贝一份 v,swap 后临时对象析构,释放原来的空间。
这里有个非常容易踩的坑:
bit::vector<size_t> v(10, 1); // 编译报错
报错大概是:
error C2100: 无法取消引用类型为“InputIterator”的操作数
with [InputIterator=int]
为什么?因为 (10, 1) 两个参数都是 int,编译器发现:
template<class InputIterator>
vector(InputIterator first, InputIterator last);
推导出 InputIterator = int,两个参数都是精确匹配;而 (size_t, T) 需要把 10 从 int 转成 size_t,是转换。精确匹配的模板胜出,然后模板体里 *first 对 int 解引用,就报错了。
这不是“歧义”,是范围构造函数模板太贪婪,把两个整数当成了迭代器区间。
bit::vector<int> v(10, 1) 为什么没事?因为 (int, T) 是非模板,两个参数都精确匹配;非模板和模板一样好时,优先选非模板。
修复方向:给范围构造函数加约束,只接受真正的输入迭代器(C++20 requires,或 enable_if + iterator_traits);或者调用时显式写 (size_t)10, (size_t)1。
三、reserve:为什么不能 memcpy

void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size(); // 必须在 new/delete 之前保存
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * old_size); // 错!
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i]; // 调用 T::operator=,深拷贝
}
delete[] _start;
}
_start = tmp;
_finish = _start + old_size;
_endofstorage = _start + n;
}
}
对 int 这种类型,memcpy 可以;对 string 这种类型,memcpy 只拷对象里的指针、size、capacity,两个 string 会指向同一块堆,析构时 double free。
对长字符串会直接 double free;对短字符串,可能因为 SSO 把字符存在对象内部,memcpy 看起来“没炸”,但仍然是未定义行为,不能依赖。
所以这里必须用 tmp[i] = _start[i],让 string::operator= 做深拷贝。
另外,old_size 必须在 new T[n] / delete[] _start 之前保存,因为 _start 一换,_finish - _start 就不是原来的 size 了。
new T[n] 还有一个代价:它会把 n 个元素都默认构造出来,再赋值;STL30 用的是 uninitialized_copy,只构造有效元素,效率更高,异常处理也更严谨。
四、resize:变大填充,变小截断
void resize(size_t n, T val = T())
{
if (n > size())
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
else
{
_finish = _start + n;
}
}
- 变大:
reserve(n)后从_finish开始填val; - 变小:直接把
_finish移到_start + n。
注意老师这版变小只是移动 _finish,没有调用元素的析构。对 vector<string> 来说,被“删掉”的 string 对象仍然活着,资源要等到 vector 析构或那段空间被重新赋值时才释放。
STL30 的 resize 变小是:
erase(begin() + new_size, end());
erase 里会 destroy 掉后面的元素,资源立刻释放。这是教学简化版和标准版的差别。
五、insert / erase 与迭代器失效

insert 的关键是扩容后重新定位 pos:
if (_finish == _endofstorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len; // reserve 后 pos 失效,必须重新算
}
reserve 可能异地扩容,旧 pos 指向已经释放的空间;不更新 pos 就是 UB。insert 返回指向插入元素的迭代器,也就是新的 pos。
erase:
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
后面的元素整体前移,返回删除位置(现在放着原来的下一个元素);如果删的是最后一个,返回 end()。
删除所有偶数要写:
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
it = v.erase(it); // 用返回值更新,旧 it 已失效
else
++it;
}
不能写:
v.erase(it);
++it; // it 已经失效,++it 是 UB

test_vector4 里 v.erase(it); *it = 1000; 也是错的:it 失效,再解引用是未定义行为。VS Debug 的迭代器调试会直接断言;g++ 可能因为内存还在、元素挪动后地址没变,看起来“能跑”,但不代表合法。
六、STL30 对照:三个指针和 destroy

5.22 课发的 stl30.tar.gz 里,stl_vector.h 是:
iterator start;
iterator finish;
iterator end_of_storage;
reserve 用 allocate_and_copy(n, start, finish):
allocate申请原始空间;uninitialized_copy只对有效元素构造;destroy(start, finish)析构旧元素;deallocate释放旧空间;- 再更新
start/finish/end_of_storage。
erase 里:
copy(position + 1, finish, position);
--finish;
destroy(finish);
return position;
copy 挪数据,--finish 调整结构,destroy(finish) 析构被删掉的那个尾部元素。
resize 变小走 erase(begin()+n, end());变大走 insert(end(), n-size(), x)。
对比老师这版:
- 老师
new T[n]会把 n 个元素都默认构造,再赋值;STL 只构造有效元素; - 老师
resize变小只移动_finish,不析构;STL 会destroy; - 老师
erase只是赋值前移 +--_finish,不析构尾部;STL 会destroy(finish)。
教学版用来理解“三指针 + 扩容 + 挪动”足够,但不要拿它当标准库的异常安全和资源管理范本。
七、面试速记
| 问题 | 一句话答案 |
|---|---|
| vector 的三指针 | _start、_finish、_endofstorage;size/capacity 都靠指针相减 |
end() 为什么不是 _endofstorage | end() 是有效元素末尾,容量末尾是备用空间 |
reserve 和 resize | reserve 只改 capacity;resize 改 size,变大填、变小截断 |
| 为什么 reserve 不能用 memcpy | 对 string 是浅拷贝,两个对象指向同一块堆,double free |
| insert 扩容后要注意什么 | pos 会失效,要先用 len = pos - _start 保存,再重新算 |
| erase 返回什么 | 被删元素之后的那个元素(现在在删除位置);删最后一个返回 end() |
为什么 it = v.erase(it) | 旧 it 失效,erase 返回的才是合法的下一个位置 |
vector<size_t> v(10,1) 为什么报错 | 范围构造函数模板把两个 int 当成迭代器,精确匹配抢走了填充构造 |
vector<string> 扩容要注意什么 | 不能用 memcpy,必须调用 string 的赋值/拷贝构造做深拷贝 |
结尾
这节课把 vector 从“会用”推进到“知道它内部怎么长出来”:
- 三指针模型是基础,
size/capacity/end都从指针相减来; reserve的核心是“申请新空间 + 深拷贝有效元素 + 释放旧空间”,对非平凡类型不能用memcpy;insert/erase会挪数据、可能扩容,旧迭代器会失效;删除用it = v.erase(it),插入用返回的新迭代器。
下一步继续 vector 的迭代器、深拷贝和更多接口。
更多推荐


所有评论(0)