CC++ & Algorithm

optional可选值类型:一个可能装着东西也可能空着的“盲盒”

较难2
语言版本:通用
概述:学习C++中的optional,它用来表示一个值可能存在也可能不存在,就像抽盲盒一样,避免使用特殊值或指针。

optional:像开盲盒一样处理“可能有也可能没有”的值

你有没有抽过盲盒?打开之前,你不知道里面是隐藏款、普通款,还是空盒(虽然实际上很少有空盒,但可以想象一下)。编程里也经常遇到类似的情景:一个函数可能返回一个有效结果,也可能因为某些原因什么也没返回。比如老师查一个学生的成绩:如果学号存在,返回分数;如果不存在,总不能返回一个“奇怪的分数”吧?以前的做法是返回 -1 表示“不存在”,但 -1 自己就是个合法分数(比如考试被倒扣分)。还有人用指针,让 nullptr 表示不存在,但指针要小心内存管理,很麻烦。

C++17 引入了一个非常优雅的工具——optional(可选值类型)。它就像一个魔法盒子:盒子要么装着你要的宝贝(比如一个整数、一个字符串),要么空空的。你可以先检查盒子有没有东西再打开,安全又方便。Python 虽然没有内置 optional,但用 None 和类型注解也能达到类似效果。今天我们就来把这个“盲盒”讲明白。


1. 生活里的“可选值”

  • 查分数:老师拿学号去数据库查成绩,如果学号错了,查不到,应该返回“没有分数”,而不是乱给一个数。
  • 零花钱:妈妈今天给你零花钱吗?可能给 10 元,可能不给(0 元?但“不给”和“给 0 元”不一样哦)。
  • 游戏装备:打怪随机掉落装备,可能掉一把剑,也可能什么都不掉。
  • 排队买奶茶:你前面还排着多少人?可能 5 个,可能一个都没有(0 个),但“没有排队”和“排队人数为 0”是有区别的——前者是还没开始排队,后者是已经排好了但前面没人。

这些情况都需要一种类型来表示“有值”或“没有值”,而不能简单用 0-1空字符串 来混用,因为那些值本身也可能是合法的。


2. C++ 中的 optional:原理与使用

optional 定义在头文件 <optional> 中。它包装一个类型 T 的对象,并额外保存一个布尔标志,表示这个对象是否有效。你可以把它想象成一个盒子,盒子本身附带一个指示灯:亮表示里面有值,灭表示空的。

2.1 状态与创建

optional<T> 有两种状态:

  • 有值:里面放着一个 T 类型的对象。
  • :里面什么也没有,用 std::nullopt 表示(一个特殊的空状态标记)。

创建方式

#include <optional>
using namespace std;

optional<int> box1;            // 默认构造,空盒子
optional<int> box2 = 42;       // 用值 42 初始化,装着一个整数
optional<int> box3 = nullopt;  // 显式声明空盒子
optional<string> box4("hello"); // 装着一个字符串 "hello"

2.2 检查是否有值

方法一:has_value()

if (box2.has_value()) {
    cout << "盒子有值!" << endl;
}

方法二:隐式转换为 bool(直接用在 if 条件里)

if (box2) {   // 等价于 box2.has_value()
    cout << "盒子有值!" << endl;
}

生活比喻:就像你摇一下盲盒,听到里面有声响(有值),或者感觉轻飘飘(空盒)。

2.3 取出值

方法一:value()

  • 如果有值,返回该值的拷贝。
  • 如果空,抛出 std::bad_optional_access 异常。
int val = box2.value();  // 安全,因为 box2 有值
// int val2 = box1.value(); // 危险!box1 为空,会抛出异常

方法二:解引用 *(类似指针)

  • 如果有值,返回该值的引用(可以直接修改)。
  • 如果空,未定义行为(通常程序崩溃或出现奇怪错误)。
*box2 = 100;          // 修改盒子里的值
cout << *box2;        // 输出 100
// *box1; // 空盒子解引用,非常危险!

方法三:value_or(default)(最安全)

  • 如果有值,返回该值;如果空,返回你指定的默认值。
int v = box1.value_or(0);  // box1 空,返回 0
int w = box2.value_or(0);  // box2 有值 42,返回 42

生活比喻value_or(default) 就像跟妈妈说“如果今天没给我零花钱,就当给了 5 块”——反正你至少能拿到 5 块。

2.4 修改和重置

emplace(...):直接在盒子内部构造一个值(避免额外拷贝)。

optional<pair<string, int>> info;
info.emplace("小明", 95);          // 直接创建 pair
cout << info->first << " " << info->second; // 输出"小明 95"

