C++ 入门笔记(十六):第16课 vector 模拟实现十问十答 —— 从三指针到迭代器失效

vector 的面试题,背出"三个指针"只是入门。真正容易挂的是这些细节:= default 到底初始化成什么?拷贝构造为什么用 capacity?reserve 和 resize 谁负责填充?insert/erase 之后迭代器为什么不能用?这篇用 10 个问题把第 16 课串一遍。

开篇

学习方式不变:听课 + 自己敲 + AI 辅助。这篇是第 16 课(5.25 vector)的复盘,不是课堂流水账,而是把最容易答错的 10 个点拆开讲。代码在 VS 2026(MSVC 14.51)下编译运行。

一、三指针:vector 的底层骨架

cpp 复制代码
iterator _start;          // 第一个元素
iterator _finish;         // 最后一个有效元素的下一个位置
iterator _endofstorage;   // 容量末尾的下一个位置
  • size() = _finish - _start
  • capacity() = _endofstorage - _start
  • begin() = _start
  • end() = _finish

end() 不是 _endofstorage。[begin, end) 是左闭右开,end() 指向有效元素的末尾;_endofstorage 后面是备用空间。

二、10 个问题

Q1:vector() = default; 和手写 vector() {} 有什么区别?
cpp 复制代码
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;

vector() = default;

三个指针不是随机值,因为类内有默认成员初始化器(NSDMI) ,= nullptr 会在构造时生效。

  • = default:让编译器生成默认构造,不是用户提供,可能保持 trivial / constexpr / noexcept;
  • vector() {}:用户提供的空构造,成员一样会走 NSDMI,但语义上不再是"编译器生成"的那个;
  • 如果成员没有 NSDMI,= default 对内置类型是默认初始化,值不确定;对 string 这类类类型会调默认构造。

vector() {} 不会产生歧义,它只是一个普通的空构造函数。

Q2:拷贝构造为什么用 reserve(v.capacity()),不用 reserve(v.size())?
cpp 复制代码
vector(const vector<T>& v)
{
    reserve(v.capacity());
    for (auto& e : v)
        push_back(e);
}

用 capacity 是为了保留原容器的容量 ,避免拷贝过程中反复扩容;用 size 只保证装得下当前元素,后面再 push_back 可能又要扩。

如果 v 是空 vector:

cpp 复制代码
reserve(0);   // 0 > 0 不成立,什么都不做

三个指针仍然是空;第一次 push_back 时 capacity() == 0,才扩到 4。

Q3:operator=(vector<T> v) 为什么参数是值?
cpp 复制代码
vector<T>& operator=(vector<T> v)
{
    swap(v);
    return *this;
}

这是 copy-and-swap:

  • 传值先拷贝一份独立的 v;
  • swap(v) 交换 _start/_finish/_endofstorage;
  • 函数结束,v 析构,释放原来的旧空间。

v1 = v1 也安全:v 是 v1 的拷贝,不是同一个对象;交换后 v1 内容不变,v 拿到旧 buffer 并释放。

传值版对左值也要拷贝一次;后面学了移动构造,才能把这次拷贝省掉。

Q4:initializer_list 构造为什么用 reserve,不用 resize?
cpp 复制代码
vector(initializer_list<T> il)
{
    reserve(il.size());
    for (auto& e : il)
        push_back(e);
}

因为:

  • reserve(n) 只改 capacity,不构造元素、不改 size;
  • resize(n) 会把 size 改成 n,并构造/填充 n 个元素。

如果这里用 resize(il.size()):

  1. 先构造 n 个元素,size 变成 n;
  2. 再 push_back n 个,最后变成 2n 个元素。

所以初始化列表用 reserve,让 push_back 逐个构造,size 从 0 到 n。

Q5:范围构造为什么写成模板?vector<int> v(s.begin(), s.end()) 能走吗?
cpp 复制代码
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
    while (first != last)
    {
        push_back(*first);
        ++first;
    }
}

写成模板,是为了接受任意容器的迭代器/指针 ,而不是只接受 T*。

string::iterator 本质是 char*,*it 是 char;传进 vector<int> 会转成 ASCII 码,所以能走这个模板。

但这个模板是无约束 的:它不只接受迭代器,连 int 都能推导成功。这就是:

cpp 复制代码
bit::vector<size_t> v(10, 1);   // 编译报错

的原因:InputIterator 被推导成 int,模板体里 *first 对 int 解引用。要严谨,得用 enable_if / concepts 限制成真正的输入迭代器。

Q6:new T[n] 和 STL 的 uninitialized_copy 差在哪?

老师这版:

cpp 复制代码
T* tmp = new T[n];          // 默认构造 n 个元素
for (i < old_size)
    tmp[i] = _start[i];     // 再对前 old_size 个赋值

STL30:

cpp 复制代码
allocate(n);                             // 只申请原始内存
uninitialized_copy(first, last, result); // 只对有效元素拷贝构造

