C++ vector

在 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 语言函数时,我们才会使用普通数组。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号