reset():清空盒子,把它变成空盒子。

box2.reset();                      // box2 现在空了

2.5 更多小例子

场景:用 optional 表示一个可能缺失的配置项
比如游戏中是否开启音效:

optional<bool> sound_enabled;   // 默认空,表示“还没设置”
sound_enabled = true;            // 用户开启了
if (sound_enabled.value_or(false)) { // 如果没有设置,默认不开启
    playSound();
}

注意optional<bool> 是一个特殊情况——它有四种状态:空、truefalse。条件判断 if (opt) 只区分空与不空,不能区分 truefalse。所以如果你要检查 opt 的值是不是 false,需要先判断有值,再取出来比较。


3. 完整示例:学生成绩查询系统

下面用一个完整的 C++ 程序展示 optional 的常见用法。代码模拟从数据库中查询学生成绩,以及安全的除法。

#include <iostream>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

using namespace std;

// 创建模拟数据库,学号 -> 成绩
unordered_map<int, int> createDatabase() {
    unordered_map<int, int> db;
    db[1001] = 95;
    db[1002] = 88;
    db[1003] = 92;
    db[1004] = 76;
    return db;
}

// 根据学号查找成绩,返回 optional<int>
optional<int> findScore(const unordered_map<int, int>& db, int id) {
    auto it = db.find(id);
    if (it != db.end()) {
        return it->second;   // 找到,返回包含成绩的 optional
    } else {
        return nullopt;      // 没找到,返回空 optional
    }
}

// 安全除法:除数不能为 0
optional<double> safeDivide(double a, double b) {
    if (b == 0.0) {
        return nullopt;
    }
    return a / b;
}

int main() {
    // ===== 1. 创建 optional 的多种方式 =====
    optional<int> o1;              // 空
    optional<int> o2 = 100;        // 包含 100
    optional<int> o3 = nullopt;    // 显式空
    optional<string> o4("hello");  // 包含字符串 "hello"

    // ===== 2. 判断是否有值 =====
    cout << boolalpha;  // 让 true/false 显示为文字
    cout << "o1 has value: " << o1.has_value() << endl;  // false
    cout << "o2 has value: " << o2.has_value() << endl;  // true

    // ===== 3. 访问值(先检查) =====
    if (o2.has_value()) {
        cout << "o2 value: " << o2.value() << endl;     // 100
        cout << "o2 value (operator*): " << *o2 << endl; // 100
    }

    // ===== 4. value_or 提供默认值 =====
    int v1 = o1.value_or(0);   // o1 为空,返回 0
    int v2 = o2.value_or(0);   // o2 有值 100,返回 100
    cout << "v1: " << v1 << ", v2: " << v2 << endl;

    // ===== 5. 在条件中直接使用 optional =====
    if (o2) {
        cout << "o2 is truthy\n";  // 输出(o2 有值)
    }
    if (!o1) {
        cout << "o1 is empty\n";   // 输出(o1 空)
    }

    // ===== 6. 成绩查询系统 =====
    auto db = createDatabase();
    int testIds[] = {1001, 1005, 1003};
    for (int id : testIds) {
        optional<int> score = findScore(db, id);
        if (score) {
            cout << "学号 " << id << " 的成绩: " << *score << endl;
        } else {
            cout << "学号 " << id << " 不存在。\n";
        }
    }

    // ===== 7. 安全除法 =====
    double x = 10.0, y = 0.0;
    optional<double> result = safeDivide(x, y);
    if (result) {
        cout << x << " / " << y << " = " << *result << endl;
    } else {
        cout << "除数不能为0!\n";
    }

    // ===== 8. emplace 就地构造 =====
    optional<pair<string, int>> opPair;
    opPair.emplace("Alice", 95);   // 直接在内部构造 pair
    if (opPair) {
        cout << "pair: " << opPair->first << " " << opPair->second << endl;
    }

    // ===== 9. reset 清空 =====
    o2.reset();
    cout << "after reset, o2 has value: " << o2.has_value() << endl;  // false

    // ===== 10. optional 中存储容器(vector) =====
    optional<vector<int>> optVec;
    // optVec 目前为空(没有 vector)
    if (!optVec) {
        optVec = vector<int>{1,2,3};  // 赋予一个含三个元素的 vector
    }
    // 注意:即使 vector 为空,optVec 仍然有值(它只是一个空的容器)
    if (optVec && optVec->size() > 0) {
        cout << "vector 的第一个元素: " << (*optVec)[0] << endl;
    }

    return 0;
}

运行结果(部分):

