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::sortvsstd::sort、newvsnew (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)
三个必须看懂的点:
Student()默认构造不能少——vector<Student> cls;在很多操作(如resize、reserve之后的部分操作)中需要"造一个空对象"。如果 10.2 那边"写了带参构造就把默认构造弄没了",这里会报no matching function for call to 'Student::Student()'。(教材里的Student类两边都写了,就是为这个。)best = cls[i];是对象整体赋值——走的是 10.2.2 复制构造函数 / 赋值运算符。成员里有指针的话这里就是浅拷贝的雷,好在string自己管好了内存。- "边遍历边删除"必须手动管
i——这是vector最经典的坑(见误区 4)。
30 秒实验
- 越界对比:把 Demo 里的
cls[i]临时改成cls.at(99),只改一处,编译运行 → 会看到terminate called after throwing an instance of 'std::out_of_range'。再换成cls[99]→ 不报错,输出乱码或崩溃。这两张脸要认下来。 reserve的效果:把cls.reserve(10);注释掉,在循环前后各加一句cout << cls.capacity() << endl;→ 看容量怎么跳。- 范围 for 的坑:把最后一个循环改成
for (Student s : cls) s.show();(去掉&)→ 能编译、结果一样,但每轮都复制了一个Student对象。这就是"引用省拷贝"的实战意义(12.1.2 说的"参数只求值一次"的同门)。 resizevsreserve:在开头加cls.resize(5);(在reserve之后),看size→ 从 4 变成 5,而且多出来的一个是"无名(0)"——resize真的造对象,reserve只是留地方。
常见误区
- "
v[i]和v.at(i)一样"——越界时完全不同:[]未定义行为(乱码/崩溃),at()抛out_of_range。来自外部的下标用at()。 - "
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++)。 - "
erase传下标"——传迭代器(v.erase(v.begin() + 2)删第 3 个)。想按值删,得先find(13.3.1)。 - "循环里
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 惯用法",一行搞定。 - "
clear()之后就省内存了"——size归零,但capacity通常还在。要真还内存:vector<int>().swap(v);。 - "
vector<bool>和普通 vector 一样"——它是标准里唯一被特化的容器:内部按位存(省空间),所以v[0]返回的不是bool&而是代理对象,&v[0]拿不到地址、也不能传bool*。用它存一堆开关可以,别指望它和vector<int>行为完全一致。(这正好是 12.3 定制在标准库里的真实例子。) - "直接用
vector<int> v[10];当二维数组"——那是"10 个 vector",每个长度可以不同(这其实是好事)。真正的二维要写vector<vector<int>> v(3, vector<int>(4));(3 行 4 列)。 - "存进
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` 值得写的原因**:**知道规模就别让它反复搬家。**- ☐ 下一节(13.1.2 双端队列类模板)