差别:

  • 老师版对 n 个元素都默认构造(包括用不到的容量),再对 old_size 个赋值;
  • STL 只构造 old_size 个有效元素,未使用的容量是裸内存。

所以"扩容必须默认构造全部"不对,这是 new T[n] 写法的代价,不是扩容的本质。

Q7:insert 在 pos == _finish 时,循环会执行吗?
cpp 复制代码
iterator end = _finish - 1;
while (end >= pos)
{
    *(end + 1) = *end;
    --end;
}
*pos = x;
++_finish;
return pos;

pos == _finish 时,end = _finish - 1,end >= pos 一开始就不成立,循环不执行;直接写 *pos,++_finish,返回 pos(新元素的位置)。

Q8:erase 删除最后一个元素时,返回什么?
cpp 复制代码
iterator it = pos + 1;
while (it != _finish)
{
    *(it - 1) = *it;
    ++it;
}
--_finish;
return pos;

删除最后一个时,pos == _finish - 1,所以 it == _finish,循环不执行;--_finish 后 pos == _finish,返回新的 end()。

删除所有偶数要写:

cpp 复制代码
it = v.erase(it);   // 用返回值更新

不能:

cpp 复制代码
v.erase(it);
++it;               // it 已失效,UB

v.erase(it); *it = 1000; 也是错的:it 失效,再解引用是未定义行为。VS Debug 会检查迭代器,g++ 可能看起来"没事",但不代表合法。

Q9:vector<string> 的 reserve 为什么不能用 memcpy?

memcpy 是浅拷贝:

  • 对 int 这种 trivially copyable 类型,没问题;
  • 对 string 这种管理资源的类型,memcpy 只复制对象里的指针/size/capacity;
  • 两个 string 指向同一块堆,析构时 double free。

SSO 短字符串可能"看起来没崩":字符存在对象内部的缓冲区,memcpy 复制的是各自的内联 buffer;长字符串存堆指针,就会共享同一块堆。

但不管长短,对 std::string 用 memcpy 都是 UB,不能依赖"没崩"。正确写法是:

cpp 复制代码
tmp[i] = _start[i];   // 调用 string::operator=,深拷贝
Q10:resize 变小后,被"删掉"的 string 什么时候析构?

老师版:

cpp 复制代码
else
{
    _finish = _start + n;
}

这时不会立刻析构 被"删掉"的 string,它们还在 new T[n] 的数组里。

什么时候析构?

  • vector 析构时:delete[] _start 会析构整个数组的 n 个元素,包括 _finish 后面的;
  • reserve 扩容时:delete[] _start 也会析构旧数组的全部 n 个元素,然后才用新空间;
  • 如果之后 push_back 写进这些位置,赋值会覆盖原来的 string,旧 buffer 这时才释放。

不是"在栈帧里析构"------_start 指向的是堆上的数组 ,局部变量 tmp 才在栈上;对象本身在堆上。

三、错题复盘

容易说错 正确说法
reserve 会填充元素 reserve 只改 capacity;resize 才填充/截断
memcpy 有浅拷贝没问题 浅拷贝正是资源管理类型的坑;trivially copyable 才没问题
扩容必须默认构造全部 这是 new T[n] 的代价;STL 用 uninitialized_copy 只构造有效元素
resize 变小立刻析构 只移动 _finish;析构被推迟到 delete[] / 覆盖 / 扩容
erase 后还能用旧迭代器 旧迭代器失效;用 it = v.erase(it) 接收返回值

结尾

这 10 个问题基本覆盖了手写 vector 的高频坑:

  1. 三指针决定 size/capacity,end() 是 _finish;
  2. reserve 只管容量,resize 才管 size;
  3. memcpy 对资源管理类型是浅拷贝,必须用赋值/拷贝构造;
  4. insert/erase 会挪数据、可能扩容,旧迭代器会失效;
  5. new T[n] 会默认构造全部,STL 的 uninitialized_copy 只构造有效元素。

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

相关推荐
D e re k ZH41 分钟前
C++ 标准库算法全景指南
数据结构·c++·算法
量化分析码农1 小时前
【Python量化波动率实战 02】波动率会传染:EWMA 指数加权给近期行情更大话语权
后端
雪落漂泊2 小时前
C++算法 二分查找篇(上)
开发语言·c++·算法
GISer_Jing2 小时前
如何学习Agent
后端·langchain
聪明的笨猪猪3 小时前
AI应用架构选型:架构师的核心决策困境与权衡
经验分享·面试·架构
不会算法的笨小孩3 小时前
异常的使用以及会出现的问题
开发语言·c++·c++11·异常
JWASX3 小时前
【Go】map 源码解析 - group
后端·golang·map·源码分析
量化分析码农4 小时前
【Python量化波动率实战 01】波动率是怎么算的:年化、滚动与区间估计一次讲清
后端
特立独行的猫A5 小时前
AI 时代,我为什么推荐用 cj-tauri 开发桌面应用
java·后端·python