o1 has value: false
o2 has value: true
o2 value: 100
o2 value (operator*): 100
v1: 0, v2: 100
o2 is truthy
o1 is empty
学号 1001 的成绩: 95
学号 1005 不存在。
学号 1003 的成绩: 92
除数不能为0!
pair: Alice 95
after reset, o2 has value: false
vector 的第一个元素: 1

关键点

  • findScore 返回 optional<int>,调用方用 if(score) 判断有没有成绩。
  • value_or 让你不用写 if-else 就可以安全拿到值或者一个备用值。
  • emplace 比直接赋值效率高(避免了临时对象的拷贝)。
  • 容器(比如 vector)也可以放进 optional,但要注意“容器为空”和“optional 为空”是两码事。

4. Python 里的等价做法

Python 没有内置 optional 类型,但通常用 None 表示“没有值”。配合 typing.OptionalUnion 类型注解,能很好地表达可选性。

from typing import Optional, Dict, List, Union

# 模拟数据库
def create_database() -> Dict[int, int]:
    return {1001: 95, 1002: 88, 1003: 92, 1004: 76}

# 查找成绩,返回 Optional[int] 等价于 int 或 None
def find_score(db: Dict[int, int], id: int) -> Optional[int]:
    return db.get(id)  # dict.get 如果键不存在返回 None

# 安全除法
def safe_divide(a: float, b: float) -> Optional[float]:
    if b == 0.0:
        return None
    return a / b

# 测试
if __name__ == "__main__":
    # 1. 创建可选值
    o1 = None          # 空
    o2 = 100           # 有值
    o4 = "hello"

    # 2. 判断是否有值(用 is None / is not None)
    print("o1 is None:", o1 is None)    # True
    print("o2 is not None:", o2 is not None)  # True

    # 3. 访问值,先判断
    if o2 is not None:
        print(f"o2 value: {o2}")

    # 4. 提供默认值(Python 没有 value_or,用条件表达式或 or 操作符)
    v1 = o1 if o1 is not None else 0   # 或者 v1 = o1 or 0
    v2 = o2 if o2 is not None else 0
    print(f"v1: {v1}, v2: {v2}")

    # 5. 在条件中判断(None 被视为 False)
    if o2:
        print("o2 is truthy")  # 输出(o2 = 100 非零)

    # 6. 成绩查询
    db = create_database()
    test_ids = [1001, 1005, 1003]
    for id in test_ids:
        score = find_score(db, id)
        if score is not None:
            print(f"学号 {id} 的成绩: {score}")
        else:
            print(f"学号 {id} 不存在.")

    # 7. 安全除法
    x, y = 10.0, 0.0
    result = safe_divide(x, y)
    if result is not None:
        print(f"{x} / {y} = {result}")
    else:
        print("除数不能为0!")

    # 8. 使用 Union 类型注解(Python 3.10+ 可以用 X | None)
    def maybe_return() -> Union[int, None]:
        import random
        if random.random() > 0.5:
            return 42
        else:
            return None

    # 9. 用 dict.get 指定默认值(但注意 -1 可能是合法分数)
    score2 = db.get(1005, -1)   # 返回 -1 作为默认值
    print(f"学号1005成绩(使用默认值): {score2}")

    # 10. 使用 list 作为“可选容器”的替代(不如 None 清晰)
    opt_list: Optional[List[int]] = None
    if opt_list is None:
        opt_list = [1, 2, 3]
    if opt_list is not None and len(opt_list) > 0:
        print(f"列表第一个元素: {opt_list[0]}")

Python 要点

  • None 表示“没有值”,检查时用 x is Nonex is not None,不要用 ==(因为 None 是单例,但 == 可能被自定义类重载出问题)。
  • 类型注解 Optional[int] 等价于 Union[int, None],帮助代码理解。
  • 在条件中 if x: 可以把 None0、空字符串、空容器都当成 False所以如果 0 或空字符串是合法值,不能直接用 if x:,必须用 if x is not None:
  • Python 没有 value_or 的现成函数,但可以用 x or default 或三元表达式,注意 0 or default 会返回 default,小心误用。

5. 新手最容易犯的错误

错误 1:解引用空 optional(C++)

optional<int> emptyOpt;
cout << *emptyOpt;   // 未定义行为!程序可能崩溃

解决方法:总是先检查 has_value() 或直接用 value_or(default)

错误 2:在 Python 中混淆 None0、空字符串

def get_age(name):
    return name_to_age.get(name)  # 可能返回 None

age = get_age("小明")
if not age:   # 如果 age 是 0,这里也会为 False,但 0 可能是合法年龄
    print("没找到年龄")

