🔷 第十三章 STL
📄 一句话概括

vector 是"能自动扩容的数组",也是现实里 90% 场合的默认选择。 它内部就一块连续内存,所以按下标取、尾部加、遍历都快——和 7.1 的数组一样;但它知道自己的长度、能长大、还能用 at() 做越界检查。记住三件事:size 是装了几个、capacity 是申请了多大;[] 不检查、at() 会检查;扩容是成倍的,提前 reserve 能省搬家。

📝 本节和前面几节的关系

上一节(13.1)讲了三个序列容器的性格,这一节正式打开第一个盒子。 它是第十二章那些模板知识的第一次真实使用: - vector<int> 就是 12.2.1 的类模板——vector 是模板名,<int> 是模板实参,必须写尖括号; - vector<int> 和 vector<string> 是两个完全独立的类(12.2 性质 2),用了几种类型就实例化出几个类; - vector 内部替你干了 10.2.2 复制构造函数(深拷贝)和 10.3 析构函数(自动释放)——它把三法则写好了,你什么都不用管; - push_back 之类的方法就是 10.4.4 this 指针在服务的普通成员函数。 往回看 7.1 一维数组:你终于可以不再写 int a[100] 然后担心"够不够、有多少个是有效的"了。

一、先看"它和数组到底差在哪"

对比项 int a[10];(7.1) vector<int> v;
长度 编译期定死,改不了 运行期能变,push_back 就长
知道几个有效元素吗 不知道(sizeof(a)/sizeof(a[0]) 只给容量) 知道,v.size()
越界检查 没有 v.at(i) 有(v[i] 还是没有)
能否整体赋值 不能(a = b; ✗) 能(v1 = v2; 是深拷贝)
能否作为函数返回值 不能直接返回 能(vector<int> f(); 没问题)
存自定义类 要自己管生命周期 自动调构造/析构(10.2 / 10.3)
代价 零开销 一点额外指针/长度记录 + 扩容时的搬家

一句话:vector ≈ 数组 + 长度 + 扩容 + 检查 + 自动生命周期管理。代价是极小的(几个字节)和可控的(提前 reserve)。

二、构造:五种起手方式

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v1;                       // ① 空的
    vector<int> v2(5);                    // ② 5 个元素,值都是 0
    vector<int> v3(5, 7);                 // ③ 5 个元素,值都是 7
    vector<int> v4 = {1, 2, 3, 4, 5};     // ④ 列表初始化(最常用)
    vector<int> v5(v4);                   // ⑤ 从另一个 vector 复制(深拷贝!)

    cout << "v2: "; for (size_t i = 0; i < v2.size(); i++) cout << v2[i] << " "; cout << endl;
    cout << "v3: "; for (size_t i = 0; i < v3.size(); i++) cout << v3[i] << " "; cout << endl;
    cout << "v4: "; for (size_t i = 0; i < v4.size(); i++) cout << v4[i] << " "; cout << endl;

    v5[0] = 99;                           // ★ 改 v5 不影响 v4
    cout << "改 v5[0] 之后 —— v4[0]=" << v4[0] << ",v5[0]=" << v5[0] << endl;
    return 0;
}

输出:

v2: 0 0 0 0 0 
v3: 7 7 7 7 7 
v4: 1 2 3 4 5 
改 v5[0] 之后 —— v4[0]=1,v5[0]=99

最后一行是重点:vector<int> v5(v4); 是深度复制(这里内部元素是 int,所以看起来像值复制;但换成 vector<string> 也一样安全)。这件事 vector 已经替你办好了——就是 10.2.2 复制构造函数 + 三法则。

三、实例01:vector 模板类的操作方法(教材实例)

任务:把 vector 的加减、查看、遍历、增删改查都过一遍。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

void show(const char *title, const vector<string> &v) {
    cout << title << " [size=" << v.size() << ", cap=" << v.capacity() << "]:";
    for (size_t i = 0; i < v.size(); i++) cout << v[i] << " ";
    cout << endl;
}

