C++ map

在实际开发中,很多数据天然是成对关联的,比如 “商品编号 → 价格”、“学生姓名 → 成绩”、“英文单词 → 中文释义”。面对这类数据,我们关心的不是 “排在第几个下标位置的是谁”,而是拿着左边的 “键(key)”,快速查出它右边对应的 “值(value)”。

在 C++ 中,std::map 就是专门用来保存这种 “键值对” 的有序关联容器。map 在英文中是 “映射” 的意思,用法类似于 Python 中的字典。

std::map 就相当于一本英汉词典:左边的单词是 key,右边的中文释义是 value。每一个 key 在 map 中都是唯一的,而且容器会自动按照 key 的大小比较规则把所有数据排好序,如下图所示。

C++ std::map 键值对映射与自动排序机制

C++ map 的创建与遍历

使用 std::map 需要先引入 <map> 头文件,容器里存放的每一个元素都是一组 “键 → 值” 配对。

语法:

#include <map>

std::map<键类型, 值类型> 变量名 = {
    {键1, 值1},
    {键2, 值2},
    ...
};

说明:

尖括号 <> 里需要指定两个类型参数:第 1 个是 key 的类型,第 2 个是 value 的类型。初始化时,每一组键值对都用一对内层花括号 {key, value} 括起来,不写初始值则会创建一个空的 map。std::map 的常用操作如下表所示。

std::map 常用操作
操作写法 功能说明
m[key] 访问或修改 key 对应的 value,如果 key 不存在则自动创建
m.at(key) 访问已存在的 key 对应的 value,如果不存在则抛出异常
m.insert({k, v}) 插入一组键值对,如果 key 已存在则保留原值
m.erase(key) 直接按键名删除对应的整组键值对
m.contains(key) 判断指定的 key 是否存在,返回 true 或 false
m.find(key) 查找 key 所在的位置,找不到则返回 m.end()
m.clear() 清空容器中的所有键值对
m.size() 返回当前键值对的个数
m.empty() 判断容器是否为空,为空返回 true

示例 1:创建并遍历 map

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> scores = {
        {"Tony", 85},
        {"Lucy", 92},
        {"Jack", 88}
    };

    for (const auto& item : scores) {
        std::cout << item.first << ": " << item.second << '\n';
    }
    return 0;
}

运行结果如下。

Jack: 88
Lucy: 92
Tony: 85

分析:

在这个例子中,我们定义了一个名为 scores 的 std::map 容器,将学生姓名作为 key,将考试成绩作为 value。使用范围 for 循环遍历 scores 时,有 2 点需要重点掌握:

  • 每个元素本质上是一个 pair:每次取出的 item 都是一组打包好的键值对,也就是前面学过的 std::pair。其中 item.first 表示键 key,item.second 表示对应的值 value。
  • 自动按 key 升序排列:虽然初始化时写的顺序是 "Tony"、"Lucy"、"Jack",但最终打印出来的顺序却是 Jack、Lucy、Tony。这是因为 std::map 默认按照键 key 的字典序从小到大排列,既不是按插入先后排,也不是按后面的成绩高低排。

另外,由于每一组键值对的完整类型名比较长,遍历时直接写成 const auto& item 可以让编译器自动推导类型,同时避免每次循环都额外拷贝一份数据。

提示 在 std::map 中,左边的键用来决定排序位置,因此 item.first 是只读的,不能原地修改。如果在遍历时需要修改右边的值,我们只需要去掉 const 写成 for (auto& item : scores),再修改 item.second 即可。

使用 [] 与 at() 访问和修改元素

在日常操作 std::map 时,使用频率最高的是中括号运算符 []。不过它的作用规则和普通数组的数字下标有很大区别,我们需要把修改、新增与读取的边界分清楚。

1. 使用 [] 修改与添加元素

在 map 中,中括号 [] 里面放的不是数字下标 0、1、2,而是具体的键 key。

