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 - _start
  • capacity() = _endofstorage - _start
  • begin() = _start
  • end() = _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

reserve 与 resize 相关代码

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 与 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

vector 运行结果

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

六、STL30 对照:三个指针和 destroy

STL30 vector 对照

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() 为什么不是 _endofstorageend() 是有效元素末尾,容量末尾是备用空间
reserve 和 resizereserve 只改 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 从“会用”推进到“知道它内部怎么长出来”:

  1. 三指针模型是基础,size/capacity/end 都从指针相减来;
  2. reserve 的核心是“申请新空间 + 深拷贝有效元素 + 释放旧空间”,对非平凡类型不能用 memcpy;
  3. insert/erase 会挪数据、可能扩容,旧迭代器会失效;删除用 it = v.erase(it),插入用返回的新迭代器。

下一步继续 vector 的迭代器、深拷贝和更多接口。

Logo

更多推荐