int main() {
    vector<string> v;

    // ── ① 加 ──────────────────────────
    v.push_back("apple");            // 尾部加
    v.push_back("banana");
    v.push_back("cherry");
    show("三次 push_back 后", v);

    v.insert(v.begin() + 1, "apricot");    // 在"第 2 个位置"插入
    show("在中间插入 apricot 后", v);

    // ── ② 查看 ────────────────────────
    cout << "v[1]        = " << v[1] << endl;         // 不检查
    cout << "v.at(1)     = " << v.at(1) << endl;      // 会检查(越界抛异常)
    cout << "v.front()   = " << v.front() << endl;    // 第一个
    cout << "v.back()    = " << v.back() << endl;     // 最后一个
    cout << "v.size()    = " << v.size() << endl;     // 实际个数
    cout << "v.empty()   = " << (v.empty() ? "空" : "非空") << endl;

    // ── ③ 改 ──────────────────────────
    v[0] = "APPLE";                   // 直接改
    v.at(1) = "APRICOT";              // at 也能改
    show("改过前两个之后", v);

    // ── ④ 删 ──────────────────────────
    v.pop_back();                     // 删最后一个
    show("pop_back 后", v);

    v.erase(v.begin());               // 删第一个(★ 参数是迭代器,不是下标!)
    show("erase(begin()) 后", v);

    v.erase(v.begin(), v.begin() + 1);   // 删区间 [begin, begin+1)
    show("erase 一个区间后", v);

    // ── ⑤ 遍历的三种写法 ───────────────
    cout << "① 下标:   ";
    for (size_t i = 0; i < v.size(); i++) cout << v[i] << " ";
    cout << endl;

    cout << "② 迭代器: ";
    for (vector<string>::iterator it = v.begin(); it != v.end(); ++it) cout << *it << " ";
    cout << endl;

    cout << "③ 范围 for:";
    for (const string &s : v) cout << s << " ";       // ★ 引用避免复制,const 避免误改
    cout << endl;

    // ── ⑥ 清空与判空 ──────────────────
    v.clear();                        // 清空所有元素(capacity 不一定变小)
    cout << "clear 后 size=" << v.size() << ", empty()=" << (v.empty() ? "true" : "false") << endl;
    return 0;
}

看到什么算成功:

三次 push_back 后 [size=3, cap=3]:apple banana cherry 
在中间插入 apricot 后 [size=4, cap=4]:apple apricot banana cherry 
v[1]        = apricot
v.at(1)     = apricot
v.front()   = apple
v.back()    = cherry
v.size()    = 4
v.empty()   = 非空
改过前两个之后 [size=4, cap=4]:APPLE APRICOT banana cherry 
pop_back 后 [size=3, cap=4]:APPLE APRICOT banana 
erase(begin()) 后 [size=2, cap=4]:APRICOT banana 
erase 一个区间后 [size=1, cap=4]:banana 
① 下标:   banana 
② 迭代器: banana 
③ 范围 for:banana 
clear 后 size=0, empty()=true

从输出里读四件事:

现象 说明
size 变、cap 不变(pop_back / erase / clear 之后) 删元素不会立刻还内存——这是设计:还了之后通常马上又要加回来。想主动还,用 vector<int>().swap(v);
erase 的参数是迭代器(v.begin()),不是下标(写 v.erase(0) 编译不过) 这是容器与迭代器分工的第一个例子:凡是"位置",STL 一律用迭代器表示
三种遍历都对,但 size_t 不能省 ① 用 size_t i 避免无符号警告;③ 一定写 const string &s(引用省拷贝、const 防误改)
v.at(1) 和 v[1] 输出一样 但越界时不一样——见下面的"30 秒实验"

四、[] 和 at():这一节的第二条命脉

vector<int> v = {1, 2, 3};

cout << v[10] << endl;        // ❌ 不报错、不检查 —— 读到的是"别人家的内存"(未定义行为)
cout << v.at(10) << endl;     // ✅ 抛异常:terminate called after throwing an instance of
                              //    'std::out_of_range'  what():  vector::_M_range_check

这就是 12.2.5 里"operator[] 故意不检查"的同款设计,也是 std::vector 的真实分工:

用法 检查吗 什么时候用
v[i] 不检查 你已经确认 i 合法(比如 for (i=0; i<v.size(); i++))——追求速度
v.at(i) 检查,越界抛 out_of_range i 来自外部输入、算出来的、心里没底 ——追求安全

为什么设计成两个:"检查"有成本,不该强加给所有人。 循环里那些铁定安全的访问不该白花力气。C++ 的一贯做法就是"给你两把刀,你自己选"——和 [] / at() 一样的还有 list::sort vs std::sort、new vs new (nothrow)。 本教材里没讲异常处理(try/catch),但你现在已经见到了它的身影——at() 越界时"抛"出来的正是那个 out_of_range。12.4 小结欠的第二笔账("出错怎么办")就是这个。 眼下你只要记住:at() 会停下来报错,[] 会默默出错。

五、capacity 的成倍增长:为什么它要"留余量"

