前言:前两天我们分析了unordered_map的自定义key类型的使用,那么今天我们就来分析一下map的自定义key类型的使用。对于map容器来说,其底层是一个红黑树,那么对于红黑树,我们应该知道红黑树是一种有序的数据结构,即其顺序就是依据其key来进行排序的。因此,对于map的key来说一定是能够进行排序的类型。正是由于map的底层是红黑树,那么我们自然可以知道,对于map的数据结构类型,可以进行范围查询。而unordered_map由于是一种无序的数据结构,因此就不适用于范围查询。
template <typename _Key, typename _Tp, typename _Compare = std::less<_Key>,
typename _Alloc = std::allocator<std::pair<const _Key, _Tp> > >
class map
{
public:
typedef _Key key_type;
typedef _Tp mapped_type;
typedef std::pair<const _Key, _Tp> value_type;
typedef _Compare key_compare;
typedef _Alloc allocator_type;
......
}
我们先看一下上面map源码的定义(只截取了部分),可以看出,map的模板类型参数有四个,各个参数的含义如下:
_Key:map的key的类型
_Tp:map的value的类型
_Compare:如何对map的key进行比较的仿函数
_Alloc:map的资源的分配器
map的一般使用即我们不需要额外的任何操作就可以直接使用,从map的源码可以看出,对于map的第三和第四个参数是提供了默认参数的,所以在一般情况下我们可以直接使用该默认参数而不需要做其他的额外操作,即我们只需确定前两个数据类型即可。那么一般使用需要满足的条件如下:
1 key的类型为基本数据类型
2 key的类型为复合类型,但是该复合类型可以直接进行比较(隐含的意思就是对该复合类型重载了对应的比较方法)
默认的第三个参数为less<_Key>,即该仿函数的意思就是将key按照从小到大的顺序排列,我们看一下less的源码:
template<typename _Tp>
struct less : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x < __y; }
};
即从上面less的源码我们可以看出该less类模板重载了()运算符,因此less类是一个仿函数,并且该仿函数的返回值最终是一个bool类型,且是直接进行的key的比较。
我们简单测试一下map的一般使用的测试,即不管map的第三第四个模板参数类型,测试过程如下:
#include
#include
using namespace std;
int main() {
map<int, int> myMapInt;
myMapInt[1] = 1;
myMapInt[2] = 2;
myMapInt[3] = 3;
for (auto& v : myMapInt)
cout << v.first << " " << v.second << endl;
cout << "============================================" << endl;
map<double, int> myMapDouble;
myMapDouble[1.0] = 1;
myMapDouble[2.0] = 2;
myMapDouble[3.1] = 3;
for (auto& v : myMapDouble)
cout << v.first << " " << v.second << endl;
cout << "============================================" << endl;
map<char, int> myMapChar;
myMapChar['a'] = 1;
myMapChar['b'] = 2;
myMapChar['c'] = 3;
for (auto& v : myMapChar)
cout << (char)v.first << " " << v.second << endl;
cout << "============================================" << endl;
return 0;
}
#include
#include
#include
#include
using namespace std;
int main() {
map<vector<int>, int> myMapVec;
myMapVec[vector<int>{1, 2, 3}] = 1;
myMapVec[vector<int>{1, 2, 5}] = 2;
myMapVec[vector<int>{1, 2, 3, 7}] = 8;
for (auto& v : myMapVec)
cout << v.second << endl;
cout << "============================================" << endl;
map<string, int> myMapStr;
myMapStr.insert(make_pair(string("num1"), 1));
myMapStr.insert(make_pair(string("num2"), 1));
myMapStr.insert(make_pair(string("num3"), 1));
for (auto& v : myMapStr)
cout << v.first << " " << v.second << endl;
return 0;
}
测试结果
如上所示,可以正常的输出结果,由于myMapVec的key是一个vector的类型,因此不能直接输出,如果想输出,可以自己实现,这里就不实现了。不过,在上述代码中,有一个问题,就是在对string类型的key进行[]这个下标访问的操作来添加数据元素时会直接报错,而对于vector类型的key进行添加时没有报错。这里主要的原因可能是[]对于string,和vector进行重载地区别。因此上述代码对string类型的key是通过insert来添加的。
自定义key类型如下
struct myDefine {
myDefine(int val1 = 1, int val2 = 2)
:a(val1), b(val2) {}
int a;
int b;
};
** 初步代码尝试**
#include
#include
#include
#include
using namespace std;
struct myDefine {
myDefine(int val1 = 1, int val2 = 2)
:a(val1), b(val2) {}
int a;
int b;
};
int main() {
map<myDefine, int> myMap;
return 0;
}
对于上面的代码,我们直接编译运行时,看起来好像是没有错误出现。不过,这样只定义不使用完全没用啊,所以我们插入几行数据看一看。
#include
#include
#include
#include
using namespace std;
struct myDefine {
myDefine(int val1 = 1, int val2 = 2)
:a(val1), b(val2) {}
int a;
int b;
};
int main() {
map<myDefine, int> myMap;
myMap.insert(make_pair(myDefine(), 3));
myMap.insert(make_pair(myDefine(3, 5), 3));
myMap.insert(make_pair(myDefine(6, 2), 3));
return 0;
}
此时我们再运行时,就会直接报错。至于报错的原因,我们应该清楚。即当我们插入数据的时候,由于map是一个有序的数据结构,因此在插入数据时会进行key的比较,然而这个key是我们自定义的类型,计算机并不知道怎么处理,不知道如何比较他们的大小。因此,这就需要了解我们的第三个模板参数了,因为之前对于第三个模板参数我们一直用的是默认的模板参数,可以应对一些基础数据类型和一些复合数据类型的处理,但是对于我们自定义的key类型,我们需要做一些额外的操作。我们在开始时观察了第三个默认模板的参数可以知道,该参数是一个仿函数,且其内部实现很简单,就是直接比较两个key的类型返回。因此,我们可以有两种做法,来使得我们的自定义类型可用。第一种方法,之所以编译时报错,原因就是计算机不知道如何比较我们的key的大小,那么我们可以这样做,继续沿用第三个模板参数,因为第三个模板参数内部是直接比较key的类型大小,因此我们可以在自定义的类型里实现重载<运算符,使得计算机知道如何比较这两个key(因为默认参数是less,所以只需要重载<运算符就可以了,如果参数是greater,那么就需要重载>运算符)。第二种方法就是,我们自己实现一个仿函数,来比较这两个key。实现如下:
#include
#include
#include
#include
using namespace std;
struct myDefine {
myDefine(int val1 = 1, int val2 = 2)
:a(val1), b(val2) {}
bool operator<(const myDefine& key) const{
return a + b < key.a + key.b;
}
int a;
int b;
};
int main() {
map<myDefine, int> myMap;
myMap.insert(make_pair(myDefine(), 31));
myMap.insert(make_pair(myDefine(3, 5), 13));
myMap.insert(make_pair(myDefine(6, 8), 38));
for (auto& v : myMap)
cout << v.second << endl;
return 0;
}
测试结果
如上所示,可以进行正常的插入数据,并输出,并且我们在重载<运算符的规则时是我自己随意定义的,即比较两个属性的和,具体如何定义可以根据实际情况来自行决定。那么如果两个属性的和相同,怎么办,我们看一下下面的额测试用例:
#include
#include
#include
#include
using namespace std;
struct myDefine {
myDefine(int val1 = 1, int val2 = 2)
:a(val1), b(val2) {}
bool operator<(const myDefine& key) const{
return a + b < key.a + key.b;
}
int a;
int b;
};
int main() {
map<myDefine, int> myMap;
myMap.insert(make_pair(myDefine(), 31));
myMap.insert(make_pair(myDefine(3, 5), 13));
myMap.insert(make_pair(myDefine(6, 8), 38));
myMap.insert(make_pair(myDefine(4, 4), 388));
myMap.insert(make_pair(myDefine(0, 1), 38));
for (auto& v : myMap)
cout << "key:[" <<v.first.a << " " << v.first.b << "] value: "<< v.second << endl;
return 0;
}
测试结果
我们可以看到,对于和相同的数据,其实是没有插入成功的,并且打印结果也是有序的,即从key的最小(属性的和,我们自己定义的比较规则)到最大的顺序打印
#include
#include
#include
#include
using namespace std;
struct myDefine {
myDefine(int val1 = 1, int val2 = 2)
:a(val1), b(val2) {}
// bool operator<(const myDefine& key) const{
// return a + b < key.a + key.b;
// }
int a;
int b;
};
template<class T>
struct myCmp {
bool operator()(const T& key1, const T& key2)const {
return key1.a + key1.b < key2.a + key2.b;
}
};
int main() {
map<myDefine, int, myCmp<myDefine> > myMap;
myMap.insert(make_pair(myDefine(), 31));
myMap.insert(make_pair(myDefine(3, 5), 13));
myMap.insert(make_pair(myDefine(6, 8), 38));
myMap.insert(make_pair(myDefine(4, 4), 388));
myMap.insert(make_pair(myDefine(0, 1), 38));
for (auto& v : myMap)
cout << "key:[" <<v.first.a << " " << v.first.b << "] value: "<< v.second << endl;
return 0;
}
测试结果
我们可以看到,这个测试结果和上面的测试结果是一样的,并且我们方法二的实现与方法一的实现的区别在于下面两点:
1 定义map对象时,我们传入了自己自定义的仿函数类
2 我们不需要任何的重载运算符
3 对于方法一来讲,没有传入第三个参数,只是重载了特定的运算符
4 小总结:对于使用何种方法,其实我们可以根据自己的实现来决定,两个方法的本质含义都差不多
以上就是我们自定义map的key的全过程,并且分析了为什么有的key可以直接使用,为什么我们自定义的key却不能直接使用。并且我们从源码的角度进行了简单的剖析,更家容易里面map的设计逻辑。如果有需要,我们可以继续研究map容器的常用方法insert,和operator[]的实现,使得我们能够更加清楚的立即map的实现。
在实现自定义方法时,我们有一点需要尤其注意,就是我们必须要保证我们重载的运算符的原型和仿函数中重载的operator()原型保持一致,即必须是传入的参数是const类型,且方法也是const类型(是方法是const类型,不是返回值是const类型!!!)