【C++ 面试真题】22. 聊聊 C++ 的 pair、tuple 与 optional

【C++ 面试真题】聊聊 C++ 的 pair、tuple 与 optional

“函数想返回两个值怎么办”“查找失败怎么表达’没有’”——这些都是值语义打包问题:把若干个值(或"没有值")装进一个对象,类型系统替你把关。背得出"pair 存两个、tuple 存多个"只是及格,真考你的是"解包三代写法、返回值怎么选、optional 和指针表达可空差在哪"。本文把打包三兄弟一次讲透。


一、开场:值语义的打包三兄弟

❓ pair、tuple、optional 各是什么?

✅ 都是"把值装进一个对象"的工具,分工不同:

工具引入回答的问题
pairC++98固定装 2 个
tuple[C++11]任意个
optional<T>[C++17]装一个值或没有

回答思路:先报"两个、多个、一个或没有"这条线,再点一句共性——它们都是值语义:装的是内容的拷贝,自动析构、不涉及所有权。面试官会顺着问解包和选型。


二、pair:最简单的二元组

❓ pair 怎么用?

✅ 两个公开成员 firstsecond

pair<string, int> p{"tom", 18};
cout << p.first;    // "tom"
cout << p.second;   // 18

auto q = make_pair("tom", 18);
// 类型自动推导

pair 最常见的身影在容器里——map 的每个元素就是 pair<const Key, Value>

💡 pair 自带字典序比较:先比 first,相等再比 second。正因如此,sort 一排 pair、map 按 key 排序才"天然成立";vector<pair<int,string>> 一句 sort 就是二级排序。


三、tuple:任意个值

❓ tuple 怎么取值?

✅ 按下标或按类型取,编译期确定:

tuple<string, int, double> t{
    "tom", 18, 1.75};

cout << get<0>(t);        // "tom"
cout << get<string>(t);   // 按类型(不重复才行)

元信息编译期可查:tuple_size 是个数、tuple_element 是类型。两个配套工具:

// apply:tuple 展开成函数实参
apply(func, make_tuple(1, 2.0));
// tuple_cat:多个 tuple 拼接
auto big = tuple_cat(
    make_tuple(1), make_tuple(2.5));

⚠️ tuple 没有成员名——只能 get<i> 按位取,可读性靠纪律。这是它和"具名 struct"最大的差距,也是后面选型的关键。


四、解包三代演进:tie → 结构化绑定

❓ 接收这些返回值有哪些写法?

✅ 三代演进,越写越顺:

第一代:按位置取——res.firstget<0>(res),下标满天飞。

第二代:tie 解包 [C++11]——把已有变量按引用"接"出来:

string name; int score;
tie(name, score) =
    make_pair("tom", 18);
// 不想接的用 ignore 跳过
tie(ignore, score) =
    make_pair("tom", 18);

第三代:结构化绑定 [C++17]——声明即解包,现在的主流:

auto [name, score] =
    make_pair("tom", 18);
// pair、tuple、struct、数组都支持

💡 结构化绑定还能绑引用auto& [k, v] : m 遍历 map 直接改值)和常量引用const auto& 只读零拷贝)——tie 做不到这么自然。tie 仅剩"复用旧变量 + ignore"的场景。


五、optional:值或空

❓ optional 解决什么问题?

✅ 解决"用魔法值表示没有"的痛点。过去"查找失败返回 -1""指针为空表示没有"全靠约定;optional 把"可能没有"变成类型的一部分:

optional<int> findAge(
    const string& name);

auto r = findAge("tom");
if (r.has_value()) {
    cout << *r;        // 有值才解引用
}
cout << r.value_or(0);  // 没有就兜底
// 语法糖:if (r) 有值、if (!r) 空

⚠️ 对空 optional 解引用 * 是未定义行为——value() 才会抛异常。判空后才能解,或直接 value_or

和指针表达可空的区别(高频):指针同时暗示"指向别处的借用、要不要 delete",语义混浊;optional 是纯值语义——装的就是自己的拷贝,不涉内存管理、不存在悬空:

int* findAge(...);        // 借用?拥有?猜
optional<int> findAge(...); // 有值/没有,仅此而已

它还是延迟初始化的占位——类成员"构造时还定不下来",之后 emplace 原地构造:

struct Conn {
    optional<Socket> sock_;
    void connect() {
        sock_.emplace(fd);
    }
};

六、多返回值:返回什么?

❓ 函数要返回"多个值"或"可能失败的结果",怎么选?