vector<int> v;
for (int i = 0; i < 10; i++) {
    cout << "加之前 size=" << v.size() << " cap=" << v.capacity();
    v.push_back(i);
    cout << "  →  加之后 size=" << v.size() << " cap=" << v.capacity() << endl;
}

看到什么算成功(capacities 会跳:1 → 2 → 4 → 8 → 16):

加之前 size=0 cap=0  →  加之后 size=1 cap=1
加之前 size=1 cap=1  →  加之后 size=2 cap=2
加之前 size=2 cap=2  →  加之后 size=3 cap=4
加之前 size=3 cap=4  →  加之后 size=4 cap=4
加之前 size=4 cap=4  →  加之后 size=5 cap=8
...
加之前 size=8 cap=8  →  加之后 size=9 cap=16

为什么要成倍(而不是每次 +1):假设每次只加一格,那么压入 n 个元素要搬 1+2+3+…+n 次,总量是 O(n²);成倍增长则是 O(n)(摊还分析的经典结论)。这就是"留余量"买来的效率。

于是 reserve 的意义就清楚了:

vector<int> v;
v.reserve(1000);            // ★ 直接申请 1000 的容量,一次到位
for (int i = 0; i < 1000; i++) v.push_back(i);   // 后面一次都不搬

两条实战规矩:

场景 做法
已知大概数量(比如读 5000 行日志) 先 reserve(5000)——省掉十几次"申请+搬运+释放"
还要更多内存 用 v.shrink_to_fit()(C++11)请求"把多余容量还回去"——只是请求,标准不保证

一个细节:reserve(10) 改的是 capacity;resize(10) 改的是 size(会真的造出 10 个元素,多余的是零值)。两个字面很像,做的事完全不同,这是最容易看错的一对。

六、动手 Demo:vector 版"学生成绩单"

任务:把第十章那个 Student 类(或下面的简化版)装进 vector,做统计。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

class Student {
private:
    string name;
    double score;
public:
    Student() : name("无名"), score(0) {}                  // ★ 要能默认构造
    Student(const string &n, double s) : name(n), score(s) {}
    double getScore() const { return score; }
    string getName()  const { return name;  }
    void show() const { cout << name << "(" << score << ") "; }
};

int main() {
    vector<Student> cls;
    cls.reserve(10);                        // ★ 提前留位置

    cls.push_back(Student("小明", 88.5));
    cls.push_back(Student("小红", 95.0));
    cls.push_back(Student("小刚", 71.5));
    cls.push_back(Student("小丽", 88.5));

    cout << "全班 " << cls.size() << " 人:";
    for (size_t i = 0; i < cls.size(); i++) cls[i].show();
    cout << endl;

    // ① 总分
    double sum = 0;
    for (size_t i = 0; i < cls.size(); i++) sum += cls[i].getScore();
    cout << "总分 " << sum << ",平均 " << sum / cls.size() << endl;

    // ② 最高分(不用写循环,用 13.3 的算法 + 自己的比较函数)
    Student best = cls[0];
    for (size_t i = 1; i < cls.size(); i++)
        if (cls[i].getScore() > best.getScore()) best = cls[i];   // ★ 对象整体赋值(复制构造)
    cout << "最高分是 " << best.getName() << " " << best.getScore() << endl;

    // ③ 删掉不及格
    for (size_t i = 0; i < cls.size(); ) {                        // ★ 不用 i++
        if (cls[i].getScore() < 60) cls.erase(cls.begin() + i);   // 删掉后 i 不用动
        else ++i;
    }
    cout << "清除不及格后剩 " << cls.size() << " 人:";
    for (const Student &s : cls) s.show();                        // ★ 用引用,不复制对象
    cout << endl;
    return 0;
}

看到什么算成功:

全班 4 人:小明(88.5) 小红(95) 小刚(71.5) 小丽(88.5) 
总分 343.5,平均 85.875
最高分是 小红 95
清除不及格后剩 4 人:小明(88.5) 小红(95) 小刚(71.5) 小丽(88.5) 

三个必须看懂的点:

  1. Student() 默认构造不能少——vector<Student> cls; 在很多操作(如 resize、reserve 之后的部分操作)中需要"造一个空对象"。如果 10.2 那边"写了带参构造就把默认构造弄没了",这里会报 no matching function for call to 'Student::Student()'。(教材里的 Student 类两边都写了,就是为这个。)
  2. best = cls[i]; 是对象整体赋值——走的是 10.2.2 复制构造函数 / 赋值运算符。成员里有指针的话这里就是浅拷贝的雷,好在 string 自己管好了内存。
  3. "边遍历边删除"必须手动管 i——这是 vector 最经典的坑(见误区 4)。

