资讯动态

C++中的STL中map用法详解(零基础入门)

发布时间:2026/10/20 12:37:21 来源:尧图企业网站定制
前言std::map是 C 标准库的关联容器保存「键—值」对按键有序排列且每个键唯一。它的典型实现是平衡二叉搜索树多为红黑树因此查找、插入、删除都是对数复杂度。对零基础读者来说最重要的一点是map不是哈希表它的迭代顺序按键的排序规则确定而不是插入顺序也不是哈希顺序。初学者最容易踩的坑是operator[]。m[missing]在键不存在时会插入一个默认构造的值并返回它的引用。很多「只想读一下」的代码因此悄悄改变了容器大小甚至让后续的遍历多出一个空项。另一个误解是「map比unordered_map慢所以不该用」——有序性本身是功能需要按序遍历、范围查询、稳定输出时map的排序正是你要的。本文从类型定义讲起覆盖插入、访问、遍历、删除、查找与自定义比较器最后给出与unordered_map的选择建议。示例以 C17 为基准C20 的新成员会单独标注。一、类型与基本性质使用map需要包含map。它的模板参数有四个通常只写前两个// 适用于 C17#include map#include stringstd::mapstd::string, int score;完整形式是std::mapKey, T, Compare, Allocator其中Compare默认是std::lessKeyAllocator一般不用管。map的元素类型是std::pairconst Key, T键是const值可以修改。这决定了迭代器解引用得到的first不能赋值——it-first x会编译失败因为改键会破坏树的排序结构。几个基本性质需要记住性质说明键唯一重复插入同一个键不会产生第二个元素有序按Compare决定的顺序排列默认升序节点稳定插入、删除其它元素不会使指向已有元素的迭代器失效被删的除外键不可改value_type是pairconst Key, T对数复杂度查找、插入、删除均为 O(log n)map的迭代器是双向迭代器不支持随机访问不能写m.begin() 3。遍历顺序由比较器决定对std::string键默认是字典序大写字母排在小写之前按字符编码比较这一点在做大小写不敏感统计时要注意。二、插入与访问插入有几种写法语义各不相同。insert接受value_type或迭代器区间返回std::pairiterator, boolsecond表示是否真的插入first指向该键对应的元素无论新插入还是已存在。用结构化绑定可以一次取出// 适用于 C17#include iostream#include map#include stringint main() {std::mapstd::string, int stock;auto r1 stock.insert({apple, 10});std::cout std::boolalpha r1.second \n; // trueauto r2 stock.insert({apple, 99});std::cout r2.second \n; // false键已存在std::cout r2.first-second \n; // 10保留原值return 0;}emplace把参数转发给value_type的构造函数语义上也是「不存在才插入」。C17 新增的try_emplace(key, args...)更精确只有当键不存在时才用args构造值键已存在时不会构造临时对象。insert_or_assign(key, value)则是「存在就赋值不存在就插入」适合「更新或新增」的语义。operator[]的行为要单独强调它要求T可默认构造键不存在时先插入一个值初始化的T再返回引用。因此它既能读也能写但不能用于纯查询。C11 起还有at(key)键不存在时抛std::out_of_range不会插入at有const版本operator[]没有所以const std::map只能用at或find访问。// 适用于 C17#include iostream#include map#include stdexcept#include stringint main() {std::mapstd::string, int m;m[apple] 1; // 插入并赋值m[apple] 2; // 已存在直接赋值std::cout m.size() \n; // 1int x m[banana]; // 插入 banana - 0再返回std::cout m.size() \n; // 2try {std::cout m.at(cherry) \n; // 抛 out_of_range} catch (const std::out_of_range e) {std::cout no such key: e.what() \n;}auto it m.find(apple);if (it ! m.end()) {std::cout it-second \n;}return 0;}如果只是想判断键是否存在用findC20 起也可以用contains不要用operator[]。count(key)返回 0 或 1可读性也不错但对map来说find更直接。操作键不存在时键存在时返回operator[]插入默认值返回引用Tat抛std::out_of_range返回引用T/const Tfind返回end()返回迭代器iteratorinsert插入不修改pairiterator, booltry_emplace构造并插入不构造、不修改pairiterator, boolinsert_or_assign插入赋值pairiterator, bool三、遍历、删除与查找遍历map得到的是pairconst Key, T。C17 的结构化绑定让代码更清晰// 适用于 C17#include iostream#include map#include stringint main() {std::mapstd::string, int counts{{apple, 3}, {banana, 5}};for (const auto [word, count] : counts) {std::cout word count \n;}for (auto kv : counts) {kv.second 1; // 值可以改键不能改}return 0;}按 C17 之前的写法是for (const auto kv : counts)再用kv.first、kv.second。注意结构化绑定默认拷贝还是引用要看写法const auto [k, v]是只读引用auto [k, v]会拷贝一份pair修改v不会影响容器。删除有三种形式。erase(key)删除指定键返回删除的元素个数对map是 0 或 1erase(iterator)删除迭代器指向的元素C11 起返回下一个元素的迭代器erase(first, last)删除区间。遍历中删除要用返回值更新迭代器// 适用于 C17#include map#include stringint main() {std::mapstd::string, int m{{a, 1}, {b, 2}, {c, 3}};for (auto it m.begin(); it ! m.end(); ) {if (it-second 2) {it m.erase(it); // 用返回值继续} else {it;}}return 0;}不要写成m.erase(it); it;因为it已经失效。map的删除只会使指向被删元素的迭代器失效其它迭代器仍然有效这是它与vector的重要区别。查找相关成员包括find、count、lower_bound、upper_bound、equal_range。lower_bound(key)返回第一个不小于key的元素upper_bound(key)返回第一个大于key的元素equal_range返回这两者组成的区间。对map而言因为键唯一equal_range的区间最多包含一个元素。这些接口在「找最接近的键」「按范围遍历」时很有用而且是对数复杂度不需要手动扫描。// 适用于 C17#include iostream#include map#include stringint main() {std::mapint, std::string m{{10, ten}, {20, twenty}, {30, thirty}};auto it m.lower_bound(15); // 指向 20if (it ! m.end()) {std::cout it-first it-second \n;}auto range m.equal_range(20);for (auto p range.first; p ! range.second; p) {std::cout p-first p-second \n;}return 0;}四、自定义比较器与容器选择map的排序规则由第三个模板参数决定。自定义比较器必须满足严格弱序strict weak ordering对任意元素comp(a, a)必须为假且传递性成立。写成return a b;是典型错误——它让相等元素也「小于」破坏严格弱序标准规定行为未定义。正确写法是用相等时返回 false。// 适用于 C17#include iostream#include map#include stringstruct ByLength {bool operator()(const std::string a, const std::string b) const {if (a.size() ! b.size()) {return a.size() b.size();}return a b; // 长度相同再按字典序保证严格弱序}};int main() {std::mapstd::string, int, ByLength m;m[bb] 2;m[a] 1;m[ccc] 3;for (const auto kv : m) {std::cout kv.first ;}std::cout \n;return 0;}比较器也可以写成 lambda但需要在构造时传入类型C20 之前 lambda 的类型不能直接作为模板参数写出通常用decltype或函数对象。C20 起 lambda 可以在未求值语境中使用写法更方便但可移植性要看编译器支持。map与unordered_map的选择取决于需求维度std::mapstd::unordered_map底层结构平衡二叉搜索树哈希表元素顺序按键排序不保证查找/插入/删除O(log n)平均 O(1)最坏 O(n)范围查询支持lower_bound等不支持键的要求可比较严格弱序可哈希且可相等比较迭代器稳定性插入删除不影响其它元素扩容rehash使迭代器失效适用场景需要有序、范围查询、稳定遍历只做精确查找、追求平均性能如果键类型没有现成的std::hash特化unordered_map还需要自己提供哈希函数map只需要。对小型容器map的常数开销可能比哈希表更小但具体差异要在实际数据上测量不能凭印象下结论。常见坑点❌ 用m[key]判断键是否存在。 ✅ 用find或 C20 的containsoperator[]会插入默认值。❌ 在const std::map上用operator[]读取。 ✅ 用at()或find()operator[]不是const成员。❌ 通过迭代器修改it-first。 ✅ 键是const不能改需要改键就删除旧元素再插入新键。❌ 自定义比较器写成a b。 ✅ 用严格弱序相等时返回 false通常写a b并在需要时补充次级比较。❌m.erase(it); it;。 ✅it m.erase(it);删除分支不再自增。❌ 认为insert会覆盖已存在的值。 ✅insert不覆盖要覆盖用insert_or_assign或m[key] value。❌ 用map保存对象地址作为键期待按对象内容排序。 ✅ 默认比较的是指针地址需要按内容排序就自定义比较器或改用值类型作键。❌ 依赖unordered_map的遍历顺序做输出。 ✅ 需要稳定顺序时排序后再输出或直接用map。总结主题关键结论本质有序、键唯一的关联容器典型为平衡树元素类型std::pairconst Key, T键不可修改访问operator[]会插入纯查询用find/at/contains插入insert不覆盖try_emplace延迟构造insert_or_assign覆盖删除erase返回迭代器遍历删除要用返回值更新比较器必须严格弱序不能写选择需要顺序与范围查询用map只需精确查找可考虑unordered_mapmap的难点不在接口数量而在几个语义细节operator[]会插入、键是常量、比较器必须严格弱序、删除要使迭代器更新。把这几条记牢再根据「是否需要有序」在map与unordered_map之间做选择日常使用就不会出大问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