解决方法:明确用 if age is None: 判断。

错误 3:把 optional 当作指针(C++)

optional<int> opt = 5;
int* p = &opt;           // 这是错的!opt 不是指针
int& ref = *opt;         // 正确,*opt 返回引用

optional 是一个值对象(不是指针),它直接存储数据(或用一个布尔标志标记空)。不要用 &opt 来获取内部数据的地址,应该用 &(*opt)&opt.value()

错误 4:optional<bool> 的陷阱

optional<bool> flag;      // 空
flag = false;             // 有值,值为 false
if (flag) {               // 这里会进入吗?会!因为 flag 有值(哪怕值是 false)
    cout << "flag is true? 不,是 false 但有值" << endl;
}

解释optional<bool>if (flag) 只检查“是否包含值”,不检查值的内容。要检查值的内容,必须先取值再比较:

if (flag.has_value() && flag.value() == false) { ... }

错误 5:在 value_or 里放一个函数调用(C++)

int expensiveCompute() { /* 很耗时的计算 */ return 42; }
optional<int> opt;
int result = opt.value_or(expensiveCompute()); // 即使 opt 有值,expensiveCompute 也会被调用!

解决方法value_or 的参数是表达式,会先求值。如果想避免不必要的计算,可以用三元表达式或先判断:

int result = opt.has_value() ? *opt : expensiveCompute();

6. 什么时候用 optional?什么时候不用?

用 optional 的场景

  • 函数可能失败但不想抛出异常(比如查找、解析、网络请求)。
  • 成员变量可能没有值(比如游戏里的可选技能槽位)。
  • 避免使用“哨兵值”(比如 -1INT_MAXnullptr 等容易误解的值)。
  • 需要区分“空状态”和“合法零值”(比如成绩为 0 和没成绩)。

不用 optional 的场景

  • 当“空”本身就是一个合理值,且可以表示为一个特殊值时(比如 string 可以用空字符串表示“没有名字”,如果空字符串不是合法名字的话)。
  • 性能极端敏感时:optional 比原始类型多一个 bool 标志,且当类型很大时(如 vector),拷贝会有开销。但通常这点开销可以忽略。
  • 需要区分“错误原因”时:optional 只能表示“有没有值”,不能告诉你为什么没有。这时候用 std::expected(C++23)或 std::variant 更合适。

7. 相关知识点

  • std::variant:表示“多选一”,可以存储多种不同类型的值,比 optional 更灵活(比如函数返回成功或不同类型的错误)。
  • std::any:类型擦除,可以存储任意类型的一个值,但运行时才检查类型,没有“空”状态的概念(你可以用 has_value ?不,any.has_value() 判断是否为空)。
  • std::expected(C++23):类似 optional,但可以同时保存一个错误信息,用于处理可能会失败但需要知道失败原因的场景。
  • Python 的 dataclassesTypedDict:可以搭配 Optional 字段表示可选属性。

掌握了 optional,你就能优雅地处理“有或无”的情况。下一个进阶知识点是 variant,它让你处理“多选一”的灵活情况,就像魔术师的帽子里可能变出鸽子、兔子或者花束——具体是什么,打开才知道!


小练习

  1. 写一个函数 tryParseInt,接收一个 string,如果字符串是合法的整数就返回 optional<int>,否则返回空。
  2. optional 实现一个小型缓存:先查缓存,如果缓存中有值直接返回,否则计算并存入缓存。

例题精讲

1单选题

关于std::optional,以下说法正确的是?

Astd::optional<T> 必须初始化才能使用
B访问一个不包含值的 optional 的 value() 方法会抛出 std::bad_optional_access 异常
Cstd::optional 可以存储引用类型
Doptional 的 has_value() 方法返回 true 表示该对象当前没有值
2判断题

使用 std::optional 时,可以直接通过解引用运算符 * 访问其包含的值,即使 optional 为空也不会导致未定义行为。

3填空题
补全以下函数,使其将字符串转换为整数,如果转换失败则返回空的optional:

std::optional<int> parseInteger(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return ___;
    }
}
4单选题

下列哪个程序片段正确地创建了一个包含值10的 std::optional<int>?

Astd::optional<int> opt = 10;
Bstd::optional<int> opt(10);
Cstd::optional<int> opt = std::make_optional(10);
D以上都正确
5填空题
假设有一个 std::optional<std::string> maybeName,请补全下面的代码,使得如果maybeName包含值则输出该值,否则输出"unknown":

std::cout << (maybeName.has_value() ? maybeName.___ : "unknown");