1.map
在上篇文章中,我着重介绍了set,由于map和set同源,所以这次我会着重介绍map别于set的地方
(1)模板参数
set是以单一的key作为成员变量,而map是以pair作为成员变量,而pair的first作为key来决定插入位置,second与first一一对应。唯一需要注意的就是map插入pair后不能修改key只能修改value。逻辑上也是必须的,因为map底层的平衡树就是依靠key的大小来建立的,改了key之后这个结点的位置就不对了,整棵树都被毁了。(关联式容器)
(2)构造函数
构造函数和set一样简洁,注意传参仿函数对象和实例化类型要匹配,写不写无所谓,编译器其会自动生成
(3)insert
map的insert和set的一模一样,都是插入数据,返回pair
但是要注意map的insert对象是pair,下面有几种方式:
make_pair
make_pair是一个模板函数,有两个模板参数,可以通过传入参数的类型来推理并实例化,返回对应的匿名pair对象。值得注意的是,pair类型之间能够发生类型转换
这里可以看到,当make_pair的返回值类型和实例化类型不同时,可以发生隐式类型转换,转换的规则遵循first和second分别的转换规则
同样的,当插入pair的类型和map对应的pair类型不相同时,也能发生隐式类型转换。这也进一步验证了pair的本质只是一个存储数据的类型,其实这也依赖pair的operator=函数的支持。
除了make_pair构造以外,还有两种构造,都很好理解
(4)initializer_list
在C++11之后,所有的容器都支持了initializer_list
我们需要抓住当用初始化列表时,最外层{}是初始化列表的标志,({1, 2, 3})标志参数为初始化列表,初始化列表里又可以使用{}标志隐式类型转换,当初始化时可以省略=(如m3)
这里要慢慢消化,可以结合C++相关概念和易错语法(14)(初始化注意事项、vector、编译器向上查找规则)多练习
(5)map里没有流插入,pair也没有流插入,需要自己显式写
(6)find、lower_bound、upper_bound用法和set一模一样,返回值均为迭代器
count、erase返回值均为size_t,标志找到的元素个数
(7)map中iterator的使用
我们要抓住迭代器的本质是模拟指向存储元素的指针,存储的是pair,一个自定义类型,所以operator->就派上了大用场,这也是我们遇到的第一个operator->特殊处理后有实用的地方。
在set中由于是key单独作为存储数据,就没有operator->的概念,直接*it就得到数据了
我们要着重区分pair<iterator, bool>和iterator,因为iterator指向的元素也是pair,在调用的时候分不清什么时候用.什么时候用->
(8)operator[]
这可以说是map中的一大难点,在vector、deque中的operator[]可以和*it互换,换句话说,我们之前接触的容器的operator[]本质上和数组的[]没什么区别,是随机迭代器的标配。
但map中是双向迭代器(功能少于随即迭代器)本应没有operator[],但STL让它支持operator[],这也使得它拥有了非同寻常的实现过程和功能。
下面是详细的分析
其中需要注意的是value的默认构造包括自定义类型和内置类型的默认构造,内置类型的默认构造如int(),它的返回值为0(所有编译器都是这样处理的)
理解了上面这些,我们就能利用map实现更多功能了
这已经不是传统opeartor[]的用法了,由于强制支持operator[],map可以向上面那样以很直观的方式插入修改和查找。
(9)范围for
迭代器模拟的是容器元素的指针,范围for返回的是元素的拷贝或引用
两者的底层都是迭代器,但是表层一个是模拟指针,一个是值或值的引用
在map中使用范围for建议都使用const auto&,因为如果返回值给e,那么每次都要构造一个pair,pair中还可能有自定义类型,这样开销太大了,直接用const引用效率会高很多
(10)multimap
multimap和map的区别类似于multiset和set的区别,都是支持多个key的存储,没有去重功能。但multimap没有operator[],因为同一个key可能对应不同value,没有意义。当使用迭代器遍历时同样遵循其元素pair的比较逻辑。
2.迭代器分类
我们已经接触过了string、vector、list、deque、stack、queue、priority_queue、map、set、multiset、multimap这些基本容器,我们会发现这些容器中有的没有迭代器,有的迭代器支持随意+num,如string,有的就不行,如list。出现这样的原因是迭代器也有它的分类,不同的迭代器有不同的功能,有的算法库的函数只能用特定迭代器才能使用。
随机迭代器:string、vector、deque
双向迭代器:list、map、set、multiset、multimap
无迭代器:stack、queue、priority_queue
不同的迭代器有不同的功能,下面是随机迭代器和双向迭代器的比较
由于两种迭代器的功能差异较大,一种是逐个访问,一种可以跳跃访问,在一些对访问灵活度要求高的算法中,就不能传双向迭代器而只能传随机迭代器,如sort
所以在很多时候我们需要转换容器。看看下面的代码,尝试加深理解
#include <iostream>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;
struct Compare
{
bool operator()(const pair<string, string>& p1, const pair<string, string>& p2) const
{
return p1 < p2;
}
};
int main()
{
map<string, string> m;
m["Huawei"] = "华为";
m["Xiaomi"] = "小米";
m["Apple"] = "苹果";
m["Samsung"] = "三星";
vector<pair<string, string>> v(m.begin(), m.end());
sort(v.begin(), v.end(), Compare());
for (const auto& e : v)
{
cout << e.first << " " << e.second << endl;
}
return 0;
}
结果是
但是这里有个疑问,为什么可以用map的迭代器来初始化vector?有没有要求呢?这就需要引出另外几个迭代器:输入迭代器
此外还有剩余两个迭代器:输出迭代器、前向迭代器
我们重点区分双向迭代器和随机迭代器,很多容器都是这两种迭代器之一。在特定情况下我们也知道去切换容器。