☰
C++中的STL中map用法详解(零基础入门)
2026/10/20 12:37:05 网站建设 项目流程

前言


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'; // true

auto 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'; // 1

int x = m["banana"]; // 插入 banana -> 0,再返回
std::cout << m.size() << '\n'; // 2

try {
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()返回迭代器iterator

insert插入不修改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); // 指向 20
if (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_map



map的难点不在接口数量,而在几个语义细节:operator[]会插入、键是常量、比较器必须严格弱序、删除要使迭代器更新。把这几条记牢,再根据「是否需要有序」在map与unordered_map之间做选择,日常使用就不会出大问题。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询