在《C++ STL容器》一节中讲到,C++ 容器大致分为 2 类,即序列式容器和关联式容器。其中,序列式容器(包括 array、vector、list、deque 和 forward_list)已经在前面章节中做了详细的介绍,从本节开始,将逐个对 C++ STL 标准库中的所有关联式容器做详细的讲解。
提到 C++ STL 关联式容器,读者可能会以下一些疑问:
别急,读完本文,这些疑问都会迎刃而解。
通过学习所有的序列式容器不难发现,无论是哪种序列式容器,其存储的都是 C++ 基本数据类型(诸如 int、double、float、string 等)或使用结构体自定义类型的元素。例如,如下是一个存储 int 类型元素的 vector 容器:
std::vector primes {2, 3, 5, 7, 11, 13, 17, 19};
关联式容器则大不一样,此类容器在存储元素值的同时,还会为各元素额外再配备一个值(又称为“键”,其本质也是一个 C++ 基础数据类型或自定义类型的元素),它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。
弃用序列式容器,转而选用关联式容器存储元素,往往就是看中了关联式容器可以快速查找、读取或者删除所存储的元素,同时该类型容器插入元素的效率也比序列式容器高。
也就是说,使用关联式容器存储的元素,都是一个一个的“键值对”(
注意,关联式容器所具备的这些特性,归咎于 STL 标准库在实现该类型容器时,底层选用了 「红黑树」这种数据结构来组织和存储各个键值对。有关红黑树组织和存储数据的方式,我们已经在数据结构中做了详细的介绍,读者可猛击《红黑树》一文做详细了解。
C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset,其各自的特点如表 1 所示。
关联式容器名称 | 特点 |
---|---|
map | 定义在 头文件中,使用该容器存储的数据,其各个元素的键必须是唯一的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序(调用 std::less)。 |
set | 定义在 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less)。 |
multimap | 定义在 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的键可以重复。 |
multiset | 定义在 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的值可以重复(一旦值重复,则意味着键也是重复的)。 |
除此之外,C++ 11 还新增了 4 种哈希容器,即 unordered_map、unordered_multimap 以及 unordered_set、unordered_multiset。严格来说,它们也属于关联式容器,但由于哈希容器底层采用的是哈希表,而不是红黑树,因此本教程将它们分开进行讲解(有关哈希容器,将放在后续章节做详细讲解)。
为了让读者直观地认识到关联式容器的特性,这里为 map 容器为例,编写了一个样例(如下所示)。对于该程序,读者只需体会关联式容器的特性即可,无需纠结 map 容器的具体用法。
#include
#include
程序输出结果为:
http://c.biancheng.net/c/ => C语言教程
http://c.biancheng.net/java/ => Java教程
http://c.biancheng.net/python/ => Python教程
通过分析该程序的执行过程不难看出,mymap 关联式容器中的存储了以下 3 个键值对:
<“http://c.biancheng.net/c/”, “C语言教程”>
<“http://c.biancheng.net/python/”, “Python教程”>
<“http://c.biancheng.net/java/”, “Java教程”>
但需要注意的一点是,由于 map 容器在存储元素时,会根据各个元素键的大小自动调整元素的顺序(默认按照升序排序),因此该容器最终存储的元素顺序为:
<“http://c.biancheng.net/c/”, “C语言教程”>
<“http://c.biancheng.net/java/”, “Java教程”>
<“http://c.biancheng.net/python/”, “Python教程”>
有关 map 容器以及表 1 中其它关联式容器的具体用法,后续章节会做详细介绍。
我们知道,关联式容器存储的是“键值对”形式的数据,比如:
<“C语言教程”, “http://c.biancheng.net/c/”>
<“Python教程”, “http://c.biancheng.net/python/”>
<“Java教程”, “http://c.biancheng.net/java/”>
如上所示,每行都表示一个键值对,其中第一个元素作为键(key),第二个元素作为值(value)。
注意,基于各个关联式容器存储数据的特点,只有各个键值对中的键和值全部对应相等时,才能使用 set 和 multiset 关联式容器存储,否则就要选用 map 或者 multimap 关联式容器。
考虑到“键值对”并不是普通类型数据,C++ STL 标准库提供了 pair 类模板,其专门用来将 2 个普通元素 first 和 second(可以是 C++ 基本数据类型、结构体、类自定的类型)创建成一个新元素
。通过其构成的元素格式不难看出,使用 pair 类模板来创建“键值对”形式的元素,再合适不过。
注意,pair 类模板定义在
头文件中,所以在使用该类模板之前,需引入此头文件。另外值得一提的是,在 C++ 11 标准之前,pair 类模板中提供了以下 3 种构造函数:
#1) 默认构造函数,即创建空的 pair 对象
pair();
#2) 直接使用 2 个元素初始化成 pair 对象
pair (const first_type& a, const second_type& b);
#3) 拷贝(复制)构造函数,即借助另一个 pair 对象,创建新的 pair 对象
template pair (const pair& pr);
在 C++ 11 标准中,在引入右值引用的基础上,pair 类模板中又增添了如下 2 个构造函数:
#4) 移动构造函数
template pair (pair&& pr);
#5) 使用右值引用参数,创建 pair 对象
template pair (U&& a, V&& b);
除此之外,C++ 11 标准中 pair 类模板还新增加了如下一种构造函数:
pair (piecewise_construct_t pwc, tuple
first_args, tuple second_args); ,该构造 pair 类模板的方式很少用到,因此本节不再对其进行详细介绍,感兴趣的读者可自行查阅资料。
下面程序演示了以上几种创建 pair 对象的方法:
#include
#include // pair
#include // string
using namespace std;
int main() {
// 调用构造函数 1,也就是默认构造函数
pair pair1;
// 调用第 2 种构造函数
pair pair2("STL教程","http://c.biancheng.net/stl/");
// 调用拷贝构造函数
pair pair3(pair2);
//调用移动构造函数
pair pair4(make_pair("C++教程", "http://c.biancheng.net/cplus/"));
// 调用第 5 种构造函数
pair pair5(string("Python教程"), string("http://c.biancheng.net/python/"));
cout << "pair1: " << pair1.first << " " << pair1.second << endl;
cout << "pair2: "<< pair2.first << " " << pair2.second << endl;
cout << "pair3: " << pair3.first << " " << pair3.second << endl;
cout << "pair4: " << pair4.first << " " << pair4.second << endl;
cout << "pair5: " << pair5.first << " " << pair5.second << endl;
return 0;
}
程序输出结果为:
pair1: 0
pair2: STL教程 http://c.biancheng.net/stl/
pair3: STL教程 http://c.biancheng.net/stl/
pair4: C++教程 http://c.biancheng.net/cplus/
pair5: Python教程 http://c.biancheng.net/python/
上面程序在创建 pair4 对象时,调用了 make_pair() 函数,它也是 头文件提供的,其功能是生成一个 pair 对象。因此,当我们将 make_pair() 函数的返回值(是一个临时对象)作为参数传递给 pair() 构造函数时,其调用的是移动构造函数,而不是拷贝构造函数。
在上面程序的基础上,C++ 11 还允许我们手动为 pair1 对象赋值,比如:
pair1.first = "Java教程";
pair1.second = "http://c.biancheng.net/java/";
cout << "new pair1: " << pair1.first << " " << pair1.second << endl;
执行结果为:
new pair1: Java教程 http://c.biancheng.net/java/
同时,上面程序中 pair4 对象的创建过程,还可以写入如下形式,它们是完全等价的:
pair pair4 = make_pair("C++教程", "http://c.biancheng.net/cplus/");
cout << "pair4: " << pair4.first << " " << pair4.second << endl;
头文件中除了提供创建 pair 对象的方法之外,还为 pair 对象重载了 <、<=、>、>=、==、!= 这 6 的运算符,其运算规则是:对于进行比较的 2 个 pair 对象,先比较 pair.first 元素的大小,如果相等则继续比较 pair.second 元素的大小。
注意,对于进行比较的 2 个 pair 对象,其对应的键和值的类型比较相同,否则将没有可比性,同时编译器提示没有相匹配的运算符,即找不到合适的重载运算符。
举个例子:
#include
#include // pair
#include // string
using namespace std;
int main() {
pair pair1("STL教程", 20);
pair pair2("C++教程", 20);
pair pair3("C++教程", 30);
//pair1和pair2的key不同,value相同
if (pair1 != pair2) {
cout << "pair != pair2" << endl;
}
//pair2和pair3的key相同,value不同
if (pair2 != pair3) {
cout << "pair2 != pair3" << endl;
}
return 0;
}
程序执行结果为:
pair != pair2
pair2 != pair3
最后需要指出的是,pair类模板还提供有一个 swap() 成员函数,能够互换 2 个 pair 对象的键值对,其操作成功的前提是这 2 个 pair 对象的键和值的类型要相同。例如:
#include
#include // pair
#include // string
using namespace std;
int main() {
pair pair1("pair", 10);
pair pair2("pair2", 20);
//交换 pair1 和 pair2 的键值对
pair1.swap(pair2);
cout << "pair1: " << pair1.first << " " << pair1.second << endl;
cout << "pair2: " << pair2.first << " " << pair2.second << endl;
return 0;
}
程序执行结果为:
pair1: pair2 20
pair2: pair 10
作为关联式容器的一种,map 容器存储的都是 pair 对象,也就是用 pair 类模板创建的键值对。其中,各个键值对的键和值可以是任意数据类型,包括 C++ 基本数据类型(int、double 等)、使用结构体或类自定义的类型。
通常情况下,map 容器中存储的各个键值对都选用 string 字符串作为键的类型。
与此同时,在使用 map 容器存储多个键值对时,该容器会自动根据各键值对的键的大小,按照既定的规则进行排序。默认情况下,map 容器选用std::less
排序规则(其中 T 表示键的数据类型),其会根据键的大小对所有键值对做升序排序。当然,根据实际情况的需要,我们可以手动指定 map 容器的排序规则,既可以选用 STL 标准库中提供的其它排序规则(比如std::greater
),也可以自定义排序规则。
关于如何自定义 map 容器的排序规则,后续章节会做详细讲解。
另外需要注意的是,使用 map 容器存储的各个键值对,键的值既不能重复也不能被修改。换句话说,map 容器中存储的各个键值对不仅键的值独一无二,键的类型也会用 const 修饰,这意味着只要键值对被存储到 map 容器中,其键的值将不能再做任何修改。
前面提到,map 容器存储的都是 pair 类型的键值对元素,更确切的说,该容器存储的都是 pair
类型(其中 K 和 T 分别表示键和值的数据类型)的键值对元素。
map 容器定义在 头文件中,并位于 std 命名空间中。因此,如果想使用 map 容器,代码中应包含如下语句:
#include
注意,第二行代码不是必需的,如果不用,则后续程序中在使用 map 容器时,需手动注明 std 命名空间(强烈建议初学者使用)。
map 容器的模板定义如下:
template < class Key, // 指定键(key)的类型
class T, // 指定值(value)的类型
class Compare = less, // 指定排序规则
class Alloc = allocator > // 指定分配器对象的类型
> class map;
可以看到,map 容器模板有 4 个参数,其中后 2 个参数都设有默认值。大多数场景中,我们只需要设定前 2 个参数的值,有些场景可能会用到第 3 个参数,但最后一个参数几乎不会用到。
map 容器的模板类中包含多种构造函数,因此创建 map 容器的方式也有多种,下面就几种常用的创建 map 容器的方法,做一一讲解。
std::mapmyMap;
如果程序中已经默认指定了 std 命令空间,这里可以省略
std::
。
通过此方式创建出的 myMap 容器,初始状态下是空的,即没有存储任何键值对。鉴于空 map 容器可以根据需要随时添加新的键值对,因此创建空 map 容器是比较常用的。
std::mapmyMap{ {"C语言教程",10},{"STL教程",20} };
由此,myMap 容器在初始状态下,就包含有 2 个键值对。
再次强调,map 容器中存储的键值对,其本质都是 pair 类模板创建的 pair 对象。因此,下面程序也可以创建出一模一样的 myMap 容器:
std::mapmyMap{std::make_pair("C语言教程",10),std::make_pair("STL教程",20)};
std::mapnewMap(myMap);
由此,通过调用 map 容器的拷贝(复制)构造函数,即可成功创建一个和 myMap 完全一样的 newMap 容器。
C++ 11 标准中,还为 map 容器增添了移动构造函数。当有临时的 map 对象作为参数,传递给要初始化的 map 容器时,此时就会调用移动构造函数。举个例子:
#创建一个会返回临时 map 对象的函数
std::map disMap() {
std::maptempMap{ {"C语言教程",10},{"STL教程",20} };
return tempMap;
}
//调用 map 类模板的移动构造函数创建 newMap 容器
std::mapnewMap(disMap());
注意,无论是调用复制构造函数还是调用拷贝构造函数,都必须保证这 2 个容器的类型完全一致。
std::mapmyMap{ {"C语言教程",10},{"STL教程",20} };
std::mapnewMap(++myMap.begin(), myMap.end());
这里,通过调用 map 容器的双向迭代器,实现了在创建 newMap 容器的同时,将其初始化为包含一个 {“STL教程”,20} 键值对的容器。
有关 map 容器迭代器,后续章节会做详细讲解。
因此,如下 2 行创建 map 容器的方式,其实是等价的:
std::mapmyMap{ {"C语言教程",10},{"STL教程",20} };
std::map >myMap{ {"C语言教程",10},{"STL教程",20} };
以上 2 中创建方式生成的 myMap 容器,其内部键值对排列的顺序为:
<“C语言教程”, 10>
<“STL教程”, 20>
下面程序手动修改了 myMap 容器的排序规则,令其作降序排序:
std::map >myMap{ {"C语言教程",10},{"STL教程",20} };
此时,myMap 容器内部键值对排列的顺序为:
<“STL教程”, 20>
<“C语言教程”, 10>
在某些特定场景中,我们还需要为 map 容器自定义排序规则,此部分知识后续将利用整整一节做重点讲解。
表 1 列出了 map 容器提供的常用成员方法以及各自的功能。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
find(key) | 在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(key) | 返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(key) | 返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(key) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的键为 key 的键值对(map 容器键值对唯一,因此该范围最多包含一个键值对)。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前 map 容器中存有键值对的个数。 |
max_size() | 返回 map 容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同。 |
operator[] | map容器重载了 [] 运算符,只要知道 map 容器中某个键值对的键的值,就可以向获取数组中元素那样,通过键直接获取对应的值。 |
at(key) | 找到 map 容器中 key 键对应的值,如果找不到,该函数会引发 out_of_range 异常。 |
insert() | 向 map 容器中插入键值对。 |
erase() | 删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。后续章节还会对该方法做重点讲解。 |
swap() | 交换 2 个 map 容器中存储的键值对,这意味着,操作的 2 个键值对的类型必须相同。 |
clear() | 清空 map 容器中所有的键值对,即使 map 容器的 size() 为 0。 |
emplace() | 在当前 map 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。 |
emplace_hint() | 在本质上和 emplace() 在 map 容器中构造新键值对的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示键值对生成位置的迭代器,并作为该方法的第一个参数。 |
count(key) | 在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。 |
下面的样例演示了表 1 中部分成员方法的用法:
#include
#include
程序执行结果为:
myMap size==3
STL教程 http://c.biancheng.net/stl/
Python教程 http://c.biancheng.net/python/
C语言教程 http://c.biancheng.net/c/
有关表 1 中其它成员函数的用法,后续章节会做详细详解。
无论是前面学习的序列式容器,还是关联式容器,要想实现遍历操作,就必须要用到该类型容器的迭代器。当然,map 容器也不例外。
C++ STL 标准库为 map 容器配备的是双向迭代器(bidirectional iterator)。这意味着,map 容器迭代器只能进行 ++p、p++、–p、p–、*p 操作,并且迭代器之间只能使用 == 或者 != 运算符进行比较。
值得一提的是,相比序列式容器,map 容器提供了更多的成员方法(如表 1 所示),通过调用它们,我们可以轻松获取具有指定含义的迭代器。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
find(key) | 在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(key) | 返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(key) | 返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(key) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的键为 key 的键值对(map 容器键值对唯一,因此该范围最多包含一个键值对)。 |
表 1 中多数的成员方法,诸如 begin()、end() 等,在学习序列式容器时已经多次使用过,它们的功能如图 2 所示。
图 2 表 1 部分成员方法的功能示意图
注意,图中 Ei 表示的是 pair 类对象,即键值对。对于 map 容器来说,每个键值对的键的值都必须保证是唯一的。
下面程序以 begin()/end() 组合为例,演示了如何遍历 map 容器:
#include
#include
程序执行结果为:
C语言教程 http://c.biancheng.net/c/
STL教程 http://c.biancheng.net/stl/
读者可自行尝试使用其他组合(如 cbegin()/cend()、 rbegin()/rend() 等)遍历 map 容器。
除此之外,map 类模板中还提供了 find() 成员方法,它能帮我们查找指定 key 值的键值对,如果成功找到,则返回一个指向该键值对的双向迭代器;反之,其功能和 end() 方法相同。
举个例子:
#include
#include
程序执行结果为:
Java教程 http://c.biancheng.net/java/
STL教程 http://c.biancheng.net/stl/
此程序中,创建并初始化的 myMap 容器,默认会根据各键值对中键的值,对各键值对做升序排序,其排序的结果为:
<“C语言教程”,“http://c.biancheng.net/c/”>
<“Java教程”,“http://c.biancheng.net/java/”>
<“STL教程”,“http://c.biancheng.net/stl/”>
在此基础上,通过调用 find() 方法,我们可以得到一个指向键为 “Java教程” 的键值对的迭代器,由此当使用 for 循环从该迭代器出开始遍历时,就只会遍历到最后 2 个键值对。
同时,map 类模板中还提供有 lower_bound(key) 和 upper_bound(key) 成员方法,它们的功能是类似的,唯一的区别在于:
下面程序演示了它们的功能:
#include
#include
程序执行结果为:
lower:Java教程 http://c.biancheng.net/java/
upper:STL教程 http://c.biancheng.net/stl/
lower_bound(key) 和 upper_bound(key) 更多用于 multimap 容器,在 map 容器中很少用到。
equal_range(key) 成员方法可以看做是 lower_bound(key) 和 upper_bound(key) 的结合体,该方法会返回一个 pair 对象,其中的 2 个元素都是迭代器类型,其中 pair.first 实际上就是 lower_bound(key) 的返回值,而 pair.second 则等同于 upper_bound(key) 的返回值。
显然,equal_range(key) 成员方法表示的一个范围,位于此范围中的键值对,其键的值都为 key。举个例子:
#include
#include //pair
#include
程序执行结果为:
C语言教程 http://c.biancheng.net/c/
和 lower_bound(key)、upper_bound(key) 一样,该方法也更常用于 multimap 容器,因为 map 容器中各键值对的键的值都是唯一的,因此通过 map 容器调用此方法,其返回的范围内最多也只有 1 个键值对。
我们知道,map 容器中存储的都是 pair 类型的键值对,但几乎在所有使用 map 容器的场景中,经常要做的不是找到指定的 pair 对象(键值对),而是从该容器中找到某个键对应的值。
注意,使用 map 容器存储的各个键值对,其键的值都是唯一的,因此指定键对应的值最多有 1 个。
庆幸的是,map 容器的类模板中提供了以下 2 种方法,可直接获取 map 容器指定键对应的值。
[ ]
运算符进行了重载,这意味着,类似于借助数组下标可以直接访问数组中元素,通过指定的键,我们可以轻松获取 map 容器中该键对应的值。举个例子:
#include
#include
程序执行结果为:
http://c.biancheng.net/c/
可以看到,在第 11 行代码中,通过指定键的值为 “C语言教程”,借助重载的 [ ] 运算符,就可以在 myMap 容器中直接找到该键对应的值。
注意,只有当 map 容器中确实存有包含该指定键的键值对,借助重载的 [ ] 运算符才能成功获取该键对应的值;反之,若当前 map 容器中没有包含该指定键的键值对,则此时使用 [ ] 运算符将不再是访问容器中的元素,而变成了向该 map 容器中增添一个键值对。其中,该键值对的键用 [ ] 运算符中指定的键,其对应的值取决于 map 容器规定键值对中值的数据类型,如果是基本数据类型,则值为 0;如果是 string 类型,其值为 “”,即空字符串(即使用该类型的默认值作为键值对的值)。
举个例子:
#include
#include
程序执行结果为:
C语言教程 0
显然,对于空的 myMap 容器来说,其内部没有以 “C语言教程” 为键的键值对,这种情况下如果使用 [ ] 运算符获取该键对应的值,其功能就转变成了向该 myMap 容器中添加一个<"C语言教程",0>
键值对(由于 myMap 容器规定各个键值对的值的类型为 int,该类型的默认值为 0)。
实际上,[ ] 运算符确实有“为 map 容器添加新键值对”的功能,但前提是要保证新添加键值对的键和当前 map 容器中已存储的键值对的键都不一样。例如:
#include
#include
程序执行结果为:
Python教程 http://c.biancheng.net/python/
STL教程 http://c.biancheng.net/stl/
注意,程序中第 9 行代码已经为 map 容器添加了一个以 “STL教程” 作为键的键值对,则第 11 行代码的作用就变成了修改该键对应的值,而不再是为 map 容器添加新键值对。
举个例子:
#include
#include
程序执行结果为:
http://c.biancheng.net/c/
程序第 12 行代码处,通过 myMap 容器调用 at() 成员方法,可以成功找到键为 “C语言教程” 的键值对,并返回该键对应的值;而第 14 行代码,由于当前 myMap 容器中没有以 “Python教程” 为键的键值对,会导致 at() 成员方法查找失败,并抛出 out_of_range 异常。
除了可以直接获取指定键对应的值之外,还可以借助 find() 成员方法间接实现此目的。和以上 2 种方式不同的是,该方法返回的是一个迭代器,即如果查找成功,该迭代器指向查找到的键值对;反之,则指向 map 容器最后一个键值对之后的位置(和 end() 成功方法返回的迭代器一样)。
举个例子:
#include
#include
程序执行结果为:
C语言教程 http://c.biancheng.net/c/
注意,此程序中如果 find() 查找失败,会导致第 13 行代码运行出错。因为当 find() 方法查找失败时,其返回的迭代器指向的是容器中最后一个键值对之后的位置,即不指向任何有意义的键值对,也就没有所谓的 first 和 second 成员了。
如果以上方法都不适用,我们还可以遍历整个 map 容器,找到包含指定键的键值对,进而获取该键对应的值。比如:
#include
#include
程序执行结果为:
C语言教程 http://c.biancheng.net/c/
本节所介绍的几种方法中,仅从“在 map 容器存储的键值对中,获取指定键对应的值”的角度出发,更推荐使用 at() 成员方法,因为该方法既简单又安全。
前面讲过,C++ STL map 类模板中对[ ]
运算符进行了重载,即根据使用场景的不同,借助[ ]
运算符可以实现不同的操作。举个例子:
#include
#include
程序执行结果为:
http://c.biancheng.net/java/
Python教程 http://c.biancheng.net/python/
STL教程 http://c.biancheng.net/stl/
可以看到,当操作对象为 map 容器中已存储的键值对时,则借助 [ ] 运算符,既可以获取指定键对应的值,还能对指定键对应的值进行修改;反之,若 map 容器内部没有存储以 [ ] 运算符内指定数据为键的键值对,则使用 [ ] 运算符会向当前 map 容器中添加一个新的键值对。
实际上,除了使用 [ ] 运算符实现向 map 容器中添加新键值对外,map 类模板中还提供有 insert() 成员方法,该方法专门用来向 map 容器中插入新的键值对。
注意,这里所谓的“插入”,指的是 insert() 方法可以将新的键值对插入到 map 容器中的指定位置,但这与 map 容器会自动对存储的键值对进行排序并不冲突。当使用 insert() 方法向 map 容器的指定位置插入新键值对时,其底层会先将新键值对插入到容器的指定位置,如果其破坏了 map 容器的有序性,该容器会对新键值对的位置进行调整。
自 C++ 11 标准后,insert() 成员方法的用法大致有以下 4 种。
//1、引用传递一个键值对
pair
//2、以右值引用的方式传递键值对
template
pair
其中,val 参数表示键值对变量,同时该方法会返回一个 pair 对象,其中 pair.first 表示一个迭代器,pair.second 为一个 bool 类型变量:
以上 2 种语法格式的区别在于传递参数的方式不同,即无论是局部定义的键值对变量还是全局定义的键值对变量,都采用普通引用传递的方式;而对于临时的键值对变量,则以右值引用的方式传参。有关右值引用,可阅读《C++右值引用》一文做详细了解。
举个例子:
#include
#include
程序执行结果为:
ret.iter = <{STL教程, http://c.biancheng.net/stl/}, 1>
ret.iter = <{C语言教程, http://c.biancheng.net/c/}, 1>
ret.iter = <{STL教程, http://c.biancheng.net/stl/}, 0>
从执行结果中不难看出,程序中共执行了 3 次插入操作,其中成功了 2 次,失败了 1 次:
另外,在程序中的第 21 行代码,还可以使用如下 2 种方式创建临时的键值对变量,它们是等价的:
//调用 pair 类模板的构造函数
ret = mymap.insert(pair{ "C语言教程","http://c.biancheng.net/c/" });
//调用 make_pair() 函数
ret = mymap.insert(make_pair("C语言教程", "http://c.biancheng.net/c/"));
//以普通引用的方式传递 val 参数
iterator insert (const_iterator position, const value_type& val);
//以右值引用的方式传递 val 键值对参数
template
iterator insert (const_iterator position, P&& val);
其中 val 为要插入的键值对变量。注意,和第 1 种方式的语法格式不同,这里 insert() 方法返回的是迭代器,而不再是 pair 对象:
举个例子:
#include
#include
程序执行结果为:
STL教程 http://c.biancheng.net/stl/
C语言教程 http://c.biancheng.net/c/
STL教程 http://c.biancheng.net/stl/
再次强调,即便指定了新键值对的插入位置,map 容器仍会对存储的键值对进行排序。也可以说,决定新插入键值对位于 map 容器中位置的,不是 insert() 方法中传入的迭代器,而是新键值对中键的值。
template
void insert (InputIterator first, InputIterator last);
其中 first 和 last 都是迭代器,它们的组合
可以表示某 map 容器中的指定区域。
举个例子:
#include
#include
程序执行结果为:
Java教程 http://c.biancheng.net/java/
STL教程 http://c.biancheng.net/stl/
此程序中,
void insert ({val1, val2, …});
其中,vali 都表示的是键值对变量。
举个例子:
#include
#include
程序执行结果为:
C语言教程 http://c.biancheng.net/c/
Java教程 http://c.biancheng.net/java/
STL教程 http://c.biancheng.net/stl/
值得一提的是,除了 insert() 方法,map 类模板还提供 emplace() 和 emplace_hint() 方法,它们也可以完成向 map 容器中插入键值对的操作,且效率还会 insert() 方法高。关于这 2 个方法,会在下一节做详细介绍。
通过前面的学习我们知道,map 容器模板类中提供有 operator[ ] 和 insert() 这 2 个成员方法,而值得一提的是,这 2 个方法具有相同的功能,它们既可以实现向 map 容器中添加新的键值对元素,也可以实现更新(修改)map 容器已存储键值对的值。
举个例子(程序一):
#include
#include
程序执行结果为:
old mymap:http://c.biancheng.net/java/
new mymap:http://c.biancheng.net/stl/
old ret.iter = <{Java教程, http://c.biancheng.net/python/}, 1>
new ret.iter =
有关程序中 operator[ ] 和 insert() 成员方法的具体用法,读者可翻阅前面的文章做详细了解,这里不再做过多解释。
显然,map 模板类中 operator[ ] 和 insert() 的功能发生了重叠,这就产生了一个问题,谁的执行效率更高呢?
总的来说,读者可记住这样一条结论:当实现“向 map 容器中添加新键值对元素”的操作时,insert() 成员方法的执行效率更高;而在实现“更新 map 容器指定键值对的值”的操作时,operator[ ] 的效率更高。
至于为什么,有兴趣的读者可继续往下阅读。
首先解释一下,为什么实现向 map 容器中添加新键值对元素,insert() 方法的执行效率比 operator[ ] 更高?回顾程序一中,如下语句完成了向空 mymap 容器添加新的键值对元素:
mymap["STL教程"] = "http://c.biancheng.net/java/";
此行代码中,mymap[“STL教程”] 实际上是 mymap.operator 的缩写(底层调用的 operator[ ] 方法),该方法会返回一个指向 “STL教程” 对应的 value 值的引用。
但需要注意的是,由于此时 mymap 容器是空的,并没有 “STL教程” 对应的 value 值。这种情况下,operator[ ] 方法会默认构造一个 string 对象,并将其作为 “STL教程” 对应的 value 值,然后返回一个指向此 string 对象的引用。在此基础上,代码还会将 “http://c.biancheng.net.java/” 赋值给这个 string 对象。
也就是说,上面这行代码的执行流程,可以等效为如下程序:
typedef map mstr;
//创建要添加的默认键值对元素
pairres = mymap.insert(mstr::value_type("STL教程", string()));
//将新键值对的值赋值为指定的值
res.first->second = "http://c.biancheng.net/java/";
注意,这里的 value_type(K,T) 指的是 map 容器中存储元素的类型,其实际上就等同于 pair
。
可以看到,使用 operator[ ] 添加新键值对元素的流程是,先构造一个有默认值的键值对,然后再为其 value 赋值。
那么,为什么不直接构造一个要添加的键值对元素呢,比如:
mymap.insert(mstr::value_type("STL教程", "http://c.biancheng.net/java/"));
此行代码和上面程序的执行效果完全相同,但它省略了创建临时 string 对象的过程以及析构该对象的过程,同时还省略了调用 string 类重载的赋值运算符。由于可见,同样是完成向 map 容器添加新键值对,insert() 方法比 operator[ ] 的执行效率更高。
仍以程序一中的代码为例,如下分别是 operator[ ] 和 insert() 实现更新 mymap 容器中指定键对应的值的代码:
//operator[]
mymap["STL教程"] = "http://c.biancheng.net/stl/";
//insert()
std::pair STL = { "Java教程","http://c.biancheng.net/python/" };
mymap.insert(STL).first->second = "http://c.biancheng.net/java/";
仅仅从语法形式本身来考虑,或许已经促使很多读者选择 operator[ ] 了。接下来,我们再从执行效率的角度对比以上 2 种实现方式。
从上面代码可以看到,insert() 方法在进行更新操作之前,需要有一个 pair 类型(也就是 map::value_type 类型)元素做参数。这意味着,该方法要多构造一个 pair 对象(附带要构造 2 个 string 对象),并且事后还要析构此 pair 对象(附带 2 个 string 对象的析构)。
而和 insert() 方法相比,operator[ ] 就不需要使用 pair 对象,自然不需要构造(并析构)任何 pair 对象或者 string 对象。因此,对于更新已经存储在 map 容器中键值对的值,应优先使用 operator[ ] 方法。
学习 map insert() 方法时提到,C++ STL map 类模板中还提供了 emplace() 和 emplace_hint() 成员函数,也可以实现向 map 容器中插入新的键值对。本节就来讲解这 2 个成员方法的用法。
值得一提的是,实现相同的插入操作,无论是用 emplace() 还是 emplace_hont(),都比 insert() 方法的效率高(后续章节会详细讲解)。
和 insert() 方法相比,emplace() 和 emplace_hint() 方法的使用要简单很多,因为它们各自只有一种语法格式。其中,emplace() 方法的语法格式如下:
template
pair
参数 (Args&&… args) 指的是,这里只需要将创建新键值对所需的数据作为参数直接传入即可,此方法可以自行利用这些数据构建出指定的键值对。另外,该方法的返回值也是一个 pair 对象,其中 pair.first 为一个迭代器,pair.second 为一个 bool 类型变量:
下面程序演示 emplace() 方法的具体用法:
#include
#include
程序执行结果为:
1、ret.iter = <{STL教程, http://c.biancheng.net/stl/}, 1>
2、ret.iter = <{C语言教程, http://c.biancheng.net/c/}, 1>
3、ret.iter = <{STL教程, http://c.biancheng.net/stl/}, 0>
可以看到,程序中共执行了 3 次向 map 容器插入键值对的操作,其中前 2 次都成功了,第 3 次由于要插入的键值对的键和 map 容器中已存在的键值对的键相同,因此插入失败。
emplace_hint() 方法的功能和 emplace() 类似,其语法格式如下:
template
iterator emplace_hint (const_iterator position, Args&&… args);
显然和 emplace() 语法格式相比,有以下 2 点不同:
下面程序演示 emplace_hint() 方法的用法:
#include
#include //map
#include //string
using namespace std;
int main()
{
//创建并初始化 map 容器
std::mapmymap;
//指定在 map 容器插入键值对
map::iterator iter = mymap.emplace_hint(mymap.begin(),"STL教程", "http://c.biancheng.net/stl/");
cout << iter->first << " " << iter->second << endl;
iter = mymap.emplace_hint(mymap.begin(), "C语言教程", "http://c.biancheng.net/c/");
cout << iter->first << " " << iter->second << endl;
//插入失败样例
iter = mymap.emplace_hint(mymap.begin(), "STL教程", "http://c.biancheng.net/java/");
cout << iter->first << " " << iter->second << endl;
return 0;
}
程序执行结果为:
STL教程 http://c.biancheng.net/stl/
C语言教程 http://c.biancheng.net/c/
STL教程 http://c.biancheng.net/stl/
注意,和 insert() 方法一样,虽然 emplace_hint() 方法指定了插入键值对的位置,但 map 容器为了保持存储键值对的有序状态,可能会移动其位置。
那么,为什么 emplace() 和 emplace_hint() 方法的执行效率,比 insert() 高呢?下一节会做详细解释。
上一节在学习 C++STL map 容器的 emplace() 和 emplace_hint() 的基本用法时,还遗留了一个问题,即为什么 emplace() 和 emplace_hint() 的执行效率会比 insert() 高?
原因很简单,它们向 map 容器插入键值对时,底层的实现方式不同:
也就是说,向 map 容器中插入键值对时,emplace() 和 emplace_hint() 方法都省略了移动键值对的过程,因此执行效率更高。下面程序提供了有利的证明:
#include
#include //map
#include //string
using namespace std;
class testDemo
{
public:
testDemo(int num) :num(num) {
std::cout << "调用构造函数" << endl;
}
testDemo(const testDemo& other) :num(other.num) {
std::cout << "调用拷贝构造函数" << endl;
}
testDemo(testDemo&& other) :num(other.num) {
std::cout << "调用移动构造函数" << endl;
}
private:
int num;
};
int main()
{
//创建空 map 容器
std::mapmymap;
cout << "insert():" << endl;
mymap.insert({ "http://c.biancheng.net/stl/", testDemo(1) });
cout << "emplace():" << endl;
mymap.emplace( "http://c.biancheng.net/stl/:", 1);
cout << "emplace_hint():" << endl;
mymap.emplace_hint(mymap.begin(), "http://c.biancheng.net/stl/", 1);
return 0;
}
程序输出结果为:
insert():
调用构造函数
调用移动构造函数
调用移动构造函数
emplace():
调用构造函数
emplace_hint():
调用构造函数
分析一下这个程序。首先,我们创建了一个存储
从输出结果可以看出,在使用 insert() 方法向 map 容器插入键值对时,整个插入过程调用了 1 次 tempDemo 类的构造函数,同时还调用了 2 次移动构造函数。实际上,程序第 28 行代码底层的执行过程,可以分解为以下 3 步:
//构造类对象
testDemo val = testDemo(1); //调用 1 次构造函数
//构造键值对
auto pai = make_pair("http://c.biancheng.net/stl/", val); //调用 1 次移动构造函数
//完成插入操作
mymap.insert(pai); //调用 1 次移动构造函数
而完成同样的插入操作,emplace() 和 emplace_hint() 方法都只调用了 1 次构造函数,这足以证明,这 2 个方法是在 map 容器内部直接构造的键值对。
因此,在实现向 map 容器中插入键值对时,应优先考虑使用 emplace() 或者 emplace_hint()。
在掌握 C++ STL map 容器的基础上,本节再讲一个和 map 相似的关联式容器,即 multimap 容器。
所谓“相似”,指的是 multimap 容器具有和 map 相同的特性,即 multimap 容器也用于存储 pair
和 map 容器一样,实现 multimap 容器的类模板也定义在
头文件,并位于 std 命名空间中。因此,在使用 multimap 容器前,程序应包含如下代码:
#include
using namespace std;
注意,第二行代码不是必需的,但若不用,则程序中在使用 multimap 容器时需手动注明 std 命名空间(强烈建议初学者使用)。
multimap 容器类模板的定义如下:
template < class Key, // 指定键(key)的类型
class T, // 指定值(value)的类型
class Compare = less, // 指定排序规则
class Alloc = allocator > // 指定分配器对象的类型
> class multimap;
可以看到,multimap 容器模板有 4 个参数,其中后 2 个参数都设有默认值。
大多数场景中,我们只需要设定前 2 个参数的值,有些场景可能会用到第 3 个参数,但最后一个参数几乎不会用到。
multimap 类模板内部提供有多个构造函数,总的来说,创建 multimap 容器的方式可归为以下 5 种。
std::multimap
如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
//创建并初始化 multimap 容器
multimapmymultimap{ {"C语言教程", "http://c.biancheng.net/c/"},
{"Python教程", "http://c.biancheng.net/python/"},
{"STL教程", "http://c.biancheng.net/stl/"} };
注意,使用此方式初始化 multimap 容器时,其底层会先将每一个{key, value}
创建成 pair 类型的键值对,然后再用已建好的各个键值对初始化 multimap 容器。
实际上,我们完全可以先手动创建好键值对,然后再用其初始化 multimap 容器。下面程序使用了 2 种方式创建 pair 类型键值对,再用其初始化 multimap 容器,它们是完全等价的:
//借助 pair 类模板的构造函数来生成各个pair类型的键值对
multimapmymultimap{
pair{"C语言教程", "http://c.biancheng.net/c/"},
pair{ "Python教程", "http://c.biancheng.net/python/"},
pair{ "STL教程", "http://c.biancheng.net/stl/"}
};
//调用 make_pair() 函数,生成键值对元素
//创建并初始化 multimap 容器
multimapmymultimap{
make_pair("C语言教程", "http://c.biancheng.net/c/"),
make_pair("Python教程", "http://c.biancheng.net/python/"),
make_pair("STL教程", "http://c.biancheng.net/stl/")
};
multimapnewmultimap(mymultimap);
由此,就成功创建一个和 mymultimap 完全一样的 newmultimap 容器。
在 C++ 11 标准中,还为 multimap 类增添了移动构造函数。即当有临时的 multimap 容器作为参数初始化新 multimap 容器时,其底层就会调用移动构造函数来实现初始化操作。举个例子:
//创建一个会返回临时 multimap 对象的函数
multimap dismultimap() {
multimaptempmultimap{ {"C语言教程", "http://c.biancheng.net/c/"},{"Python教程", "http://c.biancheng.net/python/"} };
return tempmultimap;
}
//调用 multimap 类模板的移动构造函数创建 newMultimap 容器
multimapnewmultimap(dismultimap());
上面程序中,由于 dismultimap() 函数返回的 tempmultimap 容器是一个临时对象,因此在实现初始化 newmultimap 容器时,底层调用的是 multimap 容器的移动构造函数,而不再是拷贝构造函数。
注意,无论是调用复制构造函数还是调用拷贝构造函数,都必须保证这 2 个容器的类型完全一致。
//创建并初始化 multimap 容器
multimapmymultimap{ {"C语言教程", "http://c.biancheng.net/c/"},
{"Python教程", "http://c.biancheng.net/python/"},
{"STL教程", "http://c.biancheng.net/stl/"} };
multimapnewmultimap(++mymultimap.begin(), mymultimap.end());
这里使用了 multimap 容器的迭代器,选取了 mymultimap 容器中的最后 2 个键值对,用于初始化 newmultimap 容器。
multimap 容器迭代器,和 map 容器迭代器的用法完全相同,这里不再赘述。
std::less
,这意味着以下 2 种创建 multimap 容器的方式是等价的:multimapmymultimap{ {'a',1},{'b',2} };
multimap>mymultimap{ {'a',1},{'b',2} };
mymultimap 容器中键值对的存储顺序为:
下面程序利用了 STL 模板库提供的std::greater
排序函数,实现令 multimap 容器对存储的键值对做降序排序:
multimap>mymultimap{ {'a',1},{'b',2} };
其内部键值对的存储顺序为:
在某些特定场景中,我们还可以为 multimap 容器自定义排序规则,此部分知识后续将利用整整一节做重点讲解。
表 1 列出了 multimap 类模板提供的常用成员方法及各自的功能。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)键值对的双向迭代器。如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。 |
find(key) | 在 multimap 容器中查找首个键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(key) | 返回一个指向当前 multimap 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(key) | 返回一个指向当前 multimap 容器中第一个大于 key 的键值对的迭代器。如果 multimap 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(key) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的键为 key 的键值对。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前 multimap 容器中存有键值对的个数。 |
max_size() | 返回 multimap 容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同。 |
insert() | 向 multimap 容器中插入键值对。 |
erase() | 删除 multimap 容器指定位置、指定键(key)值或者指定区域内的键值对。 |
swap() | 交换 2 个 multimap 容器中存储的键值对,这意味着,操作的 2 个键值对的类型必须相同。 |
clear() | 清空 multimap 容器中所有的键值对,使 multimap 容器的 size() 为 0。 |
emplace() | 在当前 multimap 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。 |
emplace_hint() | 在本质上和 emplace() 在 multimap 容器中构造新键值对的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示键值对生成位置的迭代器,并作为该方法的第一个参数。 |
count(key) | 在当前 multimap 容器中,查找键为 key 的键值对的个数并返回。 |
和 map 容器相比,multimap 未提供 at() 成员方法,也没有重载 [] 运算符。这意味着,map 容器中通过指定键获取指定指定键值对的方式,将不再适用于 multimap 容器。其实这很好理解,因为 multimap 容器中指定的键可能对应多个键值对,而不再是 1 个。
另外值的一提的是,由于 multimap 容器可存储多个具有相同键的键值对,因此表 1 中的 lower_bound()、upper_bound()、equal_range() 以及 count() 成员方法会经常用到。
下面例子演示了表 1 中部分成员方法的用法:
#include
#include //map
using namespace std;
int main()
{
//创建并初始化 multimap 容器
multimapmymultimap{ {'a',10},{'b',20},{'b',15}, {'c',30} };
//输出 mymultimap 容器存储键值对的数量
cout << mymultimap.size() << endl;
//输出 mymultimap 容器中存储键为 'b' 的键值对的数量
cout << mymultimap.count('b') << endl;
for (auto iter = mymultimap.begin(); iter != mymultimap.end(); ++iter) {
cout << iter->first << " " << iter->second << endl;
}
return 0;
}
程序执行结果为:
4
2
a 10
b 20
b 15
c 30
注意,只要是 multimap 容器提供的成员方法,map 容器都提供,并且它们的用法是相同的。前面章节中已经对 map 容器提供的成员方法做了详细的讲解,因此这里不再对表 1 中其它的成员方法做详细的介绍。
前面章节讲解了 map 容器和 multimap 容器的用法,类似地,C++ STL 标准库中还提供有 set 和 multiset 这 2 个容器,它们也属于关联式容器。不过,本节先讲解 set 容器,后续章节再讲解 multiset 容器。
和 map、multimap 容器不同,使用 set 容器存储的各个键值对,要求键 key 和值 value 必须相等。
举个例子,如下有 2 组键值对数据:
{<‘a’, 1>, <‘b’, 2>, <‘c’, 3>}
{<‘a’, ‘a’>, <‘b’, ‘b’>, <‘c’, ‘c’>}
显然,第一组数据中各键值对的键和值不相等,而第二组中各键值对的键和值对应相等。对于 set 容器来说,只能存储第 2 组键值对,而无法存储第一组键值对。
基于 set 容器的这种特性,当使用 set 容器存储键值对时,只需要为其提供各键值对中的 value 值(也就是 key 的值)即可。仍以存储上面第 2 组键值对为例,只需要为 set 容器提供 {‘a’,‘b’,‘c’} ,该容器即可成功将它们存储起来。
通过前面的学习我们知道,map、multimap 容器都会自行根据键的大小对存储的键值对进行排序,set 容器也会如此,只不过 set 容器中各键值对的键 key 和值 value 是相等的,根据 key 排序,也就等价为根据 value 排序。
另外,使用 set 容器存储的各个元素的值必须各不相同。更重要的是,从语法上讲 set 容器并没有强制对存储元素的类型做 const 修饰,即 set 容器中存储的元素的值是可以修改的。但是,C++ 标准为了防止用户修改容器中元素的值,对所有可能会实现此操作的行为做了限制,使得在正常情况下,用户是无法做到修改 set 容器中元素的值的。
对于初学者来说,切勿尝试直接修改 set 容器中已存储元素的值,这很有可能破坏 set 容器中元素的有序性,最正确的修改 set 容器中元素值的做法是:先删除该元素,然后再添加一个修改后的元素。
值得一提的是,set 容器定义于
头文件,并位于 std 命名空间中。因此如果想在程序中使用 set 容器,该程序代码应先包含如下语句:
#include
using namespace std;
注意,第二行代码不是必需的,如果不用,则后续程序中在使用 set 容器时,需手动注明 std 命名空间(强烈建议初学者使用)。
set 容器的类模板定义如下:
template < class T, // 键 key 和值 value 的类型
class Compare = less, // 指定 set 容器内部的排序规则
class Alloc = allocator // 指定分配器对象的类型
> class set;
注意,由于 set 容器存储的各个键值对,其键和值完全相同,也就意味着它们的类型相同,因此 set 容器类模板的定义中,仅有第 1 个参数用于设定存储数据的类型。
对于 set 类模板中的 3 个参数,后 2 个参数自带默认值,且几乎所有场景中只需使用前 2 个参数,第 3 个参数不会用到。
常见的创建 set 容器的方法,大致有以下 5 种。
std::set myset;
如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
由此就创建好了一个 set 容器,该容器采用默认的std::less
规则,会对存储的 string 类型元素做升序排序。注意,由于 set 容器支持随时向内部添加新的元素,因此创建空 set 容器的方法是经常使用的。
std::set myset{"http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/"};
由此即创建好了包含 3 个 string 元素的 myset 容器。由于其采用默认的 std::less 规则,因此其内部存储 string 元素的顺序如下所示:
“http://c.biancheng.net/java/”
“http://c.biancheng.net/python/”
“http://c.biancheng.net/stl/”
例如,在第 2 种方式创建的 myset 容器的基础上,执行如下代码:
std::set copyset(myset);
//等同于
//std::set copyset = myset
该行代码在创建 copyset 容器的基础上,还会将 myset 容器中存储的所有元素,全部复制给 copyset 容器一份。
另外,C++ 11 标准还为 set 类模板新增了移动构造函数,其功能是实现创建新 set 容器的同时,利用临时的 set 容器为其初始化。比如:
set retSet() {
std::set myset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/" };
return myset;
}
std::set copyset(retSet());
//或者
//std::set copyset = retSet();
注意,由于 retSet() 函数的返回值是一个临时 set 容器,因此在初始化 copyset 容器时,其内部调用的是 set 类模板中的移动构造函数,而非拷贝构造函数。
显然,无论是调用复制构造函数还是调用拷贝构造函数,都必须保证这 2 个容器的类型完全一致。
std::set myset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/" };
std::set copyset(++myset.begin(), myset.end());
由此初始化的 copyset 容器,其内部仅存有如下 2 个 string 字符串:
“http://c.biancheng.net/python/”
“http://c.biancheng.net/stl/”
std::less
规则。其实,借助 set 类模板定义中第 2 个参数,我们完全可以手动修改 set 容器中的排序规则。比如:std::set > myset{ "http://c.biancheng.net/java/", "http://c.biancheng.net/stl/", "http://c.biancheng.net/python/"};
通过选用 std::greater 降序规则,myset 容器中元素的存储顺序为:
“http://c.biancheng.net/stl/”
“http://c.biancheng.net/python/”
“http://c.biancheng.net/java/”
表 1 列出了 set 容器提供的常用成员方法以及各自的功能。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
find(val) | 在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(val) | 返回一个指向当前 set 容器中第一个大于或等于 val 的元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(val) | 返回一个指向当前 set 容器中第一个大于 val 的元素的迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(val) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的值为 val 的元素(set 容器中各个元素是唯一的,因此该范围最多包含一个元素)。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前 set 容器中存有元素的个数。 |
max_size() | 返回 set 容器所能容纳元素的最大个数,不同的操作系统,其返回值亦不相同。 |
insert() | 向 set 容器中插入元素。 |
erase() | 删除 set 容器中存储的元素。 |
swap() | 交换 2 个 set 容器中存储的所有元素。这意味着,操作的 2 个 set 容器的类型必须相同。 |
clear() | 清空 set 容器中所有的元素,即令 set 容器的 size() 为 0。 |
emplace() | 在当前 set 容器中的指定位置直接构造新元素。其效果和 insert() 一样,但效率更高。 |
emplace_hint() | 在本质上和 emplace() 在 set 容器中构造新元素的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示新元素生成位置的迭代器,并作为该方法的第一个参数。 |
count(val) | 在当前 set 容器中,查找值为 val 的元素的个数,并返回。注意,由于 set 容器中各元素的值是唯一的,因此该函数的返回值最大为 1。 |
下面程序演示了表 1 中部分成员函数的用法:
#include
#include
#include
using namespace std;
int main()
{
//创建空set容器
std::set myset;
//空set容器不存储任何元素
cout << "1、myset size = " << myset.size() << endl;
//向myset容器中插入新元素
myset.insert("http://c.biancheng.net/java/");
myset.insert("http://c.biancheng.net/stl/");
myset.insert("http://c.biancheng.net/python/");
cout << "2、myset size = " << myset.size() << endl;
//利用双向迭代器,遍历myset
for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
cout << *iter << endl;
}
return 0;
}
程序执行结果为:
1、myset size = 0
2、myset size = 3
http://c.biancheng.net/java/
http://c.biancheng.net/python/
http://c.biancheng.net/stl/
有关表 1 中其它成员方法的用法,后续章节会做详细讲解。
和 map 容器不同,C++ STL 中的 set 容器类模板中未提供 at() 成员函数,也未对 [] 运算符进行重载。因此,要想访问 set 容器中存储的元素,只能借助 set 容器的迭代器。
值得一提的是,C++ STL 标准库为 set 容器配置的迭代器类型为双向迭代器。这意味着,假设 p 为此类型的迭代器,则其只能进行 ++p、p++、–p、p–、*p 操作,并且 2 个双向迭代器之间做比较,也只能使用 == 或者 != 运算符。
在 set 容器类模板提供的所有成员函数中,返回迭代器的成员函数如表 1 所示。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。通常和 rbegin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
find(val) | 在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(val) | 返回一个指向当前 set 容器中第一个大于或等于 val 的元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(val) | 返回一个指向当前 set 容器中第一个大于 val 的元素的迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(val) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的值为 val 的元素(set 容器中各个元素是唯一的,因此该范围最多包含一个元素)。 |
注意,以上成员函数返回的迭代器,指向的只是 set 容器中存储的元素,而不再是键值对。另外,以上成员方法返回的迭代器,无论是 const 类型还是非 const 类型,都不能用于修改 set 容器中的值。
图 2 演示了表 1 中除最后 4 个成员函数外,其它几个成员函数的具体功能。
图 2 set容器迭代器功能示意图
其中,Ei 表示 set 容器中存储的各个元素,它们的值各不相同。
下面程序以 begin()/end() 为例,演示了如何使用图 2 中相关迭代器遍历 set 容器:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化set容器
std::set myset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/"
};
//利用双向迭代器,遍历myset
for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
cout << *iter << endl;
}
return 0;
}
程序执行结果为:
http://c.biancheng.net/java/
http://c.biancheng.net/python/
http://c.biancheng.net/stl/
再次强调,正如程序第 15 行代码所示的那样,因为 iter 迭代器指向的是 set 容器存储的某个元素,而不是键值对,因此通过 *iter 可以直接获取该迭代器指向的元素的值。
除此之外,如果只想遍历 set 容器中指定区域内的部分数据,则可以借助 find()、lower_bound() 以及 upper_bound() 实现。通过调用它们,可以获取一个指向指定元素的迭代器。
需要特别指出的是,equal_range(val) 函数的返回值是一个 pair 类型数据,其包含 2 个迭代器,表示 set 容器中和指定参数 val 相等的元素所在的区域,但由于 set 容器中存储的元素各不相等,因此该函数返回的这 2 个迭代器所表示的范围中,最多只会包含 1 个元素。
举个例子:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化set容器
std::set myset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/"
};
set::iterator iter = myset.find("http://c.biancheng.net/python/");
for (;iter != myset.end();++iter)
{
cout << *iter << endl;
}
return 0;
}
程序执行结果为:
http://c.biancheng.net/python/
http://c.biancheng.net/stl/
值得一提的是,虽然 C++ STL 标准中,set 类模板中包含 lower_bound()、upper_bound()、equal_range() 这 3 个成员函数,但它们更适用于 multiset 容器,几乎不会用于操作 set 容器。
通过前面的学习,我们已经学会如何创建一个 set 容器。在此基础上,如果想向 set 容器中继续添加元素,可以借助 set 类模板提供的 insert() 方法。
为满足不同场景的需要,C++ 11 标准的 set 类模板中提供了多种不同语法格式的 insert() 成员方法,它们各自的功能和用法如下所示。
//普通引用方式传参
pair
//右值引用方式传参
pair
其中,val 表示要添加的新元素,该方法的返回值为 pair 类型。
以上 2 种格式的区别仅在于传递参数的方式不同,即第一种采用普通引用的方式传参,而第二种采用右值引用的方式传参。右值引用为 C++ 11 新添加的一种引用方式,可阅读《C++ 右值引用》一文做详细了解。
可以看到,以上 2 种语法格式的 insert() 方法,返回的都是 pair 类型的值,其包含 2 个数据,一个迭代器和一个 bool 值:
举个例子:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化set容器
std::set myset;
//准备接受 insert() 的返回值
pair::iterator, bool> retpair;
//采用普通引用传值方式
string str = "http://c.biancheng.net/stl/";
retpair = myset.insert(str);
cout << "iter->" << *(retpair.first) << " " << "bool = " << retpair.second << endl;
//采用右值引用传值方式
retpair = myset.insert("http://c.biancheng.net/python/");
cout << "iter->" << *(retpair.first) << " " << "bool = " << retpair.second << endl;
return 0;
}
程序执行结果为:
iter->http://c.biancheng.net/stl/ bool = 1
iter->http://c.biancheng.net/python/ bool = 1
通过观察输出结果不难看出,程序中两次借助 insert() 方法向 set 容器中添加元素,都成功了。
//以普通引用的方式传递 val 值
iterator insert (const_iterator position, const value_type& val);
//以右值引用的方式传递 val 值
iterator insert (const_iterator position, value_type&& val);
以上 2 种语法格式中,insert() 函数的返回值为迭代器:
举个例子:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化set容器
std::set myset;
//准备接受 insert() 的返回值
set::iterator iter;
//采用普通引用传值方式
string str = "http://c.biancheng.net/stl/";
iter = myset.insert(myset.begin(),str);
cout << "myset size =" << myset.size() << endl;
//采用右值引用传值方式
iter = myset.insert(myset.end(),"http://c.biancheng.net/python/");
cout << "myset size =" << myset.size() << endl;
return 0;
}
程序执行结果为:
myset size =1
myset size =2
注意,使用 insert() 方法将目标元素插入到 set 容器指定位置后,如果该元素破坏了容器内部的有序状态,set 容器还会自行对新元素的位置做进一步调整。也就是说,insert() 方法中指定新元素插入的位置,并不一定就是该元素最终所处的位置。
insert() 方法的语法格式如下:
template
void insert (InputIterator first, InputIterator last);
其中 first 和 last 都是迭代器,它们的组合 [first,last) 可以表示另一 set 容器中的一块区域,该区域包括 first 迭代器指向的元素,但不包含 last 迭代器指向的元素。
举个例子:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化set容器
std::set myset{ "http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/",
"http://c.biancheng.net/java/" };
//创建一个同类型的空 set 容器
std::set otherset;
//利用 myset 初始化 otherset
otherset.insert(++myset.begin(), myset.end());
//输出 otherset 容器中的元素
for (auto iter = otherset.begin(); iter != otherset.end(); ++iter) {
cout << *iter << endl;
}
return 0;
}
程序执行结果为:
http://c.biancheng.net/python/
http://c.biancheng.net/stl/
注意,程序第 15 行在初始化 otherset 容器时,选取的是 myset 容器中从第 2 个元素开始(包括此元素)直到容器末尾范围内的所有元素,所以程序输出结果中只有 2 个字符串。
void insert ( {E1, E2,…,En} );
其中,Ei 表示新添加的元素。
举个例子:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化set容器
std::set myset;
//向 myset 中添加多个元素
myset.insert({ "http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/",
"http://c.biancheng.net/java/" });
for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
cout << *iter << endl;
}
return 0;
}
程序执行结果为:
http://c.biancheng.net/java/
http://c.biancheng.net/python/
http://c.biancheng.net/stl/
以上的讲解,即为 set 类模板中 insert() 成员方法的全部用法。指的一提的是,C++ 11 标准的 set 类模板中,还提供有另外 2 个成员方法,分别为 implace() 和 implace_hint() 方法,借助它们不但能实现向 set 容器添加新元素的功能,其实现效率也比 insert() 成员方法更高。
有关 set 类模板中 implace() 和 implace_hint() 方法的用法,后续章节会做详细介绍。
如果想删除 set 容器存储的元素,可以选择用 erase() 或者 clear() 成员方法。
set 类模板中,erase() 方法有 3 种语法格式,分别如下:
//删除 set 容器中值为 val 的元素
size_type erase (const value_type& val);
//删除 position 迭代器指向的元素
iterator erase (const_iterator position);
//删除 [first,last) 区间内的所有元素
iterator erase (const_iterator first, const_iterator last);
其中,第 1 种格式的 erase() 方法,其返回值为一个整数,表示成功删除的元素个数;后 2 种格式的 erase() 方法,返回值都是迭代器,其指向的是 set 容器中删除元素之后的第一个元素。
注意,如果要删除的元素就是 set 容器最后一个元素,则 erase() 方法返回的迭代器就指向新 set 容器中最后一个元素之后的位置(等价于 end() 方法返回的迭代器)。
下面程序演示了以上 3 种 erase() 方法的用法:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化 set 容器
std::setmyset{1,2,3,4,5};
cout << "myset size = " << myset.size() << endl;
//1) 调用第一种格式的 erase() 方法
int num = myset.erase(2); //删除元素 2,myset={1,3,4,5}
cout << "1、myset size = " << myset.size() << endl;
cout << "num = " << num << endl;
//2) 调用第二种格式的 erase() 方法
set::iterator iter = myset.erase(myset.begin()); //删除元素 1,myset={3,4,5}
cout << "2、myset size = " << myset.size() << endl;
cout << "iter->" << *iter << endl;
//3) 调用第三种格式的 erase() 方法
set::iterator iter2 = myset.erase(myset.begin(), --myset.end());//删除元素 3,4,myset={5}
cout << "3、myset size = " << myset.size() << endl;
cout << "iter2->" << *iter2 << endl;
return 0;
}
程序执行结果为:
myset size = 5
1、myset size = 4
num = 1
2、myset size = 3
iter->3
3、myset size = 1
iter2->5
如果需要删除 set 容器中存储的所有元素,可以使用 clear() 成员方法。该方法的语法格式如下:
void clear();
显然,该方法不需要传入任何参数,也没有任何返回值。
举个例子:
#include
#include
#include
using namespace std;
int main()
{
//创建并初始化 set 容器
std::setmyset{1,2,3,4,5};
cout << "1、myset size = " << myset.size() << endl;
//清空 myset 容器
myset.clear();
cout << "2、myset size = " << myset.size() << endl;
return 0;
}
程序执行结果为:
1、myset size = 5
2、myset size = 0
前面章节中,对 set 容器做了详细的讲解。回忆一下,set 容器具有以下几个特性:
在此基础上,C++ STL 标准库中还提供有一个和 set 容器相似的关联式容器,即 multiset 容器。所谓“相似”,是指 multiset 容器遵循 set 容器的前 3 个特性,仅在第 4 条特性上有差异。和 set 容器不同的是,multiset 容器可以存储多个值相同的元素。
也就是说,multiset 容器和 set 容器唯一的差别在于,multiset 容器允许存储多个值相同的元素,而 set 容器中只能存储互不相同的元素。
和 set 类模板一样,multiset 类模板也定义在
头文件,并位于 std 命名空间中。这意味着,如果想在程序中使用 multiset 容器,该程序代码应包含如下语句:
#include
using namespace std;
注意,第二行代码不是必需的,如果不用,则后续程序中在使用 multiset容器时,需手动注明 std 命名空间(强烈建议初学者使用)。
multiset 容器类模板的定义如下所示:
template < class T, // 存储元素的类型
class Compare = less, // 指定容器内部的排序规则
class Alloc = allocator > // 指定分配器对象的类型
> class multiset;
显然,multiset 类模板有 3 个参数,其中后 2 个参数自带有默认值。值得一提的是,在实际使用中,我们最多只需要使用前 2 个参数即可,第 3 个参数不会用到。
创建 multiset 容器,无疑需要调用 multiset 类模板中的构造函数。值得一提的是,multiset 类模板提供的构造函数,和 set 类模板中提供创建 set 容器的构造函数,是完全相同的。这意味着,创建 set 容器的方式,也同样适用于创建 multiset 容器。
考虑到一些读者可能并未系统学习 set 容器,因此这里还是对 multiset 容器的创建做一下详细的介绍。
multiset 类模板中提供了 5 种构造函数,也就代表有 5 种创建 multiset 容器的方式,分别如下。
std::multiset mymultiset;
如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
由此就创建好了一个 mymultiset 容器,该容器采用默认的std::less
规则,会对存储的 string 类型元素做升序排序。
注意,由于 multiset 容器支持随时向内部添加新的元素,因此创建空 multiset 容器的方法比较常用。
2)除此之外,multiset 类模板还支持在创建 multiset 容器的同时,对其进行初始化。例如:
std::multiset mymultiset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/" };
由此即创建好了包含 3 个 string 元素的 mymultiset 容器。由于其采用默认的std::less
规则,因此其内部存储 string 元素的顺序如下所示:
“http://c.biancheng.net/java/”
“http://c.biancheng.net/python/”
“http://c.biancheng.net/stl/”
例如,在第 2 种方式创建的 mymultiset 容器的基础上,执行如下代码:
std::multiset copymultiset(mymultiset);
//等同于
//std::multiset copymultiset = mymultiset;
该行代码在创建 copymultiset 容器的基础上,还会将 mymultiset 容器中存储的所有元素,全部复制给 copymultiset 容器一份。
另外,C++ 11 标准还为 multiset 类模板新增了移动构造函数,其功能是实现创建新 multiset 容器的同时,利用临时的 multiset 容器为其初始化。比如:
multiset retMultiset() {
std::multiset tempmultiset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/" };
return tempmultiset;
}
std::multiset copymultiset(retMultiset());
//等同于
//std::multiset copymultiset = retMultiset();
注意,由于 retMultiset() 函数的返回值是一个临时 multiset 容器,因此在初始化 copymultiset 容器时,其内部调用的是 multiset 类模板中的移动构造函数,而非拷贝构造函数。
显然,无论是调用复制构造函数还是调用拷贝构造函数,都必须保证这 2 个容器的类型完全一致。
std::multiset mymultiset{ "http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/" };
std::set copymultiset(++mymultiset.begin(), mymultiset.end());
以上初始化的 copyset 容器,其内部仅存有如下 2 个 string 字符串:
“http://c.biancheng.net/python/”
“http://c.biancheng.net/stl/”
std::less
规则。其实,借助 multiset 类模板定义中的第 2 个参数,我们完全可以手动修改 multiset 容器中的排序规则。下面样例中,使用了 STL 标准库提供的 std::greater 排序方法,作为 multiset 容器内部的排序规则:
std::multiset > mymultiset{
"http://c.biancheng.net/java/",
"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/" };
通过选用std::greater
降序规则,mymultiset 容器中元素的存储顺序为:
“http://c.biancheng.net/stl/”
“http://c.biancheng.net/python/”
“http://c.biancheng.net/java/”
multiset 容器提供的成员方法,和 set 容器提供的完全一样,如表 1 所示。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
find(val) | 在 multiset 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(val) | 返回一个指向当前 multiset 容器中第一个大于或等于 val 的元素的双向迭代器。如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(val) | 返回一个指向当前 multiset 容器中第一个大于 val 的元素的迭代器。如果 multiset 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(val) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含所有值为 val 的元素。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前 multiset 容器中存有元素的个数。 |
max_size() | 返回 multiset 容器所能容纳元素的最大个数,不同的操作系统,其返回值亦不相同。 |
insert() | 向 multiset 容器中插入元素。 |
erase() | 删除 multiset 容器中存储的指定元素。 |
swap() | 交换 2 个 multiset 容器中存储的所有元素。这意味着,操作的 2 个 multiset 容器的类型必须相同。 |
clear() | 清空 multiset 容器中所有的元素,即令 multiset 容器的 size() 为 0。 |
emplace() | 在当前 multiset 容器中的指定位置直接构造新元素。其效果和 insert() 一样,但效率更高。 |
emplace_hint() | 本质上和 emplace() 在 multiset 容器中构造新元素的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示新元素生成位置的迭代器,并作为该方法的第一个参数。 |
count(val) | 在当前 multiset 容器中,查找值为 val 的元素的个数,并返回。 |
注意,虽然 multiset 容器和 set 容器拥有的成员方法完全相同,但由于 multiset 容器允许存储多个值相同的元素,因此诸如 count()、find()、lower_bound()、upper_bound()、equal_range()等方法,更常用于 multiset 容器。
下面程序演示了表 1 中部分成员函数的用法:
#include
#include
#include
using namespace std;
int main() {
std::multiset mymultiset{1,2,2,2,3,4,5};
cout << "multiset size = " << mymultiset.size() << endl;
cout << "multiset count(2) =" << mymultiset.count(2) << endl;
//向容器中添加元素 8
mymultiset.insert(8);
//删除容器中所有值为 2 的元素
int num = mymultiset.erase(2);
cout << "删除了 " << num << " 个元素 2" << endl;
//输出容器中存储的所有元素
for (auto iter = mymultiset.begin(); iter != mymultiset.end(); ++iter) {
cout << *iter << " ";
}
return 0;
}
程序执行结果为:
multiset size = 7
multiset count(2) =3
删除了 3 个元素 2
1 3 4 5 8
注意,表 1 中大多数成员方法的用法,和 set 容器中相应成员方法的用法是完全一样的,只是调用者不同。因此,如果读者想详细了解表 1 中某个成员方法的用法,可以阅读讲解 set 容器相同成员方法的文章。
前面在讲解如何创建 map、multimap、set 以及 multiset 容器时,遗留了一个问题,即如何自定义关联式容器中的排序规则?
实际上,为关联式容器自定义排序规则的方法,已经在 《STL priority_queue自定义排序方法》一节中做了详细的讲解。换句话说,为 Priority_queue 容器适配器自定义排序规则的方法,同样适用于所有关联式容器。
总的来说,为关联式容器自定义排序规则,有以下 2 种方法。
在掌握此方法之前,读者必须对函数对象有基本的了解,可阅读《C++函数对象》一节。
无论关联式容器中存储的是基础类型(如 int、double、float 等)数据,还是自定义的结构体变量或类对象(包括 string 类),都可以使用函数对象的方式为该容器自定义排序规则。
下面样例以 set 容器为例,演示了如何用函数对象的方式自定义排序规则:
#include
#include // set
#include // string
using namespace std;
//定义函数对象类
class cmp {
public:
//重载 () 运算符
bool operator ()(const string &a,const string &b) {
//按照字符串的长度,做升序排序(即存储的字符串从短到长)
return (a.length() < b.length());
}
};
int main() {
//创建 set 容器,并使用自定义的 cmp 排序规则
std::setmyset{"http://c.biancheng.net/stl/",
"http://c.biancheng.net/python/",
"http://c.biancheng.net/java/"};
//输出容器中存储的元素
for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
cout << *iter << endl;
}
return 0;
}
程序执行结果为:
http://c.biancheng.net/stl/
http://c.biancheng.net/java/
http://c.biancheng.net/python/
重点分析一下 6~13 行代码,其定义了一个函数对象类,并在其重载 () 运算符的方法中自定义了新的排序规则,即按照字符串的长度做升序排序。在此基础上,程序第 17 行代码中,通过将函数对象类的类名 cmp 通过 set 类模板的第 2 个参数传递给 myset 容器,该容器内部排序数据的规则,就改为了以字符串的长度为标准做升序排序。
需要注意的是,此程序中创建的 myset 容器,由于是以字符串的长度为准进行排序,因此其无法存储相同长度的多个字符串。
另外,C++ 中的 struct 和 class 非常类似(有关两者区别,可阅读《C++ struct和class到底有什么区别》一文),前者也可以包含成员变量和成员函数。因此上面程序中,函数对象类 cmp 也可以使用 struct 关键字创建:
//定义函数对象类
struct cmp {
//重载 () 运算符
bool operator ()(const string &a, const string &b) {
//按照字符串的长度,做升序排序(即存储的字符串从短到长)
return (a.length() < b.length());
}
};
值得一提的是,在定义函数对象类时,也可以将其定义为模板类。比如:
//定义函数对象模板类
template
class cmp {
public:
//重载 () 运算符
bool operator ()(const T &a, const T &b) {
//按照值的大小,做升序排序
return a < b;
}
};
注意,此方式必须保证 T 类型元素可以直接使用关系运算符(比如这里的 < 运算符)做比较。
其实在 STL 标准库中,本就包含几个可供关联式容器使用的排序规则,如表 1 表示。
排序规则 | 功能 |
---|---|
std::less | 底层采用 < 运算符实现升序排序,各关联式容器默认采用的排序规则。 |
std::greater | 底层采用 > 运算符实现降序排序,同样适用于各个关联式容器。 |
std::less_equal | 底层采用 <= 运算符实现升序排序,多用于 multimap 和 multiset 容器。 |
std::greater_equal | 底层采用 >= 运算符实现降序排序,多用于 multimap 和 multiset 容器。 |
值得一提的是,表 1 中的这些排序规则,其底层也是采用函数对象的方式实现的。以 std::less 为例,其底层实现为:
template
struct less {
//定义新的排序规则
bool operator()(const T &_lhs, const T &_rhs) const {
return _lhs < _rhs;
}
}
在此基础上,当关联式容器中存储的数据类型为自定义的结构体变量或者类对象时,通过对现有排序规则中所用的关系运算符进行重载,也能实现自定义排序规则的目的。
注意,当关联式容器中存储的元素类型为结构体指针变量或者类的指针对象时,只能使用函数对象的方式自定义排序规则,此方法不再适用。
举个例子:
#include
#include // set
#include // string
using namespace std;
//自定义类
class myString {
public:
//定义构造函数,向 myset 容器中添加元素时会用到
myString(string tempStr) :str(tempStr) {};
//获取 str 私有对象,由于会被私有对象调用,因此该成员方法也必须为 const 类型
string getStr() const;
private:
string str;
};
string myString::getStr() const{
return this->str;
}
//重载 < 运算符,参数必须都为 const 类型
bool operator <(const myString &stra, const myString & strb) {
//以字符串的长度为标准比较大小
return stra.getStr().length() < strb.getStr().length();
}
int main() {
//创建空 set 容器,仍使用默认的 less 排序规则
std::setmyset;
//向 set 容器添加元素,这里会调用 myString 类的构造函数
myset.emplace("http://c.biancheng.net/stl/");
myset.emplace("http://c.biancheng.net/c/");
myset.emplace("http://c.biancheng.net/python/");
//
for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
myString mystr = *iter;
cout << mystr.getStr() << endl;
}
return 0;
}
程序执行结果为:
http://c.biancheng.net/c/
http://c.biancheng.net/stl/
http://c.biancheng.net/python/
在这个程序中,虽然 myset 容器表面仍采用默认的 std::less 排序规则,但由于我们对其所用的 < 运算符进行了重载,使得 myset 容器内部实则是以字符串的长度为基准,对各个 mystring 类对象进行排序。
另外,上面程序以全局函数的形式实现对 < 运算符的重载,还可以使用成员函数或者友元函数的形式实现。其中,当以成员函数的方式重载 < 运算符时,该成员函数必须声明为 const 类型,且参数也必须为 const 类型:
bool operator <(const myString & tempStr) const {
//以字符串的长度为标准比较大小
return this->str.length() < tempStr.str.length();
}
至于参数的传值方式是采用按引用传递还是按值传递,都可以(建议采用按引用传递,效率更高)。
同样,如果以友元函数的方式重载 < 运算符时,要求参数必须使用 const 修饰:
//类中友元函数的定义
friend bool operator <(const myString &a, const myString &b);
//类外部友元函数的具体实现
bool operator <(const myString &stra, const myString &strb) {
//以字符串的长度为标准比较大小
return stra.str.length() < strb.str.length();
}
当然,本节所讲自定义排序规则的方法并不仅仅适用于 set 容器,其它关联式容器(map、multimap、multiset)也同样适用,有兴趣的读者可自行编写代码验证。
通过前面的学习,读者已经掌握了所有关联式容器(包括 map、multimap、set 和 multiset)的特性和用法。其中需要指出的是,对于如何修改容器中某个键值对的键,所有关联式容器可以采用同一种解决思路,即先删除该键值对,然后再向容器中添加修改之后的新键值对。
那么,是否可以不删除目标键值对,而直接修改它的键呢?接下来就围绕此问题,给读者展开详细的讲解。
首先可以明确的是,map 和 multimap 容器只能采用“先删除,再添加”的方式修改某个键值对的键。原因很简单,C++ STL 标准中明确规定,map 和 multimap 容器用于存储类型为 pair
举个例子:
map mymap{ {1,10},{2,20} };
//map 容器的键为 const 类型,不能被修改
mymap.begin()->first = 100;
multimap mymultimap{ {10,100},{20,200} };
//multimap 容器的键为 const 类型,同样不能被修改
mymultimap.begin()->first = 100;
其中,第 3 行代码试图直接将 mymap 容器中 {1,10} 的键改为 100,同样第 7 行代码试图直接将 mymultimap 容器中 {10,100} 的键改为 100,它们都是不能通过编译的。
正如上面例子中演示的那样,直接修改 map 或 multimap 容器中某个键值对的键是行不通的。但对于 set 或者 multiset 容器来说,却是可行的。
和 map、multimap 不同,C++ STL 标准中并没有用 const 限定 set 和 multiset 容器中存储元素的类型。换句话说,对于 set 或者 multiset 类型的容器,其存储元素的类型是 T 而不是 const T。
事实上,对 set 和 multiset 容器中的元素类型作 const 修饰,是违背常理的。举个例子,假设我们使用 set 容器存储多个学生信息,如下是一个表示学生的类:
class student {
public:
student(string name, int id, int age) :name(name), id(id), age(age) {
}
const int& getid() const {
return id;
}
void setname(const string name){
this->name = name;
}
string getname() const{
return name;
}
void setage(int age){
this->age = age;
}
int getage() const{
return age;
}
private:
string name;
int id;
int age;
};
在创建 set 容器之前,我们还需要为其设计一个排序规则,这里假定以每个学生的 id 做升序排序,其排序规则如下:
class cmp {
public:
bool operator ()(const student &stua, const student &stub) {
//按照字符串的长度,做升序排序(即存储的字符串从短到长)
return stua.getid() < stub.getid();
}
};
做完以上所有的准备工作后,就可以创建一个可存储 student 对象的 set 容器了,比如:
set myset{ {"zhangsan",10,20},{"lisi",20,21},{"wangwu",15,19} };
由此创建的 myset 容器中,存储的数据依次为:
{“zhangsan”,10,20}
{“wangwu”,15,19}
{“lisi”,20,21}
注意,set 容器中每个元素也可以看做是键和值相等的键值对,但对于这里的 myset 容器来说,其实每个 student 对象的 id 才是真正的键,其它信息(name 和 age)只不过是和 id 绑定在一起而已。因此,在不破坏 myset 容器中元素的有序性的前提下(即不修改每个学生的 id),学生的其它信息是应该允许修改的,但有一个前提,即 myset 容器中存储的各个 student 对象不能被 const 修饰(这也是 set 容器中的元素类型不能被 const 修饰的原因)。
总之,set 和 multiset 容器的元素类型没有用 const 修饰。所以从语法的角度分析,我们可以直接修改容器中元素的值,但一定不要修改元素的键。
例如,在已创建好的 myset 容器的基础上,如下代码尝试修改 myset 容器中某个学生的 name 名字:
set::iterator iter = mymap.begin();
(*iter).setname("xiaoming");
注意,如果读者运行代码会发现,它也是无法通过编译的。
虽然 C++ STL 标准没有用 const 修饰 set 或者 multiset 容器中元素的类型,但也做了其它工作来限制用户修改容器的元素。例如上面代码中,iter 会调用 operator,其返回的是一个 const T& 类型元素。这意味着,C++ STL 标准不允许用户借助迭代器来直接修改 set 或者 multiset 容器中的元素。
那么,如何才能正确修改 set 或 multiset 容器中的元素呢?最直接的方式就是借助 const_cast 运算符,该运算符可以去掉指针或者引用的 const 限定符。
有关 const_cast 运算符的用法,由于不是本节重点,这里不再做详细讲解,有兴趣的读者可自行查阅相关资料。
比如,我们只需要借助 const_cast 运算符对上面程序稍作修改,就可以运行成功:
set::iterator iter = mymap.begin();
const_cast(*iter).setname("xiaoming");
由此,mymap 容器中的 {“zhangsan”,10,20} 就变成了 {“xiaoming”,10,20}。
再次强调,虽然使用 const_cast 能直接修改 set 或者 multiset 容器中的元素,但一定不要修改元素的键!如果要修改,只能采用“先删除,再添加”的方式。另外,不要试图以同样的方式修改 map 或者 multimap 容器中键值对的键,这违反了 C++ STL 标准的规定。
总的来说,map 和 multimap 容器中元素的键是无法直接修改的,但借助 const_cast,我们可以直接修改 set 和 multiset 容器中元素的非键部分。
为了加深读者的理解,如下是和本节内容相关的完整程序,读者可直接拷贝下来:
#include
#include
#include
using namespace std;
class student {
public:
student(string name, int id, int age) :name(name), id(id), age(age) {
}
const int& getid() const {
return id;
}
void setname(const string name){
this->name = name;
}
string getname() const{
return name;
}
void setage(int age){
this->age = age;
}
int getage() const{
return age;
}
void display()const {
cout << id << " " << name << " " << age << endl;
}
private:
string name;
int id;
int age;
};
//自定义 myset 容器的排序规则
class cmp {
public:
bool operator ()(const student &stua, const student &stub) {
//按照字符串的长度,做升序排序(即存储的字符串从短到长)
return stua.getid() < stub.getid();
}
};
int main() {
set mymap{ {"zhangsan",10,20},{"lisi",20,21},{"wangwu",15,19} };
set::iterator iter = mymap.begin();
//直接将 {"zhangsan",10,20} 中的 "zhangsan" 修改为 "xiaoming"
const_cast(*iter).setname("xiaoming");
while (iter != mymap.end()) {
(*iter).display();
++iter;
}
return 0;
}
程序执行结果为:
10 xiaoming 20
15 wangwu 19
20 lisi 21