在编写程序时,我们经常会遇到 “两个数据成对出现” 的情况,比如学生的姓名和成绩、商品的名称和价格,或者平面上的横坐标和纵坐标。如果只是临时把这两个值组合在一起,专门去定义一个结构体未免有些繁琐。
在 C++ 中,标准库专门提供了轻便的 std::pair 来解决这类需求。pair 在英文中是 “一对” 的意思,说白了就是把两个数据打包在一起的组合类型。如果需要打包 3 个及以上的数据,则可以使用元组 std::tuple。
C++ pair 的创建与访问
std::pair 定义在 <utility> 头文件中,内部保存的两个值既可以是相同类型,也可以是不同类型。
语法:
#include <utility>
std::pair<类型1, 类型2> 变量名 = {值1, 值2};说明:
尖括号 <> 里依次指定第 1 个值和第 2 个值的数据类型。如果只声明变量而不写初始值,比如 std::pair<std::string, int> p;,std::pair 会自动对两个成员执行零初始化,把字符串设为空串 ""、整数设为 0,不会产生随机垃圾值。
对于 std::pair 来说,内部的两个值有着固定的公开成员名称:第 1 个值叫做 first,第 2 个值叫做 second。需要注意,它们都是普通的成员变量而不是成员函数,读写时直接写 p.first 和 p.second 即可,不要像调用函数那样在后面加小括号。
示例 1:创建与修改 pair
#include <iostream>
#include <string>
#include <utility>
int main() {
std::pair<std::string, int> stu = {"Jack", 95};
std::cout << stu.first << ": " << stu.second << '\n';
stu.second = 98; // 修改第 2 个成员的值
std::cout << stu.first << ": " << stu.second << '\n';
return 0;
}运行结果如下。
Jack: 95
Jack: 98分析:
在这个例子中,stu 是一个 std::pair<std::string, int> 类型的变量,其中 stu.first 保存字符串 "Jack",stu.second 保存整数 95。只要这个 pair 变量没有加 const 修饰,我们就可以随时通过 stu.second = 98; 修改里面的值。
除了直接写明类型并使用花括号初始化之外,<utility> 头文件还提供了一个辅助函数 std::make_pair(),可以根据传入的值自动推导类型。对于这个例子来说,下面 2 种创建方式是等价的:
// 方式 1(推荐)
std::pair<std::string, int> stu = {"Jack", 95};
// 方式 2
auto stu = std::make_pair(std::string("Jack"), 95);细心的小伙伴可能会发现,方式 2 在传入 "Jack" 时专门包了一层 std::string("Jack")。这是因为双引号字符串字面量的默认类型是 const char*,如果直接写 std::make_pair("Jack", 95),第 1 个成员就会被推导成字符指针而不是 std::string。
而在方式 1 中,尖括号里已经明确写出了目标类型,花括号里的 "Jack" 会自动转为 std::string。这种写法既简短又不容易推错类型,因此在实际开发中我们更推荐使用方式 1。
比较两个 pair 的大小
只要 pair 内部保存的元素本身支持比较运算,两个同类型的 pair 就可以直接使用 ==、!=、<、> 等运算符进行比较。它们的比较规则如下:
- 判断相等(== 与 !=):只有当两个
pair的first和second分别相等时,==的结果才为true,否则为false。 - 比较大小(<、> 等):先比较两个
pair的first,只要first分出了大小,就不再看后面的second。只有当first相等时,才会继续比较second。
下面我们来看一个简单的例子。
示例 2:比较两个 pair 的大小
#include <iostream>
#include <utility>
int main() {
std::pair<int, int> p1 = {10, 20};
std::pair<int, int> p2 = {10, 35};
std::pair<int, int> p3 = {15, 5};
if (p1 < p2) {
std::cout << "p1 小于 p2\n";
}
if (p1 < p3) {
std::cout << "p1 小于 p3\n";
}
return 0;
}运行结果如下。
p1 小于 p2
p1 小于 p3分析:
判断 p1 < p2 时,程序先对比两者的 first,发现都是 10。接着继续对比 second,因为 20 小于 35,所以判定 p1 < p2 成立。
而在判断 p1 < p3 时,虽然 p3.second 的 5 比 p1.second 的 20 小,但第一步对比 first 时 10 已经小于 15 了,此时直接得出 p1 < p3 成立的结果,根本不会再去比后面的 second。对于这一点,小伙伴们别搞混了。
注意 只有当两个 std::pair 对应位置的成员类型能够相互比较时,才能使用 == 或 < 进行判断。如果一个是 std::pair<int, int>,另一个是 std::pair<std::string, int>,直接比较会在编译阶段报错。
C++ pair 的常见应用
在实际开发中,std::pair 最常见的使用场景主要有 2 种:一种是 “让函数一次返回两个结果”,另一种是 “配合容器保存成对的数据”。
1. 让函数一次返回两个结果
前面学习函数返回值时我们知道,一条 return 语句一次只能返回一个值。如果一个函数需要同时带回两个计算结果,比如做整数除法时既想算出 “商”、又想算出 “余数”,使用 std::pair 打包返回就非常合适。
示例 3:函数返回两个计算结果
#include <iostream>
#include <utility>
// 同时返回商和余数
std::pair<int, int> divide(int a, int b) {
return {a / b, a % b};
}
int main() {
std::pair<int, int> res = divide(17, 5);
std::cout << "商:" << res.first << '\n';
std::cout << "余数:" << res.second << '\n';
return 0;
}运行结果如下。
商:3
余数:2分析:
因为 divide() 函数开头已经写明了返回类型为 std::pair<int, int>,所以函数体内的 return {a / b, a % b}; 可以直接使用花括号把商 3 和余数 2 打包返回。在 main() 中接收返回值后,通过 res.first 和 res.second 就能分别取出这两个结果。
2. 配合 vector 保存成对数据
当我们手头有一批成对出现的数据需要按顺序存放时,可以把 std::pair 作为元素类型放进 std::vector 中,比如写成 std::vector<std::pair<std::string, int>>。后面学习 std::map 映射容器时,里面的每一组键值对本质上也是一个 std::pair。
示例 4:在 vector 中保存 pair
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::vector<std::pair<std::string, int>> items = {
{"apple", 5},
{"banana", 3}
};
items.push_back({"orange", 8}); // 向尾部追加一组数据
for (const auto& item : items) {
std::cout << item.first << ": " << item.second << '\n';
}
return 0;
}运行结果如下。
apple: 5
banana: 3
orange: 8分析:
items 是一个存放 std::pair<std::string, int> 元素的动态数组。初始化和调用 push_back() 时,像 {"orange", 8} 这样的每一对内层花括号都会自动构造出一个 pair 元素,通过 item.first 和 item.second 即可分别读取名称与数量。
另外可以看到,这里范围 for 循环写成了 const auto& item。因为每个元素的完整类型名比较长,配合前面学过的 auto 关键字写成 const auto&,既能自动推导类型,又能避免每次循环都额外拷贝一份 pair。
