目录
- 前言
- 一、map系列的使用
-
- [1.1 map 是什么?](#1.1 map 是什么?)
- [1.2 map 的模板结构与核心类型](#1.2 map 的模板结构与核心类型)
- [1.3 pair:map 的元素单元](#1.3 pair:map 的元素单元)
-
- [1.3.1 花括号的两种语义!](#1.3.1 花括号的两种语义!)
- [1.4 insert 插入操作全解析](#1.4 insert 插入操作全解析)
- [1.5 迭代器与遍历](#1.5 迭代器与遍历)
- [1.6 find 和 erase](#1.6 find 和 erase)
- [1.7 map::operator[]](#1.7 map::operator[])
-
- [1.7.1 operator[] 的函数签名与核心语义](#1.7.1 operator[] 的函数签名与核心语义)
- [1.7.2 底层执行流程:逐行拆解等价代码](#1.7.2 底层执行流程:逐行拆解等价代码)
- [1.7.3 countMap[e]++ 到底发生了什么?](#1.7.3 countMap[e]++ 到底发生了什么?)
- [1.7.4 operator[] 与其他 map 操作的对比](#1.7.4 operator[] 与其他 map 操作的对比)
- 二、multimap
- 结语


🎬 云泽Q :个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、map系列的使用
1.1 map 是什么?
std::map 是 C++ 标准库中的有序关联容器,核心特点:
- 存储键值对(key-value):每个元素由 key(键)和 mapped value(值)组成。
- 键唯一:同一个 key 只能出现一次,重复插入不会覆盖,只会失败。
- 按键有序:元素会按照 key 的比较规则(默认升序)自动排序。
- 底层实现:红黑树(自平衡二叉搜索树) ,保证插入 / 删除 / 查找的时间复杂度为 O(log n)。
1.2 map 的模板结构与核心类型
cpp
template <
class Key, // 键类型:map::key_type
class T, // 值类型:map::mapped_type
class Compare = less<Key>, // 比较函数:map::key_compare(默认升序)
class Alloc = allocator<pair<const Key, T>> // 分配器:map::allocator_type
> class map;
核心类型:value_type,map 中每个元素的类型是 value_type,定义为:
cpp
typedef pair<const Key, T> value_type;
首先,const Key:键是**不可修改的!**因为键是红黑树的排序依据,修改键会破坏树结构,只能删除后重新插入。其次,T:值可以自由修改。本质:map 是存储 pair<const Key, T> 的有序容器。
1.3 pair:map 的元素单元
pair 是 map 的基础单元,用来绑定两个任意类型的值(first 和 second)
1. pair 的核心结构
cpp
template <class T1, class T2>
struct pair {
T1 first; // 第一个元素(map 中对应 const Key)
T2 second; // 第二个元素(map 中对应 T)
// ... 构造函数、运算符等
};
- first 和 second 是公有成员,可以直接访问。
- map 的
value_type就是pair<const Key, T>,所以 map 的每个元素本质是一个 pair。
cpp
#include<iostream>
#include<map>
#include<string>
using namespace std;
void test_map1()
{
map<string, string> dict;
//C++98
pair<string, string> kv1("sort", "排序");
dict.insert(kv1);
dict.insert(pair<string, string>("left", "左边"));
dict.insert(make_pair("left", "左边"));
//C++11
//单参数可以隐式类型转换
//C++11之后多参数类型的构造函数也支持隐式类型转换
//这里不是initializer_list
//花括号有两种,一种是多参数类型的隐式类型转换,一种是initializer_list
dict.insert({ "right", "右边" });
//pair的initializer_list
dict.insert({ kv1, pair<string, string>("left", "左边") });
dict.insert({ {"string", "字符串"},{"map", "地图,映射"}});
//key相同就不会再插入,value不相同也不会插入
dict.insert({ "left", "左边xxx" });
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
//无法直接打印,解引用里面是一个pair,pair没有重载流插入流提取
//cout << (*it) << endl;
//成员公有,可以直接取first,second
//cout << (*it).first << ":" << (*it).second << endl;
//遍历时上面的方式也不建议使用,迭代器还重载了一个运算符->
//当容器内存的数据是一个结构的时候,迭代器模拟的是指针的行为
//普通指针获取对象直接解引用,是个结构的指针访问成员可以用->
cout << it->first << ":" << it->second << endl;
//本质上如下
//cout << it.operator->()->first << ":" << it.operator->()->second << endl;
++it;
}
cout << endl;
//map更体现范围for的价值
//是把解引用迭代器的值给e,e就是pair
for (auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
//C++17之后范围for还可以这样写
//该语法叫结构化绑定
//本质上可以这样写,x绑定first,y绑定second
auto [x, y] = kv1;
//for (auto [k, v] : dict)
//for (auto& [k, v] : dict)
for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
}
int main()
{
test_map1();
return 0;
}

2. 构造 pair 的常见方式
cpp
template <class T1, class T2>
struct pair {
// 公有成员:内存中连续存储,无额外封装开销
T1 first;
T2 second;
// 1. 默认构造:值初始化(成员为零值/空值)
pair() : first(T1()), second(T2()) {}
// 2. 多参数构造:核心!非 explicit,支持隐式转换
pair(const T1& a, const T2& b) : first(a), second(b) {}
pair(T1&& a, T2&& b) : first(std::move(a)), second(std::move(b)) {} // C++11 右值版本
// 3. 拷贝构造:深拷贝成员
pair(const pair& other) : first(other.first), second(other.second) {}
// 4. 移动构造:转移资源(C++11)
pair(pair&& other) noexcept : first(std::move(other.first)), second(std::move(other.second)) {}
// 5. 跨类型拷贝构造:支持隐式类型转换(关键!)
template <class U, class V>
pair(const pair<U, V>& other) : first(other.first), second(other.second) {}
};
✅ 核心特性:
- first 和 second 是公有成员,直接访问无开销;
- 多参数构造函数非 explicit,允许用 {a,b} 隐式构造;
- 跨类型拷贝构造让 pair 可以隐式转换 (比如
pair<const char*, const char*>→pair<string, string>)。
1️⃣ 直接构造:显式创建 pair 对象
cpp
pair<string, string> kv1("sort", "排序");
底层原理
- 模板参数显式指定:T1=string, T2=string,编译器直接生成对应结构体。
- 构造函数调用:
- 字面量 "sort"
(const char*)→ 隐式调用string(const char*)构造临时 string; - 调用
pair(const string&, const string&),将临时 string 拷贝到 kv1.first; - 同理 "排序" 拷贝到 kv1.second。
与 map 的交互
cpp
dict.insert(kv1); // 插入到 map
map<string, string> 的 value_type 是 pair<const string, string>;而 kv1 是 pair<string, string>,通过跨类型拷贝构造 隐式转换为 pair<const string, string>;map 底层红黑树节点会深拷贝这个 pair,存储为 const string 键(保证键不可修改)。
这种写法缺点就是:需要手动写模板参数,代码冗余,临时对象会产生拷贝开销
2️⃣ 模板推导构造(make_pair):自动推导类型
cpp
auto kv2 = make_pair("left", "左边"); // 推导为 pair<const char*, const char*>
dict.insert(kv2); // 隐式转换为 map 的 value_type
底层原理
第一步:make_pair 的模板推导,make_pair 是函数模板,简化实现:
cpp
template <class T1, class T2>
pair<T1, T2> make_pair(T1 x, T2 y) {
return pair<T1, T2>(std::forward<T1>(x), std::forward<T2>(y)); // C++11 完美转发
}
传入 "left"(const char*)→ T1 推导为 const char*;传入 "左边"(const char*)→ T2 推导为 const char*;返回 pair<const char*, const char*> 临时对象。
第二步:插入 map 时的隐式转换
map 的 insert 接收 pair<const string, string>,因此:
- 调用 pair 的跨类型拷贝构造 :
pair<const string, string>(const pair<const char*, const char*>&); const char* → string隐式转换,生成新的pair<const string, string>存入红黑树节点。
❌ 缺点:推导类型可能不符合预期(比如 const char* 而非 string),需要依赖隐式转换。
3️⃣ C++11 花括号初始化:隐式构造 pair
cpp
pair<string, string> kv3 = {"right", "右边"};
// 或直接在 map 插入中使用:
dict.insert({"right", "右边"});
底层原理
这是 C++11 对多参数构造函数隐式转换的扩展:
- 花括号语义 :{"right", "右边"} 不是 initializer_list,而是多参数构造的隐式调用;
- 编译器解析为:pair<string, string>("right", "右边"),直接调用多参数构造函数;
- 生成临时 pair<string, string> 对象,传入 insert 时被移动到红黑树节点
1.3.1 花括号的两种语义!
花括号的两种语义(避坑!)
cpp
// 语义 1:单 pair 隐式构造 → 调用 insert(const value_type&)
dict.insert({ "right", "右边" });
// 语义 2:initializer_list 批量插入 → 调用 insert(initializer_list<value_type>)
dict.insert({ {"string", "字符串"}, {"map", "地图,映射"} });
- 单层 {a,b}:构造单个 pair;
- 双层 {{a,b}, {c,d}}:构造
initializer_list<pair<K,V>>,批量插入多个元素。
1.4 insert 插入操作全解析
1. 单元素插入(最常用)
cpp
pair<iterator, bool> insert(const value_type& val);
template <class P> pair<iterator, bool> insert(P&& val);
- 作用:插入一个 value_type(即 pair)元素。
- 返回值:
pair<iterator, bool>
iterator:指向插入成功的元素(或已存在的冲突元素)。
bool:true 表示插入成功(键不存在),false 表示插入失败(键已存在)。
代码中这几种写法本质都是调用这个重载:
cpp
dict.insert(kv1); // 传入已有的 pair
dict.insert(pair<string, string>("left", "左边")); // 显式构造 pair
dict.insert(make_pair("left", "左边")); // make_pair 生成 pair,隐式转换
dict.insert({"right", "右边"}); // C++11 花括号构造 pair,隐式转换
2. 初始化列表批量插入(C++11)
cpp
void insert(initializer_list<value_type> il);
作用:一次性插入多个 value_type 元素。
代码示例:
cpp
dict.insert({ kv1, pair<string, string>("left", "左边") });
dict.insert({ {"string", "字符串"}, {"map", "地图,映射"} });
本质:{} 包裹多个 pair 初始化式,构成 initializer_list< value_type >,批量插入。
⚠️ 关键注意点
- insert 不会覆盖已存在的键:如果键已存在,插入失败,返回 false。
- 键唯一性:map 中键是唯一的,重复插入相同键的操作会被忽略。
- 对比 operator[]:dict["left"] = "左边xxx" 会覆盖已存在的 left 的值,而 insert 不会。
1.5 迭代器与遍历
map 的迭代器是双向迭代器 (只能向前 / 向后移动,不支持随机访问,比如 it + 5),解引用后得到 value_type(即 pair<const Key, T>)。
1. 迭代器遍历
cpp
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
// 访问方式1:解引用迭代器,再访问成员
cout << (*it).first << ":" << (*it).second << endl;
// 访问方式2:迭代器模拟指针行为,用 -> 直接访问成员
cout << it->first << ":" << it->second << endl;
++it;
}
it->first:本质是 it.operator->()->first,迭代器重载了 -> 运算符,返回 pair 的指针,所以可以直接访问成员。
2. 范围 for 循环(C++11)
更简洁的遍历方式,自动解引用迭代器:
cpp
for (auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
e 是 pair<const Key, T>&,避免拷贝,高效访问。
3. 结构化绑定(C++17)
更直观的语法,直接将 pair 的两个成员绑定到变量:
cpp
for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
- k 绑定到 first(键),v 绑定到 second(值)。
- const auto&:只读引用,既避免拷贝,又防止误修改键(k 是 const)。
1.6 find 和 erase

这里再说一下为什么insert的时候:e 能作为 map 的字符串键:
-
arr 是 std::string 类型的数组 ,每个元素都是字符串对象。范围 for 循环中 auto& e 会被推导为 std::string&(字符串的左值引用)。所以 e 本质上就是一个 std::string 类型的字符串,和 "苹果"、"西瓜" 这些字面量最终构造的类型完全一致。
-
这个 map 的 键类型(key_type)是 std::string,值类型(mapped_type)是 int。它要求插入的键必须是可以转换为 std::string 的类型,而 e 本身就是 std::string,完全匹配。
-
map::insert 要求传入
pair<const K, V>类型的参数(这里K = std::string,V = int),{e, 1} 是初始化列表 ,编译器会自动用它构造一个临时的pair<const string, int>对象(map 要求键是 const 不可修改的)。这一步是调用 pair 的构造函数完成的,不需要显式写出pair<const string, int>(e, 1),是隐式的构造行为,这个 pair 的 first 成员(键)会用 e 来构造 / 拷贝,second 成员(值)会用 1 来构造。因为 e 是 std::string,所以可以直接作为 pair 的 first,也就是 map 的键。
再细节说一下内层,内层:e → const std::string
在构造这个 pair 时,pair 的 first 成员类型是 const std::string,而传入的 e 是 std::string&:这里会发生隐式的 const 限定转换:std::string& 被转换为 const std::string(只是给变量加上 const 修饰,不改变值本身,是安全的隐式转换)。1 是 int,直接匹配 pair 的 second 成员类型 int,不需要转换。
1.7 map::operator[]
std::map::operator[] 是 C++ 标准库中最具 "魔法感" 的容器操作之一,它表面是下标访问 ,底层却封装了查找 + 插入 + 值引用返回的完整逻辑。
1.7.1 operator[] 的函数签名与核心语义
1. 函数签名(C++98 / C++11)
cpp
// C++98:仅支持左值键
mapped_type& operator[] (const key_type& k);
// C++11:新增右值键版本(支持移动语义,避免拷贝)
mapped_type& operator[] (key_type&& k);
返回值 :mapped_type& ------ 映射值的左值引用 ,意味着你可以读取 / 修改 这个值(比如 countMap[e]++ 本质是对引用自增)。
参数:key_type 类型的键(左值 / 右值)
2. 核心语义
若键 k 存在:返回对应值的引用;若键 k 不存在:自动插入一个 "键为 k、值为默认构造" 的新节点,再返回这个新值的引用。
⚠️ 关键副作用:只要调用 operator[],无论是否赋值,容器大小都可能增加(键不存在时)。
1.7.2 底层执行流程:逐行拆解等价代码
operator[] 的等价实现:
cpp
// 等价代码:operator[](k) ≡ 下面这行
(*((this->insert(make_pair(k, mapped_type()))).first)).second
我们把这行 "嵌套地狱" 拆成 5 步,彻底看清底层逻辑:
步骤 1:构造临时 pair ------ make_pair(k, mapped_type())
mapped_type():调用映射值类型的默认构造函数,生成一个 "空 / 零值"(比如 int → 0,std::string → 空串,自定义类需支持默认构造)。make_pair(k, mapped_type()):构造一个临时std::pair<key_type, mapped_type>对象,作为待插入的 "键值对"。- 注意:map 的 value_type 是
std::pair<const key_type, mapped_type>,所以这个临时 pair 会在后续被隐式转换为 const 键的版本。
步骤 2:调用 insert ------ this->insert(...)
insert 是 map 的核心插入函数,单元素版本返回 std::pair<iterator, bool>:
- iterator:指向红黑树中对应键的节点迭代器(无论是否插入成功)。
- bool:标记是否真的插入了新节点(true = 新插入,false = 键已存在)。
insert 内部执行红黑树的查找 + 插入逻辑:
- 从根节点开始,按红黑树的有序规则查找键 k 的位置;
- 若找到相同键:不插入新节点,直接返回指向该节点的迭代器 + false;
- 若未找到:在合适位置插入新节点(用临时 pair 构造
pair<const key_type, mapped_type>),返回指向新节点的迭代器 + true。
步骤 3:提取迭代器 ------ .first
insert 返回的 pair<iterator, bool> 中,.first 就是指向目标节点的迭代器(不管是旧节点还是新节点)。
步骤 4:解引用迭代器 ------ *()
*iterator 会得到红黑树节点中存储的 std::pair<const key_type, mapped_type> 对象(键 + 值的结构体)。
步骤 5:提取值并返回引用 ------ .second
pair::second 就是我们要的映射值,最终返回这个值的左值引用(mapped_type&)。
1.7.3 countMap[e]++ 到底发生了什么?

我们以第一次遍历到 "苹果" 为例:
- e 是 "苹果"(std::string& 左值),调用
countMap.operator[]("苹果"); - 构造 make_pair("苹果", int()) → pair<string, int>("苹果", 0);
- insert 发现 "苹果" 不存在,插入新节点,返回 pair<iterator, true>;
- 提取迭代器,解引用得到
pair<const string, int>("苹果", 0),取 .second 得到 0 的引用; - 执行 ++ 操作:把 0 变成 1;
第二次遍历到 "苹果" 时:
- 调用
countMap.operator[]("苹果"); - 构造 make_pair("苹果", 0),insert 发现 "苹果" 已存在,返回
pair<iterator, false>; - 提取迭代器,解引用得到已存在的
pair<const string, int>("苹果", 1),取 .second 得到 1 的引用; - 执行 ++ 操作:把 1 变成 2;
最终,countMap 中每个键的值就是它在数组中出现的总次数 ------ 这就是 operator[] 简洁实现 "计数统计" 的底层逻辑。
1.7.4 operator[] 与其他 map 操作的对比
1. 与 find + insert 手动写法对比
cpp
auto it = countMap.find(e);
if (it != countMap.end()) {
it->second++;
} else {
countMap.insert({e, 1});
}
- 相同点:时间复杂度都是 O (log n),核心逻辑都是 "查找后决定是否插入";
- 不同点 :
operator[] 会强制插入默认值 ,手动写法可以控制插入的初始值(比如这里插入 1 而非 0);
operator[] 更简洁,但隐藏了 "是否插入新节点" 的信息;手动写法可以通过 insert 的返回值 bool 明确判断插入状态;
当 mapped_type 无默认构造时,operator[] 无法使用(比如自定义类没有 T() 构造函数),手动写法更灵活。
2. 与 at() 对比
map::at() 是另一个访问函数:
cpp
mapped_type& at(const key_type& k);
const mapped_type& at(const key_type& k) const;
- 相同点:都返回映射值的引用,时间复杂度 O (log n);
- 不同点 :
at() 不会插入新节点:若键不存在,直接抛出std::out_of_range异常;
operator[] 会插入默认值,无异常但改变容器大小;
at() 可以在 const map 上调用(因为它不修改容器),operator[] 不行(因为可能插入)。
3. 与 insert 直接对比



二、multimap

multimap版本的最大特点就是允许键值冗余,基本没啥好说的,和从功能的角度看前面的map差不多,下面一张图片就可以看懂了

直接理解为给值就插入就好了
其find查找的时候依旧是找中序第一个,其次multimap既没有at,也没有[ ],原因就是其可以插入相同的key,查找的时候不好返回value值,这个接口不好实现
结语
