map 是"字典":一个键对应一个值(map<键类型, 值类型>),按键自动排序,按键查找 O(log n)。 它是本章最重要、实战用得最多的容器——"人名 → 电话""学号 → 成绩""单词 → 词频"全是它。
三条必须刻进骨头:① m[key] 在键不存在时会"自动建一个默认值",所以用 [] 读会偷偷改容器——只想查、不想改,必须用 find;② 元素是 pair<const K, V>,键是 const,不能改(要改就删了再插);③ 遍历用 it->first / it->second(或 (*it).first)。
13.2 讲过:map = set 再加一个"值"。确实如此——set<string> 的每个元素是一个 string,map<string,int> 的每个元素是一个 pair<const string, int>。
- 那个 pair 就是 12.2.2 的 Pair<T1, T2>(标准库版叫 std::pair),成员直接公开、不封装——因为它没有"不变式"要守(12.2.2 的结论);
- 键是 const 这件事,和 13.2.1 讲的"set 元素不能改" 同一个道理:它是排序依据;
- 这一节会用 [] 做"计数"(cnt[w]++),背后用到的正是 10.2 的默认构造(int() 是 0,string() 是空串)——"值类型必须能默认构造"是 operator[] 的前提。
它也是最有实用价值的一个容器:把 13.2.1 生词本里那种"一个个比"的写法彻底消灭。
一、map 长什么样
map<string, int> m;
键(key,const) 值(value,可改)
┌──────────┬─────────┐
│ "apple" │ 3 │ ← 一个元素 = 一个 pair<const string, int>
│ "banana" │ 2 │
│ "cherry" │ 1 │
└──────────┴─────────┘
按键自动升序排列(按 "apple" < "banana" < "cherry")
set 和 map 的关系,一句话说完:
set<string> |
map<string, int> |
|
|---|---|---|
| 每个元素是什么 | 一个 string |
一个 pair<const string, int> |
| 能不能拿"值" | 没有值 | it->second |
| 插入方式 | s.insert("apple") |
m["apple"] = 3; 或 m.insert(make_pair("apple", 3)); |
| 去重/有序/查找 O(log n) | ✅ | ✅(按键去重、按键排序) |
二、三种插入方式:哪种用在哪
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> m;
// ── 方式① 下标赋值:最方便,但会"偷偷建"(详见下一节)──
m["apple"] = 3;
m["banana"] = 2;
// ── 方式② insert + make_pair:不会覆盖已存在的值 ──
m.insert(make_pair("cherry", 1));
m.insert(make_pair("apple", 99)); // ★ apple 已存在 → 这一句不起作用
// ── 方式③ insert + 花括号(C++11)──
m.insert({"date", 5});
cout << "内容:" << endl;
for (map<string, int>::iterator it = m.begin(); it != m.end(); ++it)
cout << " " << it->first << " -> " << it->second << endl;
cout << "此时 m[\"apple\"] = " << m["apple"] << "(没被 99 覆盖)" << endl;
return 0;
}
看到什么算成功:
内容:
apple -> 3
banana -> 2
cherry -> 1
date -> 5
此时 m["apple"] = 3(没被 99 覆盖)
两条关键差别:
| 方式 | 键已存在时 | 适合 |
|---|---|---|
m[k] = v; |
覆盖旧值 | 要"设置/更新" |
m.insert(...) |
不起作用(静默失败) | 要"只在没有时添加"(配 .second 判断) |
insert的返回值也是pair<iterator, bool>(和 13.2.1 的set一样)。所以"别覆盖已有的"这件事可以这样写:cpp auto r = m.insert({"apple", 99}); if (!r.second) cout << "apple 已经有了,值是 " << r.first->second << endl;
三、这一节最重要的坑:m[k] 会"偷偷建"
operator[] 的行为:键存在 → 返回它的值的引用;键不存在 → 插入一个"值默认构造"的新元素,再返回它的引用。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> m;
m["apple"] = 3;
cout << "开始 size = " << m.size() << endl;
if (m["banana"] > 0) { // ★ 只是想"看一眼"
cout << "banana 有值" << endl;
}
cout << "问完 banana 之后 size = " << m.size() << " ← 多了一个!" << endl;
for (const auto &p : m) cout << " " << p.first << " -> " << p.second << endl;
// ★ 正确的"只想查询"写法:用 find
map<string, int> n;
n["apple"] = 3;
size_t before = n.size();
if (n.find("banana") != n.end()) cout << "banana 在" << endl;
else cout << "banana 不在" << endl;
cout << "用 find 查完之后 size 还是 " << n.size() << "(没变:" << before << ")" << endl;
return 0;
}
看到什么算成功:
开始 size = 1
问完 banana 之后 size = 2 ← 多了一个!
apple -> 3
banana -> 0
banana 不在
用 find 查完之后 size 还是 1(没变:1)
这张输出的教训值得单独记一页:
m["banana"]只要被求值,就会往容器里插一个banana -> 0。 你只是想"看一眼",容器却被改了。这是map第一大坑。结论: - 要"取值"、且确定键一定存在 → 用
[](或者更稳的at(),越界会抛异常) - 要"查在不在",可能不存在 → 必须用find(或count) - 绝不能写if (m[k] != 0)来"判断有没有" —— 这等于每次都往里塞一个顺带认识
at():m.at("banana")不会插入,键不存在时抛out_of_range(和 13.1.1vector::at的分工一致:[]快、at安全)。
四、遍历与修改
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> m = {{"数学", 95}, {"语文", 88}, {"英语", 92}};
// ── ① 遍历:三种写法 ─────────────────
cout << "① 长类型:";
for (map<string, int>::iterator it = m.begin(); it != m.end(); ++it)
cout << it->first << "=" << it->second << " ";
cout << endl;
cout << "② auto: ";
for (auto it = m.begin(); it != m.end(); ++it)
cout << it->first << "=" << it->second << " ";
cout << endl;
cout << "③ 范围 for:";
for (const auto &p : m) // ★ const 引用:不复制 pair
cout << p.first << "=" << p.second << " ";
cout << endl;
// ── ② 改"值":随便改(值不是 const)──
m["数学"] = 100;
cout << "改完 数学:" << m["数学"] << endl;
// ── ③ 改"键":不行!键是 const ─────────
// m.begin()->first = "物理"; // ❌ 报 assignment of read-only
// 正确做法:删了再插
int v = m["语文"];
m.erase("语文");
m["语文(新)"] = v;
cout << "改键后:";
for (const auto &p : m) cout << p.first << "=" << p.second << " ";
cout << endl;
// ── ④ 删 ─────────────────────────────
m.erase("英语"); // 按键删
m.erase(m.begin()); // 按迭代器删(删第一个)
cout << "删两次后剩 " << m.size() << " 项" << endl;
return 0;
}
看到什么算成功:
① 长类型:数学=95 英语=92 语文=88
② auto: 数学=95 英语=92 语文=88
③ 范围 for:数学=95 英语=92 语文=88
改完 数学:100
改键后:数学=100 语文(新)=88
删两次后剩 0 项
注意两点:
- 输出顺序是"数学、英语、语文" —— 不是字典序?其实中文的排序按的是字节(UTF-8 编码),所以和"读音/笔画"无关。英文场景才是标准的字典序(
apple < banana < cherry)。这一条在实盘处理中文时要知道:想按拼音排,得自己写比较器(把中文转拼音再比)。 it->first/it->second—— 分别是那个pair的两个成员。first是键、second是值,这个命名约定在 STL 里到处都是。C++17 之后可以写得更清楚:for (const auto &[name, score] : m)。
五、动手 Demo:词频统计(map 最经典的用法)
任务:统计一段英文里每个词出现几次,按词频从高到低输出前几名。
#include <iostream>
#include <map>
#include <vector>
#include <algorithm>
#include <sstream>
#include <string>
using namespace std;
int main() {
string text =
"the quick brown fox jumps over the lazy dog "
"the dog barks and the fox runs away";
// ── ① 计数:map 的看家本领,一句 cnt[w]++ ──
map<string, int> cnt;
stringstream ss(text);
string w;
while (ss >> w) cnt[w]++; // ★ 没有就建(值 0),再 +1
cout << "共 " << cnt.size() << " 个不重复的词" << endl;
// ── ② 按字典序输出(map 自带)──
cout << "-- 按字典序 --" << endl;
for (const auto &p : cnt)
cout << " " << p.first << " -> " << p.second << endl;
// ── ③ 按"次数"排序:map 做不到,要搬到 vector 里 ──
vector<pair<string, int>> v(cnt.begin(), cnt.end()); // ★ 从 map 复制到 vector
sort(v.begin(), v.end(), [](const pair<string, int> &a, const pair<string, int> &b) {
if (a.second != b.second) return a.second > b.second; // 次数多的在前
return a.first < b.first; // 次数相同按字典序
});
cout << "-- 按出现次数排序(前 3 名)--" << endl;
for (size_t i = 0; i < v.size() && i < 3; i++)
cout << " " << i + 1 << ". " << v[i].first << "(" << v[i].second << " 次)" << endl;
return 0;
}
看到什么算成功:
共 15 个不重复的词
-- 按字典序 --
and -> 1
away -> 1
barks -> 1
brown -> 1
dog -> 2
fox -> 2
jumps -> 1
lazy -> 1
over -> 1
quick -> 1
runs -> 1
the -> 5
...
-- 按出现次数排序(前 3 名)--
1. the(5 次)
2. dog(2 次)
3. fox(2 次)
这个 Demo 把本章三条线连起来了:map(13.2.3)计数 → vector(13.1.1)搬出来 → sort + lambda(13.3.3)换排序规则。
为什么必须"搬到
vector":map只能按键排序,它没有一个"按值排序"的用法(因为它的有序性是"由键决定的",是容器的骨架)。"按值排序"这种需求,标准做法就是"复制到vector再sort"。 这是map最常被问到的问题之一。
30 秒实验
[]偷偷插入:写map<string,int> m; cout << m["x"] << endl;→ 输出 0,且m.size()变成 1。这一句"读操作"改了容器。insert不覆盖:m.insert({"x", 99}); cout << m["x"];→ 如果x已存在,输出旧值(不被 99 覆盖)。at会抛异常:map<string,int> m; cout << m.at("nope");→terminate called after throwing an instance of 'std::out_of_range'。对比m["nope"]的"静默插入"。- 键不能改:
m.begin()->first = "y";→assignment of read-only location(因为first是const string)。 map的迭代器是双向的:写sort(m.begin(), m.end());→ 编译不过。它自己已经按 key 排好了。
常见误区
- "
m[k]只是读一下"——键不存在时它会插入一个默认值。这是map最著名的坑。 查在不在必须用find。 - "
m[k]和m.at(k)一样"——不一样:[]不存在就插入;at不存在就抛out_of_range。"确定存在"用[],"不确定"用at(想看异常)或find(想自己处理)。 - "
insert会覆盖旧值"——不会,静默失败。要覆盖用m[k] = v;。insert的这个特性正好用来实现"只在没有时才加"。 - "
map元素里的键可以改"——first是const,编译不过。要改键:erase再insert。("值"second随便改。) - "
map可以按值排序"——不可以。它只有"按键有序"这一种秩序。要按值排,复制到vector再sort(本节 Demo)。 - "
map里放指针很省事"——要管释放:map析构不会delete值里的指针。要么直接存对象(推荐),要么存智能指针。 - "
map的[]要求值类型什么都能行"——要求"值类型能默认构造"(因为要造那个"默认值")。如果V没有默认构造函数,就不能用[],只能用insert/emplace。 - "遍历
map时改它没关系"——插入会让迭代器失效;删除只让被删的那个失效。 别在遍历中insert。 要改就先记下来,遍历完再改。 - "
map处理中文也是按拼音/笔画排的"——是按字节(UTF-8)排的,与读音无关。要按拼音排,需要自己写比较器。
自测三问(点开对答案)
1. **`m[k]` 和 `m.find(k)` 有什么区别?为什么这条最要命?**——**`m[k]` 在键不存在时会"插入一个默认构造的值"再返回引用**(容器被改了!);**`m.find(k)` 只查不改**,返回迭代器(找不到是 `end()`)。**所以"判断有没有"必须用 `find`(或 `count`);写 `if (m[k] != 0)` 是错的——每次判断都会往里塞一个。** 2. **`map` 的元素是什么类型?键能改吗?**——**`pair- ☐ 下一节(13.2.4 multimap 类模板)
相关笔记
- multiset 类模板 —— 上一节:允许重复的键集
- multimap 类模板 —— 下一节:一个键对应多个值
- set类模板(含实例02) ——
map就是set加一个值 - 排序算法 —— 按值排序要用的
sort+ lambda - 简单类模板 ——
pair/value_type的来历 - 构造函数 ——
operator[]依赖"值类型能默认构造" - 🔷 C++基础 · 索引 —— 全课路线图