在 C++ 中,普通数组可以用来保存一组相同类型的数据,但它有一个明显的限制:数组的长度一旦确定下来,就不能在运行过程中直接增加或删除元素。而在实际开发中,我们经常会遇到 “一开始还不知道最终要存多少个数据” 的情况。
为了解决这个问题,C++ 标准库提供了 vector 容器。你可以先把 “容器” 简单理解成专门用来装一组数据的工具。说白了,vector 就是一个 “长度可以自动伸缩的动态数组”。
C++ vector 是什么?
在 C++ 标准库中,vector 的完整名称是 std::vector,其中 vector 是 “向量” 的意思。如果我们想要在程序中使用 vector,必须先在代码开头通过 #include <vector> 引入对应的头文件。
语法:
std::vector<数据类型> 容器名;
std::vector<数据类型> 容器名 = {元素1, 元素2, ...};说明:
在 std::vector<数据类型> 中,std 是前面学过的命名空间,后面的尖括号 <> 用来指定容器内部保存什么类型的元素。比如 std::vector<int> 用来保存整数,std::vector<std::string> 用来保存字符串。
需要注意的是,同一个 vector 内部所有元素的类型必须一致,不能把整数和字符串混装在同一个容器里。如果在创建时暂时还没有数据,我们可以使用第 1 种语法定义一个空容器。如果已经知道初始数据,可以使用第 2 种语法把元素写在花括号 {} 中,元素之间用英文逗号(,)隔开。
关于赋初值的更多写法,我们会在下一节 “C++ vector 初始化” 中展开介绍。创建好 vector 之后,我们不仅可以像普通数组那样按下标读取数据,还能随时调用 push_back() 函数往末尾追加新元素。下面我们来看一个简单的例子。
示例 1:定义并使用 vector
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {85, 90, 78};
scores.push_back(95); // 在末尾追加新元素
std::cout << "第 1 个元素:" << scores[0] << '\n';
std::cout << "第 4 个元素:" << scores[3] << '\n';
return 0;
}运行结果如下。
第 1 个元素:85
第 4 个元素:95分析:
在这个例子中,我们先定义了一个名为 scores 的容器,初始放入了 85、90、78 这 3 个整数。随后我们调用 push_back() 函数在末尾追加了一个新元素 95,容器长度自动变成了 4,底层的内存空间完全由 vector 自动管理。
与前面学过的数组一样,vector 中的元素在内存中也是连续存放的,下标同样从 0 开始计算。因此,scores[0] 输出第 1 个元素 85,scores[3] 输出刚追加的第 4 个元素 95。
需要注意的是,vector 里面装的是一组数据,不能像 std::string 字符串那样直接写成 std::cout << scores; 来打印整个容器,必须指定具体的元素位置才能访问。
访问和修改 vector 元素
在 C++ 中,如果我们想要访问或修改 vector 中的单个元素,常用的有以下 3 种方式:
- 下标 []:最常用、最直观的方式,用法与普通数组一致。
- at() 函数:会自动检查下标是否越界,安全性更高。
- front() 和 back() 函数:专门用于直接获取首元素和尾元素。
1. 使用下标 [](推荐)
通过中括号 [] 配合下标,我们可以直接读取或修改指定位置的元素。
语法:
容器名[下标]说明:
与普通数组一样,vector 的下标也是从 0 开始计算的。也就是说,第 1 个元素的下标是 0,第 2 个元素的下标是 1,以此类推。
示例 2:使用下标读写元素
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {85, 90, 78};
std::cout << scores[0] << '\n'; // 读取第 1 个元素
std::cout << scores[1] << '\n'; // 读取第 2 个元素
scores[0] = 95; // 修改第 1 个元素
std::cout << "修改后:" << scores[0] << '\n';
return 0;
}运行结果如下。
85
90
修改后:95分析:
scores[0] 表示读取第 1 个元素 85,scores[1] 表示读取第 2 个元素 90。接着我们通过 scores[0] = 95; 给第 1 个元素重新赋值,新值 95 就会覆盖原来的 85。
此外,普通数组创建后不能整体重新赋值,而 vector 支持直接使用 = 换上一组新数据。比如执行 scores = {92, 96}; 后,容器中原本的 3 个元素就会整体替换为 92 和 96,元素个数也会变成 2。
这里小伙伴们要特别注意:下标 [] 只能用来读写容器中 “已经存在” 的元素,千万不要对空容器直接使用下标赋值。下面这种写法就是错误的。
std::vector<int> list;
list[0] = 85;因为 list 刚定义出来时是空的,里面还没有任何元素,直接执行 list[0] = 85; 会导致下标越界。如果我们想要往空容器里添加新数据,应该调用 push_back()。同理,当容器只有 3 个元素时,最大有效下标为 2,访问 scores[3] 也会超出有效范围。
2. 使用 at() 函数
除了使用中括号 [],vector 还提供了 at() 成员函数来访问或修改指定位置的元素。
语法:
容器名.at(下标)说明:
在下标没有越界的情况下,at() 与 [] 的作用完全相同。比如 scores.at(2) 和 scores[2] 都是访问第 3 个元素。
示例 3:使用 at() 读写元素
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {85, 90, 78};
std::cout << scores.at(2) << '\n';
scores.at(2) = 88;
std::cout << "修改后:" << scores.at(2) << '\n';
return 0;
}运行结果如下。
78
修改后:88分析:
scores.at(2) 表示访问下标为 2 的元素,也就是第 3 个元素 78。随后我们通过 scores.at(2) = 88; 将它的值修改为 88。
到这里可能有些小伙伴会问:“既然 [] 和 at() 都能读写元素,那它们之间有什么区别呢?”
两者的区别主要在于是否检查 “下标越界”。使用 [] 时不会检查下标范围,一旦越界就会产生未定义行为,程序可能读出随机乱码或直接崩溃。而 at() 会自动检查下标是否有效,一旦越界就会立即报错并终止程序。请看下面例子。
示例 4:at() 越界自动报错
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {85, 90, 78};
std::cout << scores.at(3) << '\n'; // 下标 3 超出范围
return 0;
}运行结果如下。
(报错)terminate called after throwing an instance of 'std::out_of_range'分析:
从运行结果可以看出,程序直接报错并停止了运行。这是因为 scores 只有 3 个元素,最大有效下标为 2。执行 scores.at(3) 时触发了表示 “超出有效范围” 的 std::out_of_range 错误,能帮我们快速定位出错的位置。
在实际开发中,如果我们能够确定下标在有效范围内,优先使用简洁高效的 [] 即可。如果下标来自用户输入或外部计算,不太容易提前保证范围,使用 at() 会更加稳妥。
3. 使用 front() 和 back() 函数
在平时写代码时,我们经常需要直接读取或修改 vector 的首尾元素。特别是获取最后一个元素时,如果每次都写成 容器名[容器名.size() - 1] 会比较繁琐,这时可以直接使用 front() 和 back() 成员函数。
语法:
容器名.front()
容器名.back()说明:
从英文意思上很容易记住:front 表示 “最前面”,front() 的作用等价于 容器名[0]。back 表示 “最后面”,back() 专门用来访问最后一个元素。
示例 5:读写首尾元素
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {85, 90, 78};
std::cout << "首元素:" << scores.front() << '\n';
std::cout << "尾元素:" << scores.back() << '\n';
scores.back() = 100; // 修改最后一个元素
std::cout << "修改后的尾元素:" << scores.back() << '\n';
return 0;
}运行结果如下。
首元素:85
尾元素:78
修改后的尾元素:100分析:
scores.front() 直接取出排在最前面的 85,scores.back() 直接取出排在最后面的 78,不需要我们手动去计算最后一个元素的下标。同时,我们也可以直接通过 scores.back() = 100; 修改最后一个元素的值。
注意 调用 front() 和 back() 之前,需要确保 vector 中至少包含一个元素。如果对空的 vector 调用这两个函数,会导致越界访问,程序可能读出乱码或直接崩溃。
获取元素数量与判空
在使用 vector 的过程中,我们经常需要查看容器里当前存了多少个元素,或者判断容器是不是空的。对此,vector 提供了 size() 和 empty() 这两个常用的成员函数。
语法:
容器名.size()
容器名.empty()说明:
这两个函数都不会修改容器本身的数据,具体作用如下:
- size():返回当前
vector中实际存放的元素个数。 - empty():判断当前
vector是否为空。如果没有任何元素则返回true,否则返回false。
示例 6:获取元素数量与判空
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {85, 90, 78};
std::vector<int> backup;
std::cout << "scores 的元素数量:" << scores.size() << '\n';
std::cout << "backup 的元素数量:" << backup.size() << '\n';
if (scores.empty()) {
std::cout << "scores 为空\n";
} else {
std::cout << "scores 不为空\n";
}
if (backup.empty()) {
std::cout << "backup 为空\n";
}
return 0;
}运行结果如下。
scores 的元素数量:3
backup 的元素数量:0
scores 不为空
backup 为空分析:
在这个例子中,scores 包含 3 个整数,因此 scores.size() 返回 3。同时 scores.empty() 返回 false,程序进入 else 分支输出 “scores 不为空”。
而 backup 在定义时没有放入任何数据,属于空容器,所以 backup.size() 返回 0。此时 backup.empty() 返回 true,程序输出 “backup 为空”。
当我们只是想判断一个 vector 是否为空时,对于上面这个例子来说,下面 2 种方式是等价的。
// 方式 1(推荐)
if (backup.empty()) {
std::cout << "backup 为空\n";
}
// 方式 2
if (backup.size() == 0) {
std::cout << "backup 为空\n";
}在实际开发中,我们更推荐使用方式 1,因为直接调用 empty() 的代码意图更加直观清晰。
常见问题
1. 既然 vector 这么方便,那前面学的普通数组是不是就没用了?实际开发中应该怎么选?
普通数组并没有过时,不过在现代 C++ 开发中,大多数场景下我们都优先选择 std::vector。只要元素数量在运行前无法确定,或者后续需要动态增删元素,使用 std::vector 会省心很多,也不容易出错。
只有当元素数量从一开始就完全固定(比如一年 12 个月),或者需要对接传统的 C 语言函数时,我们才会使用普通数组。