语法:

映射名[key] = value;

说明:

m[key] = value; 具有 “有则修改、无则新增” 的双重作用。如果这个 key 在 map 里已经存在,就会直接修改它对应的 value。如果这个 key 在 map 里还不存在,容器就会自动新建这个 key,并把等号右边的 value 保存进去。

示例 2:修改与添加键值对

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> scores = {
        {"Jack", 88},
        {"Lucy", 92}
    };

    scores["Jack"] = 95;    // 修改已有键的值
    scores["Tony"] = 85;    // 自动新增不存在的键

    for (const auto& item : scores) {
        std::cout << item.first << ": " << item.second << '\n';
    }
    return 0;
}

运行结果如下。

Jack: 95
Lucy: 92
Tony: 85

分析:

执行 scores["Jack"] = 95; 时,因为 "Jack" 原来就在 scores 里,所以会把他的成绩从 88 覆盖修改为 95。而 "Tony" 原来不在容器中,因此 scores["Tony"] = 85; 会自动往容器里新增一组 {"Tony", 85}。

2. 使用 [] 读取元素的注意事项

既然写 m[key] = value; 在找不到 key 时会自动新建元素,那如果我们不写等号赋值,只是单纯写一句 std::cout << stock["orange"]; 想看看 "orange" 的数量是多少,会发生什么呢?

这正是初学者最容易踩坑的地方:只要对 map 使用了 stock["orange"],一旦发现 "orange" 不存在,容器就会立即把这个新键插入进去。对于 int 类型的值,它会自动赋默认初值 0 并返回出来。

示例 3:对比 [] 与 at() 读取

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock = {
        {"apple", 5},
        {"banana", 3}
    };

    std::cout << "读取前个数:" << stock.size() << '\n';

    // 键不存在时,[] 会自动插入新元素
    std::cout << "orange 数量:" << stock["orange"] << '\n';
    std::cout << "读取后个数:" << stock.size() << '\n';

    // 安全读取已存在的键
    std::cout << "apple 数量:" << stock.at("apple") << '\n';
    return 0;
}

运行结果如下。

读取前个数:2
orange 数量:0
读取后个数:3
apple 数量:5

分析:

从运行结果可以看出,在执行 stock["orange"] 之前,stock 里只有两样水果,元素个数为 2。执行 stock["orange"] 时,虽然本意只是想查看有没有橘子,但因为使用了 [],容器自动插入了 {"orange", 0},导致元素个数变成了 3。

如果我们确定某个 key 已经存在、只想安全地读取或修改它的值,可以调用 stock.at("apple")。当传入的 key 不存在时,at() 会直接抛出越界异常报错,而不会悄悄往容器里新建元素。

按照上面 [] 的规则,下面这段代码最终输出的 scores.size() 是 3 而不是 1,小伙伴们可以思考一下为什么:

std::map<std::string, int> scores = {{"Jack", 88}};
int a = scores["Lucy"];
int b = scores["Tony"];
std::cout << scores.size() << '\n';

注意 如果只是想查询某个 key 在不在 map 中,千万不要直接写 m[key]。我们应该使用后面介绍的 contains() 或 find() 来查询,避免因为一次读取操作意外往容器里塞入多余数据。

插入与删除元素:insert() 和 erase()

除了使用 [] 之外,std::map 也提供了 insert() 和 erase() 成员函数,用来插入和删除键值对。

语法:

映射名.insert({key, value});
映射名.erase(key);

说明:

在实际开发中,我们要分清 m.insert({k, v}) 与 m[k] = v 的区别。如果希望直接写入或覆盖旧值,可以使用 m[k] = v。如果只希望在键不存在时填入初始值、不想覆盖已有的数据,则应该调用 insert()。需要删除元素时,直接调用 m.erase(key) 即可把对应的整组键值对删掉。

