前言std::map是 C++ 标准库的关联容器,保存「键—值」对,按键有序排列,且每个键唯一。它的典型实现是平衡二叉搜索树(多为红黑树),因此查找、插入、删除都是对数复杂度。对零基础读者来说,最重要的一点是:map不是哈希表,它的迭代顺序按键的排序规则确定,而不是插入顺序,也不是哈希顺序。初学者最容易踩的坑是operator[]。m["missing"]在键不存在时会插入一个默认构造的值,并返回它的引用。很多「只想读一下」的代码因此悄悄改变了容器大小,甚至让后续的遍历多出一个空项。另一个误解是「map比unordered_map慢所以不该用」——有序性本身是功能,需要按序遍历、范围查询、稳定输出时,map的排序正是你要的。本文从类型定义讲起,覆盖插入、访问、遍历、删除、查找与自定义比较器,最后给出与unordered_map的选择建议。示例以 C++17 为基准,C++20 的新成员会单独标注。一、类型与基本性质使用map需要包含<map>。它的模板参数有四个,通常只写前两个:// 适用于 C++17#include <map>#include <string>std::map<std::string, int> score;完整形式是std::map<Key, T, Compare, Allocator>,其中Compare默认是std::less<Key>,Allocator一般不用管。map的元素类型是std::pair<const Key, T>:键是const,值可以修改。这决定了迭代器解引用得到的first不能赋值——it->first = "x"会编译失败,因为改键会破坏树的排序结构。几个基本性质需要记住:性质说明键唯一重复插入同一个键不会产生第二个元素有序按Compare决定的顺序排列,默认升序节点稳定插入、删除其它元素不会使指向已有元素的迭代器失效(被删的除外)键不可改value_type是pair<const Key, T>对数复杂度查找、插入、删除均为 O(log n)map的迭代器是双向迭代器,不支持随机访问,不能写m.begin() + 3。遍历顺序由比较器决定;对std::string键,默认是字典序,大写字母排在小写之前(按字符编码比较),这一点在做大小写不敏感统计时要注意。二、插入与访问插入有几种写法,语义各不相同。insert接受value_type或迭代器区间,返回std::pair<iterator, bool>:second表示是否真的插入,first指向该键对应的元素(无论新插入还是已存在)。用结构化绑定可以一次取出:// 适用于 C++17#include <iostream>#include <map>#include <string>int main() {std::map<std::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的构造函数,语义上也是「不存在才插入」。C++17 新增的try_emplace(key, args...)更精确:只有当键不存在时,才用args构造值;键已存在时不会构造临时对象。insert_or_assign(key, value)则是「存在就赋值,不存在就插入」,适合「更新或新增」的语义。operator[]的行为要单独强调:它要求T可默认构造;键不存在时先插入一个值初始化的T,再返回引用。因此它既能读也能写,但不能用于纯查询。C++11 起还有at(key):键不存在时抛std::out_of_range,不会插入;at有const版本,operator[]没有,所以const std::map只能用at或find访问。// 适用于 C++17#include <iostream>#include <map>#include <stdexcept>#include <string>int main() {std::map<std::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;}如果只是想判断键是否存在,用find(C++20 起也可以用contains),不要用operator[]。count(key)返回 0 或 1,可读性也不错,但对map来说find更直接。操作键不存在时键存在时返回operator[]插入默认值返回引用T&at抛std::out_of_range返回引用T&/const T&find返回end()返回迭代器iteratorinsert插入不修改pair<iterator, bool>try_emplace构造并插入不构造、不修改pair<iterator, bool>insert_or_assign插入赋值pair<iterator, bool>三、遍历、删除与查找遍历map得到的是pair<const Key, T>。C++17 的结构化绑定让代码更清晰:// 适用于 C++17#include <iostream>#include <map>#include <string>int main() {std::map<std::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;}按 C++17 之前的写法是for (const auto& kv : counts),再用kv.first、kv.second。注意结构化绑定默认拷贝还是引用要看写法:const auto& [k, v]是只读引用,auto [k, v]会拷贝一份pair,修改v不会影响容器。删除有三种形式。erase(key)删除指定键,返回删除的元素个数(对map是 0 或 1);erase(iterator)删除迭代器指向的元素,C++11 起返回下一个元素的迭代器;erase(first, last)删除区间。遍历中删除要用返回值更新迭代器:// 适用于 C++17#include <map>#include <string>int main() {std::map<std::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的区间最多包含一个元素。这些接口在「找最接近的键」「按范围遍历」时很有用,而且是对数复杂度,不需要手动扫描。// 适用于 C++17#include <iostream>#include <map>#include <string>int main() {std::map<int, 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。// 适用于 C++17#include <iostream>#include <map>#include <string>struct 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::map<std::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,但需要在构造时传入类型(C++20 之前 lambda 的类型不能直接作为模板参数写出,通常用decltype或函数对象)。C++20 起 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或 C++20 的contains;operator[]会插入默认值。❌ 在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::pair<const Key, T>,键不可修改访问operator[]会插入;纯查询用find/at/contains插入insert不覆盖;try_emplace延迟构造;insert_or_assign覆盖删除erase返回迭代器,遍历删除要用返回值更新比较器必须严格弱序,不能写<=选择需要顺序与范围查询用map,只需精确查找可考虑unordered_mapmap的难点不在接口数量,而在几个语义细节:operator[]会插入、键是常量、比较器必须严格弱序、删除要使迭代器更新。把这几条记牢,再根据「是否需要有序」在map与unordered_map之间做选择,日常使用就不会出大问题。
前言std::map是 C++ 标准库的关联容器,保存「键—值」对,按键有序排列,且每个键唯一。它的典型实现是平衡二叉搜索树(多为红黑树),因此查找、插入、删除都是对数复杂度。对零基础读者来说,最重要的一点是:map不是哈希表,它的迭代顺序按键的排序规则确定,而不是插入顺序,也不是哈希顺序。初学者最容易踩的坑是operator[]。m["missing"]在键不存在时会插入一个默认构造的值,并返回它的引用。很多「只想读一下」的代码因此悄悄改变了容器大小,甚至让后续的遍历多出一个空项。另一个误解是「map比unordered_map慢所以不该用」——有序性本身是功能,需要按序遍历、范围查询、稳定输出时,map的排序正是你要的。本文从类型定义讲起,覆盖插入、访问、遍历、删除、查找与自定义比较器,最后给出与unordered_map的选择建议。示例以 C++17 为基准,C++20 的新成员会单独标注。一、类型与基本性质使用map需要包含<map>。它的模板参数有四个,通常只写前两个:// 适用于 C++17#include <map>#include <string>std::map<std::string, int> score;完整形式是std::map<Key, T, Compare, Allocator>,其中Compare默认是std::less<Key>,Allocator一般不用管。map的元素类型是std::pair<const Key, T>:键是const,值可以修改。这决定了迭代器解引用得到的first不能赋值——it->first = "x"会编译失败,因为改键会破坏树的排序结构。几个基本性质需要记住:性质说明键唯一重复插入同一个键不会产生第二个元素有序按Compare决定的顺序排列,默认升序节点稳定插入、删除其它元素不会使指向已有元素的迭代器失效(被删的除外)键不可改value_type是pair<const Key, T>对数复杂度查找、插入、删除均为 O(log n)map的迭代器是双向迭代器,不支持随机访问,不能写m.begin() + 3。遍历顺序由比较器决定;对std::string键,默认是字典序,大写字母排在小写之前(按字符编码比较),这一点在做大小写不敏感统计时要注意。二、插入与访问插入有几种写法,语义各不相同。insert接受value_type或迭代器区间,返回std::pair<iterator, bool>:second表示是否真的插入,first指向该键对应的元素(无论新插入还是已存在)。用结构化绑定可以一次取出:// 适用于 C++17#include <iostream>#include <map>#include <string>int main() {std::map<std::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的构造函数,语义上也是「不存在才插入」。C++17 新增的try_emplace(key, args...)更精确:只有当键不存在时,才用args构造值;键已存在时不会构造临时对象。insert_or_assign(key, value)则是「存在就赋值,不存在就插入」,适合「更新或新增」的语义。operator[]的行为要单独强调:它要求T可默认构造;键不存在时先插入一个值初始化的T,再返回引用。因此它既能读也能写,但不能用于纯查询。C++11 起还有at(key):键不存在时抛std::out_of_range,不会插入;at有const版本,operator[]没有,所以const std::map只能用at或find访问。// 适用于 C++17#include <iostream>#include <map>#include <stdexcept>#include <string>int main() {std::map<std::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;}如果只是想判断键是否存在,用find(C++20 起也可以用contains),不要用operator[]。count(key)返回 0 或 1,可读性也不错,但对map来说find更直接。操作键不存在时键存在时返回operator[]插入默认值返回引用T&at抛std::out_of_range返回引用T&/const T&find返回end()返回迭代器iteratorinsert插入不修改pair<iterator, bool>try_emplace构造并插入不构造、不修改pair<iterator, bool>insert_or_assign插入赋值pair<iterator, bool>三、遍历、删除与查找遍历map得到的是pair<const Key, T>。C++17 的结构化绑定让代码更清晰:// 适用于 C++17#include <iostream>#include <map>#include <string>int main() {std::map<std::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;}按 C++17 之前的写法是for (const auto& kv : counts),再用kv.first、kv.second。注意结构化绑定默认拷贝还是引用要看写法:const auto& [k, v]是只读引用,auto [k, v]会拷贝一份pair,修改v不会影响容器。删除有三种形式。erase(key)删除指定键,返回删除的元素个数(对map是 0 或 1);erase(iterator)删除迭代器指向的元素,C++11 起返回下一个元素的迭代器;erase(first, last)删除区间。遍历中删除要用返回值更新迭代器:// 适用于 C++17#include <map>#include <string>int main() {std::map<std::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的区间最多包含一个元素。这些接口在「找最接近的键」「按范围遍历」时很有用,而且是对数复杂度,不需要手动扫描。// 适用于 C++17#include <iostream>#include <map>#include <string>int main() {std::map<int, 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。// 适用于 C++17#include <iostream>#include <map>#include <string>struct 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::map<std::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,但需要在构造时传入类型(C++20 之前 lambda 的类型不能直接作为模板参数写出,通常用decltype或函数对象)。C++20 起 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或 C++20 的contains;operator[]会插入默认值。❌ 在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::pair<const Key, T>,键不可修改访问operator[]会插入;纯查询用find/at/contains插入insert不覆盖;try_emplace延迟构造;insert_or_assign覆盖删除erase返回迭代器,遍历删除要用返回值更新比较器必须严格弱序,不能写<=选择需要顺序与范围查询用map,只需精确查找可考虑unordered_mapmap的难点不在接口数量,而在几个语义细节:operator[]会插入、键是常量、比较器必须严格弱序、删除要使迭代器更新。把这几条记牢,再根据「是否需要有序」在map与unordered_map之间做选择,日常使用就不会出大问题。