C++ STL map/set 从入门到精通:核心迭代器与 pair 用法全解析
前言
在 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 调用。
更多推荐


所有评论(0)