示例 4:插入与删除键值对

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock = {
        {"apple", 5},
        {"banana", 3}
    };

    stock.insert({"pear", 6});      // 插入新键
    stock.insert({"apple", 99});    // 插入已存在的键

    std::cout << "apple 数量:" << stock.at("apple") << '\n';

    stock.erase("banana");          // 按键名删除元素
    std::cout << "剩余种数:" << stock.size() << '\n';
    return 0;
}

运行结果如下。

apple 数量:5
剩余种数:2

分析:

上面代码中,stock.insert({"pear", 6}); 成功加入了新元素。而 "apple" 原来就已经存在且数量为 5,所以执行 stock.insert({"apple", 99}); 时会被自动忽略,数量依然保持为 5。我们可以把这两种写入方式放在一起对比:

// 方式 1
stock["apple"] = 99;

// 方式 2
stock.insert({"apple", 99});

其中,方式 1 使用 [] 赋值,当 "apple" 已存在时会直接把旧值覆盖为 99。方式 2 调用 insert() 插入,当 "apple" 已存在时会保留原来的旧值 5。

最后调用的 stock.erase("banana"); 会把整组 {"banana", 3} 从容器中移除。如果传入的 key 原本就不存在,erase(key) 也会安全跳过而不会报错。

查找元素:contains() 和 find()

如果我们想在不修改容器的前提下查询某个 key 是否存在,或者找到该 key 后读取它的值,可以调用 contains() 和 find() 成员函数。

语法:

映射名.contains(key);
auto pos = 映射名.find(key);

说明:

如果只是想判断某个 key 在不在容器中,直接调用 contains(key) 即可返回 true 或 false。如果希望找到该元素后顺带读取或修改它的值,可以调用 find(key) 获取指向该键值对的迭代器 pos。一旦没找到目标,find() 就会返回代表末尾边界的 end()。

示例 5:查找 map 中的键值

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock = {
        {"apple", 5},
        {"banana", 3}
    };

    if (!stock.contains("grape")) {
        std::cout << "元素个数:" << stock.size() << '\n';
    }

    auto pos = stock.find("banana");
    if (pos != stock.end()) {
        std::cout << pos->first << ": " << pos->second << '\n';
    }
    return 0;
}

运行结果如下。

元素个数:2
banana: 3

分析:

stock.contains("grape") 返回 false,查询后元素个数依然保持为 2,不会像 stock["grape"] 那样往容器里插入多余数据。接着调用 stock.find("banana") 查找位置,通过 pos != stock.end() 确认不等于末尾边界后,顺利打印出了键名和库存数量。

可能有些小伙伴会觉得奇怪:“为什么示例 1 遍历时写的是点号 item.first,而这里却写成了箭头 pos->first 呢?”

这是因为范围 for 循环里的 item 是键值对对象本身,直接使用点号(.)访问成员即可。而 find() 返回的 pos 是指向该键值对的迭代器,用法跟前面学过的结构体指针非常相似,因此要使用箭头运算符(->)来访问 pos->first 和 pos->second。

此外,如果实际开发中只需要通过 key 快速查改 value、并不要求容器按 key 自动排序,也可以选用无序映射 std::unordered_map。

常见问题

1. std::map 可以按后面的值(value)自动排序吗?

不可以。std::map 固定只按照左边的键(key)来进行自动排序和去重。如果需要按照右边的值排序,比如按考试成绩高低排列,通常先把 map 中的键值对拷贝到 std::vector 动态数组中,再配合 std::sort() 自定义排序规则来处理。

2. 在 std::map 中,两个不同的 key 可以对应相同的 value 吗?

完全可以。std::map 只要求左边的键(key)必须唯一,对右边的值(value)没有任何去重限制。比如 "Jack" 和 "Lucy" 是两个不同的学生姓名,他们的考试成绩完全可以都是 95 分。

上一篇:

下一篇:

给站长反馈

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

邮箱:lvyenet@vip.qq.com

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