map是一个关联式容器,所谓的关联式容器,也就是类似与关联性数据库。每个元素都有一个键(key)和一个值(value)一一对应。关联式容器一般都是默认进行排序的。排序规则是按照键值的大小。容器的内部结构一般为 RB-tree,或者hashtable。
映射和多重映射基于某一类型Key的键集的存在,提供对T类型的数据进行快速和高效的检索。键和值的数据类型一般是不一致的。是一个pair类型中的两个分量。
所有类型的 map 容器保存的都是键值对类型的元素。map 容器的元素是 pair
再看map的定义之前,我们先看看pair的定义,下面是
template <class _T1, class _T2>
struct pair {
typedef _T1 first_type;
typedef _T2 second_type;
_T1 first;
_T2 second;
pair() : first(_T1()), second(_T2()) {}
pair(const _T1& __a, const _T2& __b) : first(__a), second(__b) {}
#ifdef __STL_MEMBER_TEMPLATES
template <class _U1, class _U2>
pair(const pair<_U1, _U2>& __p) : first(__p.first), second(__p.second) {}
#endif
};
上面的例子中我们需要注意的是:成员 first和second是public的。而我们经常会使用make_pair(const K&, const T&)来创建map,因此我们了解下这个函数。
template <class _T1, class _T2>
inline pair<_T1, _T2> make_pair(const _T1& __x, const _T2& __y)
{
return pair<_T1, _T2>(__x, __y);
}
看完pair的定义之后,我们看下map的定义,在SGI源码里面,map被定义为:
template <class _Key, class _Tp, //键和值
class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>), //默认缺省,并且采用递增排序(也就是键值的比较)
class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) > //空间分配器
class map;
template <class _Key, class _Tp, class _Compare, class _Alloc>
class map {
public:
// requirements:
__STL_CLASS_REQUIRES(_Tp, _Assignable);
__STL_CLASS_BINARY_FUNCTION_CHECK(_Compare, bool, _Key, _Key);
// typedefs:
typedef _Key key_type; //键类型
typedef _Tp data_type; //值类型
typedef _Tp mapped_type;
typedef pair<const _Key, _Tp> value_type; //元素
typedef _Compare key_compare; //键比较
class value_compare : public binary_function<value_type, value_type, bool> {
friend class map<_Key,_Tp,_Compare,_Alloc>;
protected :
_Compare comp;
value_compare(_Compare __c) : comp(__c) {}
public:
bool operator()(const value_type& __x, const value_type& __y) const {
return comp(__x.first, __y.first);
}
};
private:
typedef _Rb_tree<key_type, value_type,
_Select1st<value_type>, key_compare, _Alloc> _Rep_type;
_Rep_type _M_t; // red-black tree representing map
//下面是定义了一些指针、引用、迭代器等类型。
public:
typedef typename _Rep_type::pointer pointer;
typedef typename _Rep_type::const_pointer const_pointer;
typedef typename _Rep_type::reference reference;
typedef typename _Rep_type::const_reference const_reference;
typedef typename _Rep_type::iterator iterator;
typedef typename _Rep_type::const_iterator const_iterator;
typedef typename _Rep_type::reverse_iterator reverse_iterator;
typedef typename _Rep_type::const_reverse_iterator const_reverse_iterator;
typedef typename _Rep_type::size_type size_type;
typedef typename _Rep_type::difference_type difference_type;
typedef typename _Rep_type::allocator_type allocator_type;
};
到此,SGI中map的定义已经结束了。我们能够清晰的看到,map实际上就是用了一个RB-tree来表现。需要注意的一点是:因为map是不允许键值重复的,因此一定是使用RB-tree底层的insert_unique,而非insert_equal。
stl中map有多种构造函数,我们先开看看他的多种构造函数。
在看构造方法之前,我们需要先了解一个概念:
#ifdef __STL_MEMBER_TEMPLATES
含义是:如果编译器支持template members of classes(模板类内嵌套模板) 就定义。
//construction
map() : _M_t(_Compare(), allocator_type()) {}
explicit map(const _Compare& __comp, const allocator_type& __a = allocator_type())
: _M_t(__comp, __a) {}
定义一个键和实值类型都为string类型的空map data。
std::map<std::string, std::string> data;
map
中的每个元素都是同时封装了对象及其键的pair
类型对象,因此不能修改const K。因此我们可以看作是data中的每个元素都是这样的 pair
。
//construction
map(const value_type* __first, const value_type* __last) : _M_t(_Compare(), allocator_type())
{ _M_t.insert_unique(__first, __last); }
map(const value_type* __first, const value_type* __last,
const _Compare& __comp, const allocator_type& __a = allocator_type())
: _M_t(__comp, __a) { _M_t.insert_unique(__first, __last); }
理解这俩构造函数之前,我们先看一看 value_type的定义。从上面的map类的定义中我们能找到,typedef pair
它其实是map中的每个元素。上面这两个构造函数的不同在于第二个指定了自己定义的排序函数。因此这中构造方法我们能够通过初始化列表的形式来看一下。
std::map<std::string, std::string> data {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};
手动修改自定义排序
std::map<std::string, std::string, std::greater<std::string> > data {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};
初始化列表中的值是通过将每个嵌套花括号中的两个值传递给构造函数产生的,因此列表会包含 4 个 pair
对象。
而我们前面已经知晓了pair 的定义和他的一个方法。 make_pair
模板。因此,上面的定义我们也可以写成是:
std::map<std::string, std::string> data {std::make_pair("a", "A"), std::make_pair("b", "B"), std::make_pair("c", "C"), std::make_pair("d", "D")};
同样的,下面的构造函数是以迭代器的形式出现的。
//construction
map(const_iterator __first, const_iterator __last) : _M_t(_Compare(), allocator_type())
{ _M_t.insert_unique(__first, __last); }
map(const_iterator __first, const_iterator __last, const _Compare& __comp, const allocator_type& __a = allocator_type())
: _M_t(__comp, __a) { _M_t.insert_unique(__first, __last); }
也就是说,我们可以用另一个map的其中一段迭代器定义一个新的map,如下:
我们使用data的第二个元素到倒数第二个元素来创建一个新的map-data2.
std::map<std::string, std::string> data2 { ++std::begin(data), --std::end(data)};
在这边,我们还需要了解一下定义在宏__STL_MEMBER_TEMPLATES
下面的两个构造函数:
template <class _InputIterator> map(_InputIterator __first, _InputIterator __last) : _M_t(_Compare(), allocator_type())
{ _M_t.insert_unique(__first, __last); }
template <class _InputIterator> map(_InputIterator __first, _InputIterator __last, const _Compare& __comp, const allocator_type& __a = allocator_type())
: _M_t(__comp, __a) { _M_t.insert_unique(__first, __last); }
这俩构造的功能和上面的两个是一样的,都是从一个map的部分区间来定义另外一个map。
//construction
map(const map<_Key,_Tp,_Compare,_Alloc>& __x) : _M_t(__x._M_t) {}
map<_Key,_Tp,_Compare,_Alloc>&operator = (const map<_Key, _Tp, _Compare, _Alloc>& __x)
{
_M_t = __x._M_t;
return *this;
}
复制构造是比较简单的定义的方法,如下:
我们使用data来创建一个新的map-data3
std::map<std::string, std::string> data3 {data};
需要注意的是,采用复制构造的map的compare必须和参数的相同。也就是说如果data自定义了排序函数,那么data2也必须使用相同的排序函数。
以上,我们通过map对象的构造函数来看了下怎么定义一个map。下面我们先通过一个简单的例子来看下上面的这几种构造方法,毕竟实验是检验真理的唯一有效途径。
#include
#include
#include
using namespace std;
int main(int argc, char* argv[])
{
map<string, string> data {make_pair("a", "A"), make_pair("b", "B"), make_pair("c", "C"), make_pair("d", "D")};
for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << endl;
}
map<string, string> data2 { ++data.begin(), --data.end()}; //另一个map的部分迭代器区间
for(map<string, string>::iterator itor = data2.begin(); itor != data2.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << endl;
}
map<string, string> data3 {data}; //移动复制构造
for(map<string, string>::iterator itor = data3.begin(); itor != data3.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << endl;
}
//自定义排序函数
map<string, string, greater<string>> data4 {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};
for(map<string, string>::iterator itor = data4.begin(); itor != data4.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << endl;
}
map<string, string, greater<string> > data5 {data4};
for(map<string, string>::iterator itor = data5.begin(); itor != data5.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << endl;
}
return 0;
}
map的成员函数大多数都是已经被RB-tree 实现的,而map只是做了中间调用。
成员方法 | 功能 |
---|---|
find(key) | 在map容器中找键值为key的键值对是否存在,如果存在,返回指向该键值对的迭代器,如果不存在,则返回map最后一个元素所在位置的后一个位置的迭代器,如果map被const修饰,则迭代器也为const。 |
lower_bound(key) | 返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(key) | 返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(key) | 返回一个范围 pair |
empty() | 空 ?true :false |
size() | 实际存有的键值对个数 |
max_size() | 返回 map 容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同。 |
operator[] | 重载[]运算符,下标为key。 |
at(key) | 找到 map 容器中 key 键对应的值,该函数会引起out_of_rang的异常(找不到的情况下)。 |
insert() | 向 map 容器中插入键值对。 |
erase() | 删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。后续章节还会对该方法做重点讲解。 |
swap() | 交换 2 个 map 容器中存储的键值对,这意味着,操作的 2 个键值对的类型必须相同。 |
clear() | 清空 map 容器中所有的键值对,即使 map 容器的 size() 为 0。 |
emplace() | 在当前 map 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。 返回值是一个pair |
emplace_hint() | 在本质上和 emplace() 在 map 容器中构造新键值对的方式是一样的,不同之处,第一个参数必须时迭代器,指定在什么位置添加键值对,但好像没什么用,因为map的中排序函数会进行自动排序。 |
count(key) | 在当前 map 容器中,查找键为 key 的键值对的个数并返回。map中只返回0或1(键值对唯一) |
我们主要看下有关迭代器的成员函数,这些成员函数和上面的基本一样,已经被RB-tree实现,map只是做了过程调用。着重说下他的反向迭代器。rbegin();
这是一个比较方便的能够遍历容器的方法,因为我曾经在vector上遍历的时候,没有记起来这个方法,导致从尾向前进行了迭代器的减法,以至于在过程中出现了一下不必要的麻烦。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个元素的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
cend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
首先我们看下重载运算符[]在map中的使用。前面我们已经知道了,map重载了运算符[],带给我们的便利了就是我们能够像操作数组一样来操作map。我们通过一个简单的例子来看下:
#include
#include
#include
using namespace std;
int main(int argc, char* argv[])
{
map<string, string> data {make_pair("a", "A"), make_pair("b", "B"), make_pair("c", "C"), make_pair("d", "D")};
data["e"] = "E";
cout << data["e"] << " " << data["f"] << endl;
for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << endl;
}
return 0;
}
结果如下:
接下来我们看下在STL源码中是怎么定义重载运算符[]的。
_Tp& operator[](const key_type& __k) {
iterator __i = lower_bound(__k);
// __i->first is greater than or equivalent to __k.
if (__i == end() || key_comp()(__k, (*__i).first))
__i = insert(__i, value_type(__k, _Tp()));
return (*__i).second;
}
看定义,如果能够找到键值为key的键值对,则返回该键值对的实值。如果找不到,则先插入一条空类型的键值对,然后返回该键值对的实值。
并且,该函数的返回值是引用,因此,我们可以通过赋值的方法来给他赋值。
首先看下他的函数定义。
pair<iterator,bool> insert(const value_type& __x)
{ return _M_t.insert_unique(__x); }
函数返回一个键值对pair
std::map<std::string, std::string> data;
data.insert({"a", "A"});
函数原型:
iterator insert(iterator position, const value_type& __x)
{ return _M_t.insert_unique(position, __x); }
上面的这个函数则指定了插入的位置,返回值跟上面的差不多,返回的是插入之后或者已经存在的位置的迭代器。其实,指不指定位置都一样,如果指定了位置,会先按照指定的位置进行插入,插入之后,如果map底层发现破坏了他的有序性,则会触发自己的排序函数进行排序。
std::map<std::string, std::string> data;
data.insert(data.bengin(), {"a", "A"});
void insert(const value_type* __first, const value_type* __last) {
_M_t.insert_unique(__first, __last);
}
void insert(const_iterator __first, const_iterator __last) {
_M_t.insert_unique(__first, __last);
}
上面的函数都是一样的操作,插入另一个map的部分。
std::map<std::string, std::string> data {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};
std::map<std::string, std::string> data2;
data2.insert(++data.bengin(), --data.end());
同样的,我们还需要了解一下定义在宏__STL_MEMBER_TEMPLATES
下面的insert函数:
template <class _InputIterator>
void insert(_InputIterator __first, _InputIterator __last) {
_M_t.insert_unique(__first, __last);
}
跟声明map时一样,我们还可以插入一个pair
std::map<std::string, std::string> data;
data2.insert({ {"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"} });
先来看下函数定义:
template<typename... _Args>
std::pair<iterator, bool>
emplace(_Args&&... __args)
{ return _M_t._M_emplace_unique(std::forward<_Args>(__args)...); }
参数 (Args&&… args) 指的是,这里只需要将创建新键值对所需的数据作为参数直接传入即可,此方法可以自行利用这些数据构建出指定的键值对。另外,该方法的返回值也是一个 pair 对象,其中 pair.first 为一个迭代器,pair.second 为一个 bool 类型变量:
先来看下函数定义:
template<typename... _Args>
iterator
emplace_hint(const_iterator __pos, _Args&&... __args)
{
return _M_t._M_emplace_hint_unique(__pos,
std::forward<_Args>(__args)...);
}
显然和 emplace 语法格式相比,有以下 2 点不同:
通过下面的例子来看下上面的这几种插入数据的方法。
#include
#include
#include
using namespace std;
int main(int argc, char* argv[])
{
map<string, string> data {make_pair("a", "A"), make_pair("b", "B"), make_pair("c", "C"), make_pair("d", "D")};
data["e"] = "E";
cout << data["e"] << " " << data["f"] << endl;
for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << " ";
}
cout << endl;
data.insert({"g", "G"});
data.insert(++data.begin(), {"h", "H"});
data.insert({{"i", "I"}, {"j", "J"}});
auto pr = data.emplace("k", "K");
data.emplace_hint(pr.first, "l", "L");
for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
{
cout << itor->first << ' ' << itor->second << " ";
}
cout << endl;
map<string, string> data2;
data2.insert(++data.begin(), --data.end());
for(map<string, string>::iterator itor = data2.begin(); itor != data2.end(); ++itor)
{
cout << itor->first << " " << itor->second << " ";
}
return 0;
}
上面我们看了插入数据的几中方法,其中主要的有3个成员函数,那么,这3个函数之间有什么区别呢?我们主要通过下面的一个例子来看下:
#include
#include
#include
using namespace std;
class MapValue{
public:
MapValue(int num) : m_number(num) {
std::cout << "construction..." << endl;
}
MapValue(MapValue&& other) : m_number(other.m_number) {
std::cout << "move construction..." << endl;
}
private:
int m_number;
};
int main(int argc, char* argv[])
{
map<string, MapValue> data;
// 分为两种情况来看
// cout << "insert..." << endl;
// data.insert({"a", MapValue(1)});
//
// cout << "emplace..." << endl;
// data.emplace("b", MapValue(1));
//
// cout << "emplace_hint..." << endl;
// data.emplace_hint(data.begin(), "c", MapValue(1));
cout << "insert..." << endl;
data.insert({"a", 1});
cout << "emplace..." << endl;
data.emplace("b", 1);
cout << "emplace_hint..." << endl;
data.emplace_hint(data.begin(), "c", 1);
return 0;
}
从上面的结果来看,(以注释掉的代码运行结果做分析)调用后insert函数时,程序总共调用了类的构造函数一次、移动构造函数两次。而emplace和emplace_hint函数都只调用了一次构造和一次移动构造。
如果我们让编译器自己做数据类型推导时发现,都会少一次移动构造。
缺少的是在最后插入的一次移动构造。
可以得出结论:
emplace
和emplace_hint
在效率上明显高于insert
方法。所以在map插入数据时,尽量使用emplace
和emplace_hint
代替 insert
。
上面insert方法的三次调用我们可以看作是:
MapValue val = MapValue(1);
auto pair = make_pair<"a", val>;
data.insert(pair);
std::make_pair
模板形成pair对象pair,调用一次移动构造函数data::insert(pair<>)
进行数据插入,调用一次移动构造以上,stl-map基本上已经学完了。
会用-明理-掌握。