Appearance
📦11. Vector 向量
数组虽然使用方便,但其长度固定的局限性十分突出——一旦定义,无法在运行时改变大小。
vector是 C++ 标准库提供的动态数组容器,可自动管理存储空间。
0. 逻辑一致,但是类型不同
假设要写一个函数,打印一个数的值:
cpp
void printValue(float x) {
cout << "值是:" << x << endl;
}这个函数只能接收 float。如果还想打印 int、double、string应该怎么办呢?
在C++中,我们可以把每种类型都写一个版本:
cpp
void printValue(int x) {
cout << "值是:" << x << endl;
}
void printValue(double x) {
cout << "值是:" << x << endl;
}
void printValue(string x) {
cout << "值是:" << x << endl;
}这样调用方写起来很简洁:printValue(3)、printValue(3.14)、printValue("hello")。
这种"同一个函数名,多个不同参数类型版本"的写法叫函数重载。
在C语言中,没有重载,所以这种情况只能写3个不同的函数名字。
0.2 冗繁的重载
上面的写法有一个问题:代码重复。
如果程序里需要支持 10 种、20 种类型,就得写 10 份、20 份几乎一模一样的函数——函数体完全一样,只是参数类型不同。
有没有办法把"类型"也当作参数传进去?
0.3 把类型也放到参数里
C++提供了一个更方便的写法——使用关键字 template:
cpp
template <typename T>
void printValue(T x) {
cout << "值是:" << x << endl;
}调用的时候,编译器会自动根据参数推断 T 是什么,然后生成对应版本的代码:
cpp
printValue(3); // T 是 int,生成 int 版本
printValue(3.14); // T 是 double,生成 double 版本
printValue("hello"); // T 是 string,生成 string 版本只需要写一份代码,编译器帮忙生成所有类型的版本。
这里的 T 叫作类型参数,它充当具体类型的"占位符"。编译器为每种类型生成一份代码的过程叫模板实例化。
这种用 template<typename T> 定义函数的方式叫函数模板。
例子:求两个数的最大值:
cpp
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
cout << max(3, 5); // int → 5
cout << max(3.14, 2.71); // double → 3.14
cout << max('A', 'B'); // char → 'B'0.4 两个参数类型不同呢?
max(3, 4.5) 传了一个 int 和一个 double,T 应该是什么?
编译器无法确定。这时需要声明两个类型参数:
cpp
template <typename T, typename U>
auto max(T a, U b) {
return (a > b) ? a : b;
}
cout << max(3, 4.5); // T=int, U=double → 4.5
cout << max(2.71, 3); // T=double, U=int → 3.0类型参数可以有多个,用逗号隔开:
cpp
template <typename T, typename U, typename V>auto 让编译器根据 return 自动推导返回类型(C++14 起可用)。
0.5 类也有同样的问题
函数有"写一个,适配任意类型"的需求,类也有。
vector 就是一个例子——它需要能存放 int、double、string 等任意类型的元素。
和函数模板一样,vector 内部也用了 template<typename T>:
vector<int> → 元素类型是 int 的 vector
vector<double> → 元素类型是 double 的 vector尖括号 <> 里的内容就是告诉 vector"你的元素是什么类型"。
实际上上一课学的
string也是模板类(basic_string<char>的别名),只是<>在别名定义里被隐藏了。
1. 数组的局限性
cpp
int scores[5]; // 固定存储 5 个整数
// 若需存储第 6 个数据,数组容量已不足实际编程中,数据量往往在程序运行时才能确定。例如:
- 需要读入 N 个数,但 N 的值由用户输入决定
- 从文件中读取学生成绩,文件中的学生数量未知
- 数据需要动态增长,无法预先估算最大容量
传统数组难以应对这类场景——要么分配过多造成空间浪费,要么分配不足导致数据溢出。
vector 正是为解决这一问题而设计。
2. vector 概述
vector 是 C++ 标准库提供的动态数组容器(亦称向量),定义于 <vector> 头文件中。
cpp
#include <vector>其主要特性如下:
- 动态扩容:元素数量增长时自动分配更多内存
- 长度查询:通过
.size()成员函数获取当前元素个数 - 下标访问:支持
v[0]、v[1]等下标操作,语法与数组一致 - 丰富接口:支持尾部添加、尾部删除、插入、清空等操作
vector本身也是一个类(基于模板实现),因此自带了各类成员函数。
3. 定义与初始化
cpp
#include <vector>
vector<int> v1; // 空的 vector,元素类型为 int
vector<int> v2(5); // 包含 5 个元素,均初始化为 0
vector<int> v3(5, 3); // 包含 5 个元素,均初始化为 3 → [3,3,3,3,3]
vector<int> v4 = {1, 2, 3}; // 列表初始化 → [1,2,3](C++11 起支持)语法格式为 vector<类型>,类型参数置于尖括号 <> 中。
尖括号语法源自模板机制——
vector是一个可适配任意元素类型的容器。vector<double>表示存储double类型的动态数组,vector<string>表示存储string类型的动态数组。
4. 基本操作
4.1 尾部添加 —— push_back()
cpp
vector<int> v;
v.push_back(10); // v = [10]
v.push_back(20); // v = [10, 20]
v.push_back(30); // v = [10, 20, 30]push_back() 在 vector 尾部追加一个新元素。vector 内部自动管理内存分配,当前容量不足时自动扩容。
4.2 元素访问 —— [] 与 .at()
cpp
vector<int> v = {10, 20, 30};
cout << v[0]; // 10,下标操作与数组一致
cout << v[1]; // 20
cout << v[2]; // 30
v[0] = 99; // 可通过下标修改元素.at() 与 [] 功能相似,但额外执行越界检查:
cpp
cout << v[100]; // 越界访问,行为未定义(可能读到垃圾数据或导致崩溃)
cout << v.at(100); // 越界访问,抛出异常(安全性更高)4.3 获取长度 —— .size()
cpp
vector<int> v = {10, 20, 30};
cout << v.size(); // 3
v.push_back(40);
cout << v.size(); // 44.4 删除尾部元素 —— .pop_back()
cpp
vector<int> v = {10, 20, 30};
v.pop_back(); // 删除最后一个元素 → v = [10, 20]
v.pop_back(); // → v = [10]4.5 判空和清空
cpp
vector<int> v = {10, 20, 30};
cout << v.empty(); // false(不为空)
v.clear(); // 清空所有元素
cout << v.empty(); // true
cout << v.size(); // 04.6 遍历 vector
方式一:下标遍历
cpp
vector<int> v = {10, 20, 30, 40, 50};
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
// 输出:10 20 30 40 50方式二:基于范围的 for 循环(C++11)
cpp
for (int x : v) {
cout << x << " ";
}
// 输出:10 20 30 40 50基于范围的 for 循环语法更为简洁,无需处理下标索引,直接遍历每个元素。
5. 常用操作速查表
| 操作 | 写法 | 说明 |
|---|---|---|
| 定义 | vector<int> v; | 空 vector |
| 尾部添加 | v.push_back(x); | 在末尾加一个元素 |
| 尾部删除 | v.pop_back(); | 删除最后一个元素 |
| 访问元素 | v[i] 或 v.at(i) | 按下标访问 |
| 长度 | v.size() | 当前元素个数 |
| 判空 | v.empty() | 空返回 true |
| 清空 | v.clear() | 删除所有元素 |
| 排序 | sort(v.begin(), v.end()); | 需要<algorithm> 头文件 |
TIP
sort(v.begin(), v.end()) 可以对 vector 排序,默认从小到大。需要 #include <algorithm>。
6. vector 与数组对比
| 对比项 | 传统数组int a[N] | vector |
|---|---|---|
| 长度 | 固定,编译时确定 | 动态,运行时可变 |
| 长度获取 | 需通过sizeof 计算 | .size() 直接获取 |
| 越界检测 | 无越界检查(危险) | .at() 执行越界检查 |
| 参数传递 | 退化为指针,长度信息丢失 | 传递 vector 本身,长度信息保留 |
| 新增元素 | 无法动态增加 | push_back() 随时添加 |
| 性能 | 略优(无额外开销) | 基本无差异(现代编译器的优化) |
除嵌入式系统等内存极度受限的场景外,应优先使用 vector 而非传统数组。
7. 例题:统计成绩
题目: 输入 N 个学生的成绩,输出最高分、最低分和平均分。
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cout << "请输入学生人数:";
cin >> n;
vector<int> scores; // 用 vector 存成绩
cout << "请输入 " << n << " 个成绩:";
for (int i = 0; i < n; i++) {
int s;
cin >> s;
scores.push_back(s); // 逐个添加到 vector
}
// 计算最高分、最低分、总分
int max = scores[0];
int min = scores[0];
int sum = 0;
for (int i = 0; i < scores.size(); i++) {
if (scores[i] > max) max = scores[i];
if (scores[i] < min) min = scores[i];
sum += scores[i];
}
double avg = (double)sum / scores.size();
cout << "最高分:" << max << endl;
cout << "最低分:" << min << endl;
cout << "平均分:" << avg << endl;
return 0;
}如果用传统数组,你得先知道 N 才能定义数组大小——而 vector 可以在不知道 N 的情况下先定义好,边读边加。
8. 和 string 的联系
学完 string 又学 vector,你会发现它们有很多相似之处:
| 操作 | string | vector<int> |
|---|---|---|
| 长度 | s.length() / s.size() | v.size() |
| 下标访问 | s[i] | v[i] |
| 尾部添加 | s += 'a' 或 s.push_back('a') | v.push_back(x) |
| 判空 | s.empty() | v.empty() |
| 清空 | s.clear() | v.clear() |
这是 C++ 标准库的设计理念之一:不同类型的容器,操作接口尽量保持一致,这样学起来事半功倍。
后面在 L2 的 STL 章节,你还会学到更多的容器(如
list、map),操作方式都非常相似!
9. 总结
vector是 C++ 的动态数组,长度可以随数据量自动变化- 用
push_back()在尾部添加,用[]或.at()访问 .size()获取当前元素个数- 可以用下标或范围 for 循环遍历
- 优先用 vector,而不是传统数组——除非你有非常特殊的理由
10. 练习题
- 编写一个函数模板
myMin,能返回两个参数中较小的那个。 - 编写一个函数模板
sum,接受两个可能不同类型的参数,返回它们的和。 - 编写一个类模板
Box,能存储任意类型的单个值,并提供getValue()方法返回该值。 - 以下代码能编译通过吗?为什么?
cpp
template <typename T>
T add(T a, T b)
{
return a + b;
}
cout << add(3, 4.5);