✅ 一张表定案:

需求首选
语义明显的二元组(key-value)pair
三个以上、字段含义不直观具名 struct
“成功才有结果”optional<T>
“成功/失败 + 原因 + 结果”struct 或 expected [C++23]
// ❌ 老写法:魔法值 + 出参
bool findAge(const string&,
             int& out);

// ✅ 新写法:类型即文档
optional<int> findAge(
    const string& name);

🎯 选型一句话:二元组 pair、多元 struct、可空 optional——结构化绑定对三者通吃,名字白拿不亏。


七、实用细节

❓ 还有哪些值得知道的细节?

✅ 四个高频细节:

① 结构化绑定配 if 初始化——"插入 + 判重"一行写完:

if (auto [it, ok] =
        m.insert({k, v}); ok) {
    // 插入成功才进来
}

② 逐成员构造与比较——pair/tuple 的拷贝、移动、比较都是逐成员进行,成员是 string 就有 string 的代价,没有魔法。

③ optional 的大小——就是 T 的存储加一个标志(常打进 padding),零堆分配sizeof(optional<int>) 通常 8 字节,性能和传指针同量级。

④ 聚合初始化直接花括号构造,map<string, int> m{{"a",1},{"b",2}} 里的每个 {"a",1} 就是 pair。


八、面试高频追问

❓ Q1:结构化绑定和 tie 的本质区别?

✅ tie 是"给已有变量赋值的表达式",要求变量先存在;结构化绑定是"声明新名字的语句",类型自动推导,支持引用与 const 修饰。日常新代码一律结构化绑定,tie 只剩"复用旧变量 + ignore 跳过"的场景。

❓ Q2:get<i>(t) 里的 i 能是运行期变量吗?

✅ 不能。i编译期常量——tuple 的类型在编译期定死,元素位置是类型信息的一部分。tuple 没有 for 循环,"遍历"要靠 index_sequence 或 apply 折叠表达式做编译期展开。

❓ Q3:optional<int> 和返回指针,性能上有差吗?

✅ 没有实质差别——内联存储、传参走寄存器,同量级。选 optional 不是为了性能,是为了语义干净:调用方一眼知道"可能没有",且不暗示借用或需要释放。

❓ Q4:optional<T&>(装引用)合法吗?

✅ 不合法,标准明确不支持。要"可空引用"就 optional<reference_wrapper<T>>,或者老实用指针表达借用。

❓ Q5:make_pair / make_tuple 存在的意义?

✅ 两个字:推导。免写模板参数,类型按实参推导兜底。[C++17] 有了类模板实参推导(CTAD),pair p{"tom", 18} 直接写也行,make 系列使命小了一半。

❓ Q6:optional 和 pair<bool, T> 都能表达"成功+结果",哪个好?

✅ optional 好。pair<bool,T> 里 T 永远存在(失败时还是得构造一个假值),调用方还容易搞混 first/second;optional 的空状态是内置语义,value_or、判空一气呵成。[C++23] 的 expected<T, E> 是它的完全体——还能带上失败原因。

❓ Q7:为什么 map 用 pair 而不是专门的 Entry 类型?

✅ 历史与通用性:pair 提供了"二元组 + 全套比较 + 结构化绑定"的零成本通用件,map/set 全系列复用它做 value_type;代价是 first/second 无语义——所以遍历推荐用结构化绑定起好名字。


九、总结速查表

考点一句话结论
三兄弟pair 两个 / tuple 多个 / optional 一个或没有
访问.first/.second;get<i>();* / value_or
解包演进下标 → tie → 结构化绑定
tie 独有复用旧变量 + ignore
比较字典序,sort 直接可用
空解引用* 是 UB,value() 抛异常
vs 指针值语义,无借用/拥有歧义
延迟初始化成员 optional + emplace
多返回值二元 pair / 多元 struct / 可空 optional
optional 大小内联 + 标志,零堆分配

一句话回顾

pair 装两个、tuple 装多个、optional 装一个或没有——共性是值语义:自动拷贝、自动析构、不涉所有权;解包用 [C++17] 结构化绑定(tie 只剩复用旧变量);表达"可能没有"用 optional 而不是指针或魔法值,空解引用是 UB、value_or 兜底——返回值选型一句话:二元 pair、多元 struct、可空 optional

如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们继续标准库篇——聊聊 std::any 与 std::variant:开集的类型擦除和闭集的"多选一"怎么选、visit 与 overload 惯用法怎么写,敬请关注 👋

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码工许师傅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值