前言
在 C++ 开发中,STL 容器是绕不开的核心技能,而map和set作为关联式容器的代表,凭借红黑树底层实现的 O (logn) 级增删查效率、自动有序性、key 唯一性,成为了面试高频考点、业务开发高频使用的工具。
很多初学者对map/set的用法只停留在简单的插入、删除,却始终没搞懂最核心的迭代器和pair—— 而这两个知识点,恰恰是理解和用好这两个容器的关键。
本文将从基础定义到实战用法,带大家彻底吃透map/set,重点拆解迭代器与 pair 的核心逻辑,所有代码均可直接复制运行,兼顾入门学习与面试复习。
一、先搞懂基础:map/set 到底是什么?
map和set都是 C++ STL 提供的有序关联式容器,底层均由红黑树实现,默认按 key 的升序排列,核心特性高度一致,核心区别仅在于存储的元素类型:
表格
| 容器 | 存储结构 | 核心特性 | 典型适用场景 |
|---|---|---|---|
set | 单值集合 | 元素唯一、自动排序,无法通过迭代器修改元素 | 数据去重、有序集合维护、存在性判断 |
map | 键值对集合 | key 唯一、自动排序,每个元素是pair<key, value> | 字典映射、频次统计、key-value 型数据管理 |
补充说明:二者的增、删、查操作时间复杂度均为 O (logn),且均不允许 key 重复(重复插入会被自动忽略);multi_map/multi_set 支持 key 重复,本文不做重点展开。
二、map 全用法详解:pair 是它的基本单元
map的核心本质:容器里的每一个元素,都是一个pair<const Key, T>类型的对象。
这里的pair是 C++ 的结构体模板,包含两个成员:
first:对应 map 的key,类型为const Key(不可修改,保证 key 的唯一性与有序性)second:对应 map 的value,可正常修改
我们对 map 的插入、遍历、查找,本质上都是在操作pair和指向pair的迭代器。
2.1 基础准备:头文件与定义
使用 map 必须包含<map>头文件,定义格式如下:
cpp
运行
#include <iostream> #include <map> // map核心头文件 #include <string> using namespace std; // 示例简化命名空间,工程中可按需使用 int main() { // 定义格式:map<key类型, value类型> 容器名; map<int, string> studentMap; // key:学号(int), value:姓名(string) return 0; }2.2 元素插入:4 种常用方式,全和 pair 相关
map 的插入本质,就是把 pair 对象存入容器,常用 4 种插入方式,各有适用场景:
cpp
运行
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> studentMap; // 方式1:[]运算符重载(最常用、最简单) // 底层逻辑:若key不存在,自动创建pair<key, 默认value>,再给second赋值 studentMap[1] = "张三"; studentMap[2] = "李四"; // 方式2:C++11 列表初始化(简洁直观,推荐) studentMap.insert({3, "王五"}); // 方式3:make_pair构建pair插入(通用写法,兼容旧标准) studentMap.insert(make_pair(4, "赵六")); // 方式4:显式构建pair对象插入(最贴合底层逻辑) studentMap.insert(pair<int, string>(5, "钱七")); // 测试:重复插入key=1,会被自动忽略 studentMap.insert({1, "新张三"}); // 遍历输出,验证结果 for (auto &p : studentMap) { cout << "学号:" << p.first << " 姓名:" << p.second << endl; } return 0; }⚠️ 注意坑点:[]运算符有副作用 —— 如果访问的 key 不存在,会自动向 map 中插入一个该 key、value 为默认值的元素,即使你只是想读取而非赋值。因此只读场景优先用 find (),而非 []。
2.3 元素查找:迭代器是查找的返回值
map 的find()函数是最安全的查找方式,入参为 key,返回值是迭代器:
- 若找到 key,迭代器指向该 key 对应的
pair对象 - 若未找到,迭代器等于
map.end()(容器末尾的后一位,不可解引用)
cpp
运行
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> studentMap = {{1, "张三"}, {2, "李四"}, {3, "王五"}}; // 查找key=2的元素 map<int, string>::iterator it = studentMap.find(2); // 必须先判断迭代器是否有效,否则解引用会导致程序崩溃 if (it != studentMap.end()) { cout << "找到目标:学号=" << it->first << " 姓名=" << it->second << endl; // 可通过迭代器修改value,不可修改key(it->first是const类型) it->second = "新李四"; cout << "修改后姓名:" << it->second << endl; } else { cout << "未找到目标学号" << endl; } return 0; }2.4 元素删除
map 支持两种核心删除方式,可按需选择:
cpp
运行
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> studentMap = {{1, "张三"}, {2, "李四"}, {3, "王五"}, {4, "赵六"}}; // 方式1:按key删除(最常用),返回值为删除的元素个数(0或1) int delNum = studentMap.erase(2); cout << "删除了" << delNum << "个元素" << endl; // 方式2:按迭代器删除,返回值为下一个有效迭代器 auto it = studentMap.find(3); if (it != studentMap.end()) { it = studentMap.erase(it); // 避免迭代器失效 } // 清空所有元素 // studentMap.clear(); // 遍历验证结果 for (auto &p : studentMap) { cout << p.first << " : " << p.second << endl; } return 0; }2.5 核心重点:map 迭代器全解析
迭代器是 STL 容器的 “通用访问接口”,对于 map 来说,迭代器本质就是指向 pair 对象的指针,所有遍历操作都依赖迭代器实现。
map 迭代器的核心访问规则:
it->first:访问 pair 的第一个成员,即 map 的 key(const 类型,不可修改)it->second:访问 pair 的第二个成员,即 map 的 value(可正常修改)++it:迭代器向后移动,按 key 升序访问下一个元素it == map.begin():迭代器指向容器第一个元素it == map.end():迭代器指向容器末尾的后一位,无有效元素
3 种常用遍历方式(全掌握,面试必考)
cpp
运行
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> studentMap = {{1, "张三"}, {2, "李四"}, {3, "王五"}}; cout << "===== 方式1:经典正向迭代器遍历(必须掌握)=====" << endl; // 完整迭代器类型写法,理解底层逻辑,面试必写 map<int, string>::iterator it; for (it = studentMap.begin(); it != studentMap.end(); ++it) { cout << "key: " << it->first << " value: " << it->second << endl; } cout << "\n===== 方式2:auto简化迭代器(工作常用,简洁高效)=====" << endl; for (auto it = studentMap.begin(); it != studentMap.end(); ++it) { cout << "key: " << it->first << " value: " << it->second << endl; } cout << "\n===== 方式3:C++11 范围for循环(最简写法)=====" << endl; // p就是map中的每个pair对象,用引用&避免拷贝,提升效率 for (auto &p : studentMap) { cout << "key: " << p.first << " value: " << p.second << endl; } // 补充:反向迭代器,按key降序遍历 cout << "\n===== 补充:反向迭代器(降序遍历)=====" << endl; map<int, string>::reverse_iterator rit; for (rit = studentMap.rbegin(); rit != studentMap.rend(); ++rit) { cout << "key: " << rit->first << " value: " << rit->second << endl; } return 0; }三、set 全用法详解:迭代器是只读的!
set 和 map 底层逻辑一致,核心区别是:set 只存储单个值,没有 key 和 value 之分,set 的元素本身就是排序和去重的依据。
也正因如此,set 的迭代器有一个核心特性:只读,不可通过迭代器修改元素值—— 一旦修改,会破坏红黑树的有序结构,导致容器失效。
3.1 基础准备:头文件与定义
使用 set 必须包含<set>头文件,定义格式如下:
cpp
运行
#include <iostream> #include <set> // set核心头文件 using namespace std; int main() { // 定义格式:set<元素类型> 容器名; set<int> numSet; return 0; }3.2 核心操作:插入、查找、删除
cpp
运行
#include <iostream> #include <set> using namespace std; int main() { set<int> numSet; // 1. 插入元素:自动去重 + 自动升序排序 numSet.insert(5); numSet.insert(2); numSet.insert(8); numSet.insert(2); // 重复元素,自动忽略 numSet.insert(1); // 2. 查找元素:返回迭代器 auto it = numSet.find(2); if (it != numSet.end()) { cout << "找到元素:" << *it << endl; // 解引用迭代器获取元素值 } // 3. 删除元素 numSet.erase(5); // 按值删除 auto it_del = numSet.find(8); if (it_del != numSet.end()) { numSet.erase(it_del); // 按迭代器删除 } // 4. 常用属性 cout << "容器大小:" << numSet.size() << endl; cout << "是否为空:" << (numSet.empty() ? "是" : "否") << endl; // numSet.clear(); // 清空容器 return 0; }3.3 set 迭代器遍历
set 迭代器的核心规则:
*it:解引用迭代器,获取元素值(只读,不可修改)- 迭代器支持 ++/-- 操作,按元素升序 / 降序移动
- 不支持通过迭代器修改元素值
cpp
运行
#include <iostream> #include <set> using namespace std; int main() { set<int> numSet = {5, 2, 8, 1, 2}; cout << "===== 方式1:经典迭代器遍历 =====" << endl; set<int>::iterator it; for (it = numSet.begin(); it != numSet.end(); ++it) { cout << *it << " "; // *it = 10; // 错误!set迭代器是只读的,不可修改元素 } cout << endl; cout << "\n===== 方式2:范围for循环(最简写法)=====" << endl; for (auto num : numSet) { cout << num << " "; } cout << endl; cout << "\n===== 补充:反向迭代器(降序遍历)=====" << endl; set<int>::reverse_iterator rit; for (rit = numSet.rbegin(); rit != numSet.rend(); ++rit) { cout << *rit << " "; } return 0; }四、灵魂核心:迭代器与 pair 一句话总结
很多人学完还是混乱,这里用最精简的话,帮你彻底记住核心逻辑,一辈子忘不掉:
*map 里存的是 pair,迭代器指向 pair,it->first 是 key,it->second 是 value;set 里存的是单值,迭代器指向值,it 取值,且只读不可改。
再给大家整理一张核心用法对比表,面试复习直接背:
表格
| 操作场景 | map 迭代器用法 | set 迭代器用法 |
|---|---|---|
| 获取元素 | it->first(key)、it->second(value) | *it(元素值) |
| 元素修改 | 可修改it->second,不可修改it->first | 不可修改*it,迭代器只读 |
| 查找返回 | 找到指向对应 pair,否则等于end() | 找到指向对应元素,否则等于end() |
| 遍历方向 | 正向begin()/end(),反向rbegin()/rend() | 与 map 完全一致 |
| 迭代器失效 | 仅被删除元素的迭代器失效,其他不受影响 | 与 map 完全一致 |
五、实战案例:高频业务场景落地
光说不练假把式,这里给大家两个开发中最常用的实战案例,把 map 和 set 结合起来用,代码可直接复用。
案例 1:用 map 统计字符串中每个单词的出现频次
cpp
运行
#include <iostream> #include <map> #include <string> #include <sstream> using namespace std; int main() { string text = "hello world hello c++ world stl map stl"; map<string, int> countMap; istringstream ss(text); // 字符串流,拆分单词 string word; // 统计频次 while (ss >> word) { countMap[word]++; // 单词不存在则自动创建,value默认0,再++ } // 遍历输出结果 cout << "单词频次统计结果:" << endl; for (auto &p : countMap) { cout << p.first << " : " << p.second << "次" << endl; } return 0; }案例 2:用 set 实现数组去重 + 排序
cpp
运行
#include <iostream> #include <set> #include <vector> using namespace std; int main() { vector<int> nums = {5, 2, 9, 1, 2, 5, 6, 9, 3}; set<int> numSet(nums.begin(), nums.end()); // 直接用vector初始化set,自动去重排序 // 输出去重排序后的结果 cout << "去重排序后的数组:" << endl; for (auto num : numSet) { cout << num << " "; } return 0; }六、面试高频考点避坑指南
map 的 [] 和 insert 有什么区别?
[]:若 key 不存在,会自动插入默认值,即使只读操作也会修改容器,线程不安全insert:key 不存在则插入,存在则忽略,不会修改已有元素,更安全- 只读场景优先用
find(),插入场景优先用insert,赋值场景可用[]
为什么 set 的迭代器不能修改元素?set 的元素本身就是红黑树的排序依据,修改元素会破坏红黑树的有序结构,导致容器的查找、增删逻辑全部失效,因此 STL 将 set 的迭代器设计为 const 只读类型。
map/set 的迭代器什么时候会失效?对于红黑树实现的 map/set,只有被删除的元素对应的迭代器会失效,其他元素的迭代器不受影响;插入操作不会导致任何迭代器失效。这和 vector 的迭代器失效规则有本质区别,面试常考对比。
map 的 key 可以是自定义类型吗?可以,但必须重载
<运算符,为自定义类型提供排序规则,否则红黑树无法完成排序和去重。
结尾
map 和 set 是 C++ STL 中最实用的容器之一,而迭代器和 pair 就是它们的灵魂 —— 只有彻底搞懂这两个核心知识点,才能真正用好这两个容器,而不是只会简单的 API 调用。