30 秒实验

  1. 越界对比:把 Demo 里的 cls[i] 临时改成 cls.at(99),只改一处,编译运行 → 会看到 terminate called after throwing an instance of 'std::out_of_range'。再换成 cls[99] → 不报错,输出乱码或崩溃。这两张脸要认下来。
  2. reserve 的效果:把 cls.reserve(10); 注释掉,在循环前后各加一句 cout << cls.capacity() << endl; → 看容量怎么跳。
  3. 范围 for 的坑:把最后一个循环改成 for (Student s : cls) s.show();(去掉 &)→ 能编译、结果一样,但每轮都复制了一个 Student 对象。这就是"引用省拷贝"的实战意义(12.1.2 说的"参数只求值一次"的同门)。
  4. resize vs reserve:在开头加 cls.resize(5);(在 reserve 之后),看 size → 从 4 变成 5,而且多出来的一个是"无名(0)"——resize 真的造对象,reserve 只是留地方。

常见误区

  1. "v[i] 和 v.at(i) 一样"——越界时完全不同:[] 未定义行为(乱码/崩溃),at() 抛 out_of_range。来自外部的下标用 at()。
  2. "size() 返回 int"——返回 size_t(无符号)。for (int i = 0; i < v.size(); i++) 会警告;while (i <= v.size() - 1) 在空容器时会因下溢而变成死循环。统一的正确写法:for (size_t i = 0; i < v.size(); i++)。
  3. "erase 传下标"——传迭代器(v.erase(v.begin() + 2) 删第 3 个)。想按值删,得先 find(13.3.1)。
  4. "循环里 erase 就写 i++"——这是最经典的 bug:删掉第 i 个后,后面的元素整体前移,原来的第 i+1 个变成了第 i 个;i++ 会跳过它。正确写法有两种: cpp // 写法一:删了就不动 i for (size_t i = 0; i < v.size(); ) { if (要删) v.erase(v.begin() + i); else ++i; } // 写法二:用 erase 的返回值(返回删除后的下一个位置) for (vector<int>::iterator it = v.begin(); it != v.end(); ) { if (要删) it = v.erase(it); else ++it; } 注:更地道的做法是 13.3.2 的"erase-remove 惯用法",一行搞定。
  5. "clear() 之后就省内存了"——size 归零,但 capacity 通常还在。要真还内存:vector<int>().swap(v);。
  6. "vector<bool> 和普通 vector 一样"——它是标准里唯一被特化的容器:内部按位存(省空间),所以 v[0] 返回的不是 bool& 而是代理对象,&v[0] 拿不到地址、也不能传 bool*。用它存一堆开关可以,别指望它和 vector<int> 行为完全一致。(这正好是 12.3 定制在标准库里的真实例子。)
  7. "直接用 vector<int> v[10]; 当二维数组"——那是"10 个 vector",每个长度可以不同(这其实是好事)。真正的二维要写 vector<vector<int>> v(3, vector<int>(4));(3 行 4 列)。
  8. "存进 vector 里就不用管析构了,随便 new"——vector 只管它自己的内存,不管你 new 出来的指针。vector<int*> v; v.push_back(new int(5)); 之后 v.clear() 不会 delete 那些 int——内存泄漏,就是你自己的责任。要么存对象(推荐),要么存智能指针。
自测三问(点开对答案) 1. **`vector` 和 7.1 的数组比,强在哪、弱在哪?**——强在:**能扩容、知道自己的 `size()`、有 `at()` 检查、能整体赋值/返回、自动管理元素生命周期**。弱在:**多几个字节的开销**,以及**扩容时要搬家**(可以 `reserve` 缓解)。**`[]` 依然不检查**这点和数组一样。 2. **`size()`、`capacity()`、`reserve()`、`resize()` 四者什么关系?**——`size` = 已经装了几个(会参与遍历);`capacity` = 已经申请了多大(内部)。**`reserve(n)` 只改 `capacity`(留地方,不造元素);`resize(n)` 改 `size`(真的造/删元素,新元素取零值)**。删元素后 `capacity` 一般不变。 3. **为什么 `vector` 扩容要"成倍"而不是"+1"?**——如果每次只加一格,压入 n 个要搬 1+2+…+n ≈ n²/2 次,**总代价 O(n²)**;成倍增长时平均每次压入只搬常数次,**总代价 O(n)**。**这就是 `reserve` 值得写的原因**:**知道规模就别让它反复搬家。**

相关笔记