第3章 C++ STL关联式容器总结

本章将介绍 STL 标准库中另一类容器,即关联式容器,包括 mapmultimapset 以及 multiset 这 4 种容器。

C++ STL关联式容器是什么?

 

 STL 标准库在实现该类型容器时,底层选用了 「红黑树」这种数据结构来组织和存储各个键值对。(个人:也就是set,map的底层实现都是红黑树)

C++ STL 标准库提供了 4 种关联式容器,分别为 mapsetmultimapmultiset,其各自的特点如表 1 所示。

表 1 C++ STL关联式容器类别
关联式容器名称特点
map 定义在 <map> 头文件中,使用该容器存储的数据,其各个元素的键必须是唯一的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序(调用 std::less<T>)。(个人:这里的排序,是指的按照键值进行排序)
set 定义在 <set> 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less<T>)
multimap 定义在 <map> 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的键可以重复
multiset 定义在 <set> 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的值可以重复(一旦值重复,则意味着键也是重复的)

除此之外,C++ 11 还新增了 4 种哈希容器,即 unordered_map、unordered_multimap 以及 unordered_set、unordered_multiset。严格来说,它们也属于关联式容器,但由于哈希容器底层采用的是哈希表,而不是红黑树,因此我们将他们放在无序关联容器中进行介绍。

#include <iostream>
#include <map> //使用 map 容器,必须引入该头文件
#include <string>

using namespace std;

int main() {
    //创建一个空的 map 关联式容器,该容器中存储的键值对,其中键为 string 字符串,值也为 string 字符串类型
    map<string, string> mymap;
    //向 mymap 容器中添加数据
    mymap["C++教程"] = "C++primer第五版";
    mymap["深度学习教程"] = "动手学习深度学习";
    mymap["java教程"] = "廖雪峰Java教程";

    //使用 map 容器的迭代器,遍历 mymap 容器,并输出其中存储的各个键值对
    for (map<string, string>::iterator it = mymap.begin(); it != mymap.end(); ++it) {
        //输出各个元素中的键和值
        cout << it->first << " => " << it->second << '\n';
    }
    return 0;
}

C++ STL pair用法详解

考虑到“键值对”并不是普通类型数据,C++ STL 标准库提供了pair类模板,其专门用来将 2 个普通元素 firstsecond(可以是 C++ 基本数据类型、结构体、类自定的类型)创建成一个新元素<first, second>。通过其构成的元素格式不难看出,使用 pair 类模板来创建“键值对”形式的元素,再合适不过。 

注意,pair 类模板定义在<utility>头文件中,所以在使用该类模板之前,需引入此头文件

  • 在 C++ 11 标准之前,pair 类模板中提供了以下 3 种构造函数:
#1) 默认构造函数,即创建空的 pair 对象
pair();
#2) 直接使用 2 个元素初始化成 pair 对象
pair (const first_type& a, const second_type& b);
#3) 拷贝(复制)构造函数,即借助另一个 pair 对象,创建新的 pair 对象
template<class U, class V> pair (const pair<U,V>& pr);
  • 在 C++ 11 标准中,在引入右值引用的基础上,pair 类模板中又增添了如下2个构造函数:(个人:总的构造方式就是默认构造函数,以各个元素为参数的左值,右值构造函数,以另一个同类型的pair对象的左值,右值构造函数,总共五个构造函数)  
#4) 移动构造函数
template<class U, class V> pair (pair<U,V>&& pr);
#5) 使用右值引用参数,创建 pair 对象
template<class U, class V> pair (U&& a, V&& b);

例子,

#include <iostream>
#include <utility>      // pair
#include <string>       // string
using namespace std;
int main() {
    // 调用构造函数 1,也就是默认构造函数
    pair <int, double> pair1;
    // 调用第 2 种构造函数
    pair <string, string> pair2("keras教程","python深度学习");
    // 调用拷贝构造函数
    pair <string, string> pair3(pair2);
    //调用移动构造函数
    pair <string, string> pair4(make_pair("Python教程", "廖雪峰Python教程"));
    // 调用第 5 种构造函数
    pair <string, string> pair5(string("计算机基础知识"), string("csapp"));

    cout << "pair1: " << pair1.first << " " << pair1.second << endl;
    pair1.first = 36;
    pair1.second = 72;
    cout << "new 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;
}

 

  • 上面程序在创建 pair4对象时,调用了 make_pair() 函数,它也是 <utility> 头文件提供的,其功能是生成一个 pair 对象。因此,当我们将 make_pair() 函数的返回值(是一个临时对象)作为参数传递给 pair() 构造函数时,其调用的是移动构造函数,而不是拷贝构造函数。(个人:也就是make_pair()是一个函数,它的返回值是一个临时对象,没有名字,是一个右值)
  • 在上面程序的中,C++ 11 还允许我们手动为 pair1 对象赋值,(个人:通过例子知道,pair这个类的成员变量first,second是public的,)
    pair1.first = 36;
    pair1.second = 72;
    cout << "new pair1: " << pair1.first << " " << pair1.second << endl;
  • 同时,上面程序中 pair4 对象的创建过程,还可以写入如下形式,它们是完全等价的:(个人:make_pair是一个函数模板,实例化时编译器可以根据它的参数推断出它的类型参数,所以可以不用显示的指定它的模板类型参数,而pair不一样,它是一个类模板,实例化时必须显示的指定它的模板类型参数)

    pair <string, string> pair4 = make_pair("python教程", "廖雪峰Python教程");
    

<utility>头文件中除了提供创建 pair 对象的方法之外,还为 pair 对象重载了 <、<=、>、>=、==、!= 这 6 的运算符,其运算规则是:对于进行比较的 2 个 pair 对象,先比较 pair.first 元素的大小,如果相等则继续比较 pair.second 元素的大小。  注意,对于进行比较的 2 个 pair 对象,其对应的键和值的类型必须相同,否则将没有可比性,同时编译器提示没有相匹配的运算符,即找不到合适的重载运算符。(个人:也就是说,我们可以对类型相同的pair进行是否相等,大小进行比较)  

#include <iostream>
#include <utility>      // pair
#include <string>       // string
using namespace std;
int main() {
    pair <string, int> pair1("STL教程", 20);
    pair <string, int> pair2("C++教程", 20);
    pair <string, int> 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类模板还提供有一个 swap() 成员函数,能够互换 2 个 pair 对象的键值对,其操作成功的前提是这 2 个 pair 对象的键和值的类型要相同。例如:

#include <iostream>
#include <utility>      // pair
#include <string>       // string
using namespace std;
int main() {
    pair <string, int> pair1("pair1", 10);
    pair <string, int> pair2("pair2", 20);
    //交换 pair1 和 pair2 的键值对
    pair1.swap(pair2);
    cout << "pair1: " << pair1.first << " " << pair1.second << endl;
    cout << "pair2: " << pair2.first << " " << pair2.second << endl;
    return 0;
}

 C++ STL map容器

  • 作为关联式容器的一种,map容器存储的都是pair对象,也就是用pair类模板创建的键值对。(个人:也就是map里面存储的元素的类型为pair类型,也就是所谓的键值对)
  • 在使用map容器存储多个键值对时,该容器会自动根据各键值对的键的大小,按照既定的规则进行排序默认情况下,map 容器选用std::less<T>排序规则(其中 T 表示键的数据类型),其会根据键的大小对所有键值对做升序排序。当然,根据实际情况的需要,我们可以手动指定 map 容器的排序规则,既可以选用 STL 标准库中提供的其它排序规则(比如std::greater<T>),也可以自定义排序规则。
  • 使用 map 容器存储的各个键值对,键的值既不能重复也不能被修改。换句话说,map 容器中存储的各个键值对不仅键的值独一无二,键的类型也会用 const 修饰,这意味着只要键值对被存储到map容器中,其键的值将不能再做任何修改。前面提到,map容器存储的都是pair类型的键值对元素,更确切的说,该容器存储的都是pair<const K, T> 类型(其中 K 和 T 分别表示键和值的数据类型)的键值对元素。

map 容器定义在 <map> 头文件中,并位于 std 命名空间中。因此,如果想使用 map 容器,代码中应包含如下语句:

#include <map>
using namespace std;

map 容器的模板定义如下:(个人:allocator,英/ˈæləʊkeɪtə/,allocator类是C++的一个模板,它提供类型化的内存分配以及对象的分配和撤销)  

template < class Key,                                     // 指定键(key)的类型
           class T,                                       // 指定值(value)的类型
           class Compare = less<Key>,                     // 指定排序规则
           class Alloc = allocator<pair<const Key,T> >    // 指定分配器对象的类型
           > class map;

可以看到,map容器模板有4个参数,其中后2个参数都设有默认值。大多数场景中,我们只需要设定前 2 个参数的值,有些场景可能会用到第 3 个参数,但最后一个参数几乎不会用到。  

创建C++ map容器的几种方法

  • 通过调用map容器类的默认构造函数,可以创建出一个空的map容器,比如:(个人:调用默认构造函数)  
std::map<std::string, int>myMap;
  • 当然在创建 map 容器的同时,也可以进行初始化,比如:(个人:使用列表初始化,由于map里面存储的元素是一个类类型,所以这里的列表初始化是列表嵌套列表,里面的列表对map存储的元素pair进行列表初始化)
std::map<std::string, int>myMap{ {"C语言教程",10},{"STL教程",20} };

再次强调,map 容器中存储的键值对,其本质都是 pair 类模板创建的 pair 对象。因此,下面程序也可以创建出一模一样的 myMap 容器:  

std::map<std::string, int>myMap{std::make_pair("C语言教程",10),std::make_pair("STL教程",20)};
  • 除此之外,在某些场景中,可以利用先前已创建好的 map 容器,再创建一个新的 map 容器。例如:(个人:使用拷贝构造的方式进行初始化) 
std::map<std::string, int>newMap(myMap);

由此,通过调用 map 容器的拷贝(复制)构造函数,即可成功创建一个和 myMap 完全一样的 newMap 容器。  

C++ 11 标准中,还为 map 容器增添了移动构造函数。当有临时的 map 对象作为参数,传递给要初始化的 map 容器时,此时就会调用移动构造函数。举个例子:(个人:函数的返回值为一个临时对象,没有名字,是右值,此时会调用移动构造函数)  

#创建一个会返回临时 map 对象的函数
std::map<std::string,int> disMap() {
    std::map<std::string, int>tempMap{ {"C语言教程",10},{"STL教程",20} };
    return tempMap;
}
//调用 map 类模板的移动构造函数创建 newMap 容器
std::map<std::string, int>newMap(disMap());

注意,无论是调用复制构造函数还是调用移动构造函数,都必须保证这 2 个容器的类型完全一致。  

  • map类模板还支持取已建map容器中指定区域内的键值对,创建并初始化新的 map 容器。例如:(个人:使用另一个map的通过迭代器指定一段范围的元素进行初始化)  
std::map<std::string, int>myMap{ {"C语言教程",10},{"STL教程",20} };
std::map<std::string, int>newMap(++myMap.begin(), myMap.end());
  • 当然,在以上几种创建map容器的基础上,我们都可以手动修改 map 容器的排序规则。默认情况下,map 容器调用 std::less<T> 规则,根据容器内各键值对的键的大小,对所有键值对做升序排序。因此,如下 2 行创建 map 容器的方式,其实是等价的:
std::map<std::string, int>myMap{ {"C语言教程",10},{"STL教程",20} };
std::map<std::string, int, std::less<std::string> >myMap{ {"C语言教程",10},{"STL教程",20} };

个人:总结下来,map的初始化方式无非就是默认初始化,列表初始化,复制/移动构造函数初始化,列表初始化,使用另一个同类型的map的通过迭代器对指定的范围进行初始化)

C++ map容器包含的成员方法

表 1 C++ 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)

个人:上面的lower_bound,upper_bound是针对

equal_range这个成员函数返回的range的左右边界而言的)

该方法返回一个 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()

个人:hint,提示)

在本质上和 emplace() 在 map 容器中构造新键值对的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示键值对生成位置的迭代器,并作为该方法的第一个参数
count(key) 在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。

 ====================================

 =========================================

pair<const_iterator,const_iterator> equal_range (const key_type& k) const;
pair<iterator,iterator> equal_range (const key_type& k);

======================================

#include <iostream>
#include <map>      // map
#include <string>       // string
using namespace std;

int main() {
    //创建空 map 容器
    std::map<std::string, std::string, std::greater<std::string>>myMap;
    //调用 emplace() 方法,直接向 myMap 容器中指定位置构造新键值对
    myMap.emplace("a教程","111");
    myMap.emplace("b教程", "222");
    myMap.emplace("c教程", "333");

    //输出当前 myMap 容器存储键值对的个数
    cout << "myMap size==" << myMap.size() << endl;
    //判断当前 myMap 容器是否为空
    if (!myMap.empty()) {
        //借助 myMap 容器迭代器,将该容器的键值对逐个输出
        cout<<"遍历元素:\n"<<endl;
        for (auto i = myMap.begin(); i != myMap.end(); ++i) {
            cout << i->first << " " << i->second << endl;
        }
    }

    return 0;
}

C++ STL map容器迭代器以及查找方法

C++ STL 标准库为 map 容器配备的是双向迭代器(bidirectional iterator)。这意味着,map 容器迭代器只能进行 ++p、p++、--p、p--、*p 操作,并且迭代器之间只能使用 == 或者 != 运算符进行比较。 

表 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 容器键值对唯一,因此该范围最多包含一个键值对)。

 

注意,图中 Ei 表示的是 pair 类对象,即键值对。对于 map 容器来说,每个键值对的键的值都必须保证是唯一的。 

find() 成员方法:  

#include <iostream>
#include <map>      // pair
#include <string>       // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教程","111"},
                                             {"C语言教程","222"},
                                             {"Java教程","333"} };
    //查找键为 "Java教程" 的键值对
    auto iter = myMap.find("Java教程");
    //从 iter 开始,遍历 map 容器
    for (; iter != myMap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}
C:\Users\guoyo\CLionProjects\cpp-exercise\cmake-build-debug\cpp_exercise.exe
Java教程 333
STL教程 111

进程已结束,退出代码0

lower_bound(key) 和 upper_bound(key) 成员方法:  

  • lower_bound(key) 返回的是指向第一个键不小于 key 的键值对的迭代器
  • upper_bound(key) 返回的是指向第一个键大于 key 的键值对的迭代器

lower_bound(key) 和 upper_bound(key) 更多用于 multimap 容器,在 map 容器中很少用到

#include <iostream>
#include <map>      // pair
#include <string>       // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教程","111"},
                                             {"C语言教程","222"},
                                             {"Java教程","333"} };
    //找到第一个键的值不小于 "Java教程" 的键值对
    auto iter = myMap.lower_bound("Java教程");
    cout << "lower:" << iter->first << " " << iter->second << endl;

    //找到第一个键的值大于 "Java教程" 的键值对
    iter = myMap.upper_bound("Java教程");
    cout <<"upper:" << iter->first << " " << iter->second << endl;
    return 0;
}

 equal_range(key) 成员方法

显然,equal_range(key) 成员方法表示的一个范围,位于此范围中的键值对,其键的值都为 key和lower_bound(key)、upper_bound(key) 一样,该方法也更常用于multimap 容器,因为 map 容器中各键值对的键的值都是唯一的,因此通过 map 容器调用此方法,其返回的范围内最多也只有 1 个键值对。

举个例子:  

#include <iostream>
#include <utility>  //pair
#include <map>      // map
#include <string>       // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<string, string>myMap{ {"STL教程","111"},
                                   {"C语言教程","222"},
                                   {"Java教程","333"} };
    //创建一个 pair 对象,来接收 equal_range() 的返回值
    pair <std::map<string, string>::iterator, std::map<string, string>::iterator> myPair = myMap.equal_range("C语言教程");
    //通过遍历,输出 myPair 指定范围内的键值对
    for (auto iter = myPair.first; iter != myPair.second; ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

 C++ STL map获取键对应值的几种方法

  • map 类模板中对[ ]运算符进行了重载,这意味着,类似于借助数组下标可以直接访问数组中元素,通过指定的键,我们可以轻松获取 map 容器中该键对应的值。注意,只有当 map 容器中确实存有包含该指定键的键值对,借助重载的 [ ] 运算符才能成功获取该键对应的值;反之,若当前 map 容器中没有包含该指定键的键值对,则此时使用 [ ] 运算符将不再是访问容器中的元素,而变成了向该 map 容器中增添一个键值对。其中,该键值对的键用 [ ] 运算符中指定的键,其对应的值取决于 map 容器规定键值对中值的数据类型,如果是基本数据类型,则值为 0;如果是 string 类型,其值为 "",即空字符串(即使用该类型的默认值作为键值对的值)。
#include <iostream>
#include <map>      // map
#include <string>   // string
using namespace std;

int main() {
    //创建空 map 容器
    std::map<std::string, int>myMap;
    int cValue = myMap["C语言教程"];
    for (auto i = myMap.begin(); i != myMap.end(); ++i) {
        cout << i->first << " "<< i->second << endl;
    }
    return 0;
}

  • 除了借助 [ ] 运算符获取 map 容器中指定键对应的值,还可以使用 at() 成员方法。和前一种方法相比,at() 成员方法也需要根据指定的键,才能从容器中找到该键对应的值;不同之处在于,如果在当前容器中查找失败,该方法不会向容器中添加新的键值对,而是直接抛出 out_of_range 异常
#include <iostream>
#include <map>      // map
#include <string>   // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教程","111"},
                                             {"C语言教程","222"},
                                             {"Java教程","333"} };

    cout << myMap.at("C语言教程") << endl;
    //下面一行代码会引发 out_of_range 异常
    //cout << myMap.at("Python教程") << endl;
    return 0;
}

  •  除了可以直接获取指定键对应的值之外,还可以借助 find() 成员方法间接实现此目的。和以上 2 种方式不同的是,该方法返回的是一个迭代器,即如果查找成功,该迭代器指向查找到的键值对;反之,则指向 map 容器最后一个键值对之后的位置(和 end() 成功方法返回的迭代器一样)。
  • 如果以上方法都不适用,我们还可以遍历整个 map 容器,找到包含指定键的键值对,进而获取该键对应的值。比如:
#include <iostream>
#include <map>      // map
#include <string>   // string
using namespace std;

int main() {
    //创建并初始化 map 容器
    std::map<std::string, std::string>myMap{ {"STL教程","111"},
                                             {"C语言教程","222"},
                                             {"Java教程","333"} };

    for (auto iter = myMap.begin(); iter != myMap.end(); ++iter) {
        //调用 string 类的 compare() 方法,找到一个键和指定字符串相同的键值对
        if (!iter->first.compare("C语言教程")) {
            cout << iter->first << " " << iter->second << endl;
        }
    }
    return 0;
}

C++ STL map insert()插入数据的4种方式

自 C++ 11 标准后,insert() 成员方法的用法大致有以下 4 种。 

  • 无需指定插入位置,直接将键值对添加到 map 容器中。insert() 方法的语法格式有以下 2 种:  
template <class P>
//1、引用传递一个键值对
pair<iterator,bool> insert (const P& val);
//2、以右值引用的方式传递键值对
    pair<iterator,bool> insert (P&& val);
    • 如果成功插入 val,则该迭代器指向新插入的 val,bool 值为 true
    • 如果插入 val 失败,则表明当前 map 容器中存有和 val 的键相同的键值对(用 p 表示),此时返回的迭代器指向 p,bool 值为 false。  

以上 2 种语法格式的区别在于传递参数的方式不同,即无论是局部定义的键值对变量还是全局定义的键值对变量,都采用普通引用传递的方式;而对于临时的键值对变量,则以右值引用的方式传参。  

#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;
int main()
{
    //创建一个空 map 容器
    std::map<string, string> mymap;

    //创建一个真实存在的键值对变量
    std::pair<string, string> STL = { "STL教程","111" };

    std::pair<std::map<string, string>::iterator, bool> ret;

    //插入 STL,由于 STL 并不是临时变量,因此会以第一种方式传参
    ret = mymap.insert(STL);
    cout << "ret.iter = <{" << ret.first->first << ", " << ret.first->second << "}, " << ret.second << ">" << endl;

    //以右值引用的方式传递临时的键值对变量
    ret = mymap.insert({ "C语言教程","222" });
    //上面的等价形式
    //调用 pair 类模板的构造函数
    //ret = mymap.insert(pair<string,string>{ "C语言教程","222" });
    //调用 make_pair() 函数
    //ret = mymap.insert(make_pair("C语言教程", "222"));
    cout << "ret.iter = <{" << ret.first->first << ", " << ret.first->second << "}, " << ret.second << ">" << endl;

    //插入失败样例
    ret = mymap.insert({ "STL教程","333" });
    cout << "ret.iter = <{" << ret.first->first << ", " << ret.first->second << "}, " << ret.second << ">" << endl;
    return 0;
}

  • 除此之外,insert() 方法还支持向 map 容器的指定位置插入新键值对,该方法的语法格式如下:
template <class P>
//以普通引用的方式传递 val 参数
iterator insert (const_iterator position, const P& val);
//以右值引用的方式传递 val 键值对参数
iterator insert (const_iterator position, P&& val);

注意,和第 1 种方式的语法格式不同,这里 insert() 方法返回的是迭代器,而不再是 pair 对象: 

    • 如果插入成功,insert() 方法会返回一个指向 map 容器中已插入键值对的迭代器
    • 如果插入失败,insert() 方法同样会返回一个迭代器,该迭代器指向 map 容器中和 val 具有相同键的那个键值对。  
#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;
int main()
{
    //创建一个空 map 容器
    std::map<string, string> mymap;

    //创建一个真实存在的键值对变量
    std::pair<string, string> STL = { "STL教程","111" };
    //指定要插入的位置
    std::map<string, string>::iterator it = mymap.begin();
    //向 it 位置以普通引用的方式插入 STL
    auto iter1 = mymap.insert(it, STL);
    cout << iter1->first << " " << iter1->second << endl;

    //向 it 位置以右值引用的方式插入临时键值对
    auto iter2 = mymap.insert(it, std::pair<string, string>("C语言教程", "222"));
    cout << iter2->first << " " << iter2->second << endl;

    //插入失败样例
    auto iter3 = mymap.insert(it, std::pair<string, string>("STL教程", "333"));
    cout << iter3->first << " " << iter3->second << endl;
    cout<<"遍历整个map:\n";
    for(auto it = mymap.begin();it!=mymap.end();++it){
        cout<<it->first<<" "<<it->second<<endl;
    }
    return 0;
}

再次强调,即便指定了新键值对的插入位置,map 容器仍会对存储的键值对进行排序。也可以说,决定新插入键值对位于 map 容器中位置的,不是 insert() 方法中传入的迭代器,而是新键值对中键的值。 

  • insert() 方法还支持向当前 map 容器中插入其它 map 容器指定区域内的所有键值对,该方法的语法格式如下:(个人:注意这个insert版本的返回值类型为void)  
template <class InputIterator>
void insert (InputIterator first, InputIterator last);

其中 first 和 last 都是迭代器,它们的组合<first,last>可以表示某 map 容器中的指定区域。  

#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;
int main()
{
    //创建并初始化 map 容器
    std::map<std::string, std::string>mymap{ {"STL教程","111"},
                                             {"C语言教程","222"},
                                             {"Java教程","333"} };
    //创建一个空 map 容器
    std::map<std::string, std::string>copymap;
    //指定插入区域
    std::map<string, string>::iterator first = ++mymap.begin();
    std::map<string, string>::iterator last = mymap.end();
    //将<first,last>区域内的键值对插入到 copymap 中
    copymap.insert(first, last);
    //遍历输出 copymap 容器中的键值对
    for (auto iter = copymap.begin(); iter != copymap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

  • 除了以上一种格式外,insert() 方法还允许一次向 map 容器中插入多个键值对,其语法格式为: (个人:注意,这个insert版本的返回值类型为void)
void insert ({val1, val2, ...});

其中,vali 都表示的是键值对变量。

#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;
int main()
{
    //创建空的 map 容器
    std::map<std::string, std::string>mymap;
    //向 mymap 容器中添加 3 个键值对
    mymap.insert({ {"STL教程", "111"},
                   { "C语言教程","222" },
                   { "Java教程","333" } });

    for (auto iter = mymap.begin(); iter != mymap.end(); ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

  

当实现“向 map 容器中添加新键值对元素”的操作时,insert() 成员方法的执行效率更高;而在实现“更新 map 容器指定键值对的值”的操作时,operator[ ] 的效率更高。 

  • 向map容器中增添新元素,insert()效率更高:  
mymap["STL教程"] = "111";

此行代码中,mymap["STL教程"] 实际上是 mymap.operator[ ](“STL教程”) 的缩写(底层调用的 operator[ ] 方法),该方法会返回一个指向 “STL教程” 对应的 value 值的引用。  但需要注意的是,由于此时 mymap 容器是空的,并没有 "STL教程" 对应的 value 值。这种情况下,operator[ ] 方法会默认构造一个 string 对象,并将其作为 "STL教程" 对应的 value 值,然后返回一个指向此 string 对象的引用。在此基础上,代码还会将 "111" 赋值给这个 string 对象。  也就是说,上面这行代码的执行流程,可以等效为如下程序:  

typedef map<string, string> mstr;
//创建要添加的默认键值对元素
pair<mstr::iterator, bool>res = mymap.insert(mstr::value_type("STL教程", string()));
//将新键值对的值赋值为指定的值
res.first->second = "111";

可以看到,使用operator[ ] 添加新键值对元素的流程是,先构造一个有默认值的键值对,然后再为其 value 赋值。  

那么,为什么不直接构造一个要添加的键值对元素呢,比如:  

mymap.insert(mstr::value_type("STL教程", "111"));

此行代码和上面程序的执行效果完全相同,但它省略了创建临时 string 对象的过程以及析构该对象的过程,同时还省略了调用 string 类重载的赋值运算符。由于可见,同样是完成向 map 容器添加新键值对,insert() 方法比 operator[ ] 的执行效率更高。  

  • 更新map容器中的键值对,operator[]效率更高:  
//operator[]
mymap["STL教程"] = "222";
//insert()
std::pair<string, string> STL = { "STL教程","111" };
mymap.insert(STL).first->second = "222";

仅仅从语法形式本身来考虑,或许已经促使很多读者选择 operator[ ] 了。

接下来,我们再从执行效率的角度对比以上 2 种实现方式。  从上面代码可以看到,insert() 方法在进行更新操作之前,需要有一个 pair 类型元素做参数。这意味着,该方法要多构造一个 pair 对象(附带要构造 2 个 string 对象),并且事后还要析构此 pair 对象(附带 2 个 string 对象的析构)。  而和 insert() 方法相比,operator[ ] 就不需要使用 pair 对象,自然不需要构造(并析构)任何 pair 对象或者 string 对象。因此,对于更新已经存储在 map 容器中键值对的值,应优先使用 operator[ ] 方法。  

C++ STL map emplace()和emplace_hint()方法  

和 insert() 方法相比,emplace() 和 emplace_hint() 方法的使用要简单很多,因为它们各自只有一种语法格式

emplace() 方法的语法格式如下:  

template <class... Args>
pair<iterator,bool> emplace (Args&&... args);

该方法的返回值也是一个 pair 对象,其中 pair.first 为一个迭代器,pair.second 为一个 bool 类型变量:(个人:返回值的类型和single版本的insert保持一致)

  • 当该方法将键值对成功插入到 map 容器中时,其返回的迭代器指向该新插入的键值对,同时 bool 变量的值为 true
  • 当插入失败时,则表明 map 容器中存在具有相同键的键值对,此时返回的迭代器指向此具有相同键的键值对,同时 bool 变量的值为 false
#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;

int main()
{
    //创建并初始化 map 容器
    std::map<string, string>mymap;
    //插入键值对
    pair<map<string, string>::iterator, bool> ret = mymap.emplace("STL教程", "111");
    cout << "1、ret.iter = <{" << ret.first->first << ", " << ret.first->second << "}, " << ret.second << ">" << endl;
    //插入新键值对
    ret = mymap.emplace("C语言教程", "222");
    cout << "2、ret.iter = <{" << ret.first->first << ", " << ret.first->second << "}, " << ret.second << ">" << endl;

    //失败插入的样例
    ret = mymap.emplace("STL教程", "333");
    cout << "3、ret.iter = <{" << ret.first->first << ", " << ret.first->second << "}, " << ret.second << ">" << endl;
    return 0;
}

 emplace_hint() 方法的功能和 emplace() 类似,其语法格式如下:(个人:返回值的类型和hint版本的insert保持一致)

template <class... Args>
iterator emplace_hint (const_iterator position, Args&&... args);

显然和 emplace() 语法格式相比,有以下 2 点不同: 

  • 该方法不仅要传入创建键值对所需要的数据,还需要传入一个迭代器作为第一个参数,指明要插入的位置(新键值对键会插入到该迭代器指向的键值对的前面);
  • 该方法的返回值是一个迭代器,而不再是 pair 对象。当成功插入新键值对时,返回的迭代器指向新插入的键值对;反之,如果插入失败,则表明 map 容器中存有相同键的键值对,返回的迭代器就指向这个键值对。  
#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;

int main()
{
    //创建并初始化 map 容器
    std::map<string, string>mymap;
    //指定在 map 容器插入键值对
    map<string, string>::iterator iter = mymap.emplace_hint(mymap.begin(),"STL教程", "111");
    cout << iter->first << " " << iter->second << endl;

    iter = mymap.emplace_hint(mymap.begin(), "C语言教程", "222");
    cout << iter->first << " " << iter->second << endl;

    //插入失败样例
    iter = mymap.emplace_hint(mymap.begin(), "STL教程", "333");
    cout << iter->first << " " << iter->second << endl;
    return 0;
}

注意,和 insert() 方法一样,虽然 emplace_hint() 方法指定了插入键值对的位置,但 map 容器为了保持存储键值对的有序状态,可能会移动其位置。 

为什么 emplace() 和 emplace_hint() 的执行效率会比 insert() 高?

原因很简单,它们向 map 容器插入键值对时,底层的实现方式不同: 

  • 使用 insert() 向 map 容器中插入键值对的过程是,先创建该键值对,然后再将该键值对复制或者移动到 map 容器中的指定位置
  • 使用 emplace() 或 emplace_hint() 插入键值对的过程是,直接在 map 容器中的指定位置构造该键值对。  

也就是说,向 map 容器中插入键值对时,emplace() 和 emplace_hint() 方法都省略了复制或者移动键值对的过程,因此执行效率更高。  

#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;

class testDemo
{
public:
    testDemo(int num) :num(num) {
        std::cout << "调用testDemo构造函数" << endl;
    }
    testDemo(const testDemo& other) :num(other.num) {
        std::cout << "调用testDemo拷贝构造函数" << endl;
    }
    testDemo(testDemo&& other) :num(other.num) {
        std::cout << "调用testDemo移动构造函数" << endl;
    }
private:
    int num;
};

int main()
{
    //创建空 map 容器
    std::map<std::string, testDemo>mymap;

    cout << "insert():" << endl;
    mymap.insert({ "stl", testDemo(1) });

    cout << "emplace():" << endl;
    mymap.emplace( "java:", 2);

    cout << "emplace_hint():" << endl;
    mymap.emplace_hint(mymap.begin(), "python", 3);
    return 0;
}

 从输出结果可以看出,在使用 insert() 方法向 map 容器插入键值对时,整个插入过程调用了 1 次 tempDemo 类的构造函数,同时还调用了 2 次移动构造函数。实际上,程序第 28 行代码底层的执行过程,可以分解为以下 3 步:

//构造类对象
testDemo val = testDemo(1); //调用 1 次构造函数
//构造键值对
auto pai = make_pair("stl", val); //调用 1 次移动构造函数
//完成插入操作
mymap.insert(pai); //调用 1 次移动构造函数

而完成同样的插入操作,emplace() 和 emplace_hint() 方法都只调用了 1 次构造函数,这足以证明,这 2 个方法是在 map 容器内部直接构造的键值对。  因此,在实现向 map 容器中插入键值对时,应优先考虑使用 emplace() 或者 emplace_hint()。 

C++ map 删除erase方法

 

C++ STL multimap容器  

和map容器的区别在于,multimap容器中可以同时存储多(≥2)个键相同的键值对。  

和 map 容器一样,实现 multimap 容器的类模板也定义在<map>头文件,并位于 std 命名空间中。因此,在使用 multimap 容器前,程序应包含如下代码: 

#include <map>
using namespace std;

multimap 容器类模板的定义如下:  

template < class Key,                                   // 指定键(key)的类型
           class T,                                     // 指定值(value)的类型
           class Compare = less<Key>,                   // 指定排序规则
           class Alloc = allocator<pair<const Key,T> >  // 指定分配器对象的类型
           > class multimap;

可以看到,multimap 容器模板有 4 个参数,其中后 2 个参数都设有默认值。  大多数场景中,我们只需要设定前 2 个参数的值,有些场景可能会用到第 3 个参数,但最后一个参数几乎不会用到。  

创建C++ multimap容器的方法  

  • 通过调用 multimap 类模板的默认构造函数,可以创建一个空的 multimap 容器:  
std::multimap<std::string, std::string>mymultimap;
  • 当然,在创建 multimap 容器的同时,还可以进行初始化操作。比如:  
//创建并初始化 multimap 容器
multimap<string, string>mymultimap{ {"C语言教程", "111"},
                                    {"Python教程", "222"},
                                    {"STL教程", "333"} };

注意,使用此方式初始化multimap容器时,其底层会先将每一个{key, value}创建成 pair 类型的键值对,然后再用已建好的各个键值对初始化 multimap 容器。  实际上,我们完全可以先手动创建好键值对,然后再用其初始化 multimap 容器。下面程序使用了 2 种方式创建 pair 类型键值对,再用其初始化 multimap 容器,它们是完全等价的:  

//借助 pair 类模板的构造函数来生成各个pair类型的键值对
multimap<string, string>mymultimap{
    pair<string,string>{"C语言教程", "111"},
    pair<string,string>{ "Python教程", "222"},
    pair<string,string>{ "STL教程", "333"}
};
//调用 make_pair() 函数,生成键值对元素
//创建并初始化 multimap 容器
multimap<string, string>mymultimap{
    make_pair("C语言教程", "111"),
    make_pair("Python教程", "222"),
    make_pair("STL教程", "333")
};
  • 除此之外,通过调用 multimap 类模板的拷贝(复制)构造函数,也可以初始化新的 multimap 容器。例如:  
multimap<string, string>newmultimap(mymultimap);

在 C++ 11 标准中,还为 multimap 类增添了移动构造函数。即当有临时的 multimap 容器作为参数初始化新 multimap 容器时,其底层就会调用移动构造函数来实现初始化操作。举个例子:  

//创建一个会返回临时 multimap 对象的函数
multimap<string, string> dismultimap() {
    multimap<string, string>tempmultimap{ {"C语言教程", "111"},{"Python教程", "222"} };
    return tempmultimap;
}  
//调用 multimap 类模板的移动构造函数创建 newMultimap 容器
multimap<string, string>newmultimap(dismultimap());

注意,无论是调用复制构造函数还是移动拷贝构造函数,都必须保证这 2 个容器的类型完全一致。  

  • multimap 类模板还支持从已有 multimap 容器中,选定某块区域内的所有键值对,用作初始化新 multimap 容器时使用。例如:  
//创建并初始化 multimap 容器
multimap<string, string>mymultimap{ {"C语言教程", "111"},
                                    {"Python教程", "222"},
                                    {"STL教程", "333"} };
multimap<string, string>newmultimap(++mymultimap.begin(), mymultimap.end());
  • multimap 类模板共可以接收 4 个参数,其中第 3 个参数可用来修改 multimap 容器内部的排序规则。默认情况下,此参数的值为std::less<T>,这意味着以下 2 种创建 multimap 容器的方式是等价的:  
multimap<char, int>mymultimap{ {'a',1},{'b',2} };
multimap<char, int, std::less<char>>mymultimap{ {'a',1},{'b',2} };

下面程序利用了 STL 模板库提供的std::greater<T>排序函数,实现令 multimap 容器对存储的键值对做降序排序:  

multimap<char, int, std::greater<char>>mymultimap{ {'a',1},{'b',2} };

其内部键值对的存储顺序为:  

<b,2>
<a,1>

个人:上面的对multimap进行的初始化方式和map完全一致)

C++ multimap容器包含的成员方法  

表 1 C++ 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() 成员方法会经常用到

 

#include <iostream>
#include <map>  //map
using namespace std;

int main()
{
    //创建并初始化 multimap 容器
    multimap<char, int>mymultimap{ {'a',10},{'b',20},{'b',15}, {'b',30} };
    //输出 mymultimap 容器存储键值对的数量
    cout << mymultimap.size() << endl;
    //输出 mymultimap 容器中存储键为 'b' 的键值对的数量
    cout << mymultimap.count('b') << endl;
    auto _range = mymultimap.equal_range('b');
    for (auto iter = _range.first; iter != _range.second; ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}

C++ STL set容器

使用set容器存储的各个元素的值必须各不相同。更重要的是,从语法上讲 set 容器并没有强制对存储元素的类型做const修饰,即 set 容器中存储的元素的值是可以修改的。但是,C++ 标准为了防止用户修改容器中元素的值,对所有可能会实现此操作的行为做了限制,使得在正常情况下,用户是无法做到修改set容器中元素的值的。  对于初学者来说,切勿尝试直接修改set容器中已存储元素的值,这很有可能破坏 set 容器中元素的有序性,最正确的修改 set 容器中元素值的做法是:先删除该元素,然后再添加一个修改后的元素。 

set 容器定义于<set>头文件,并位于 std 命名空间中。因此如果想在程序中使用 set 容器,该程序代码应先包含如下语句:

#include <set>
using namespace std;

set 容器的类模板定义如下:  

template < class T,                        // 键 key 和值 value 的类型
           class Compare = less<T>,        // 指定 set 容器内部的排序规则
           class Alloc = allocator<T>      // 指定分配器对象的类型
           > class set;

创建C++ set容器的几种方法  

  • 调用默认构造函数,创建空的 set 容器。比如:  
std::set<std::string> myset;

由此就创建好了一个 set 容器,该容器采用默认的std::less<T>规则,会对存储的 string 类型元素做升序排序。  

  • 除此之外,set 类模板还支持在创建set容器的同时,对其进行初始化。例如:
std::set<std::string> myset{"111",
                            "222",
                            "333"};
  • set 类模板中还提供了拷贝(复制)构造函数,可以实现在创建新 set 容器的同时,将已有 set 容器中存储的所有元素全部复制到新 set 容器中。
std::set<std::string> copyset(myset);
//等同于
//std::set<std::string> copyset = myset

另外,C++ 11标准还为set类模板新增了移动构造函数,其功能是实现创建新set容器的同时,利用临时的 set 容器为其初始化。比如:  

set<string> retSet() {
    std::set<std::string> myset{ "111",
                            "222",
                            "333" };
    return myset;
}
std::set<std::string> copyset(retSet());
//或者
//std::set<std::string> copyset = retSet();

无论是调用复制构造函数还是调用移动构造函数,都必须保证这 2 个容器的类型完全一致。  

  • set类模板还支持取已有set容器中的部分元素,来初始化新set容器。例如:  
std::set<std::string> myset{ "111",
                    "222",
                    "333" };
std::set<std::string> copyset(++myset.begin(), myset.end());
  • 以上几种方式创建的 set 容器,都采用了默认的std::less<T>规则。其实,借助 set 类模板定义中第 2 个参数,我们完全可以手动修改 set 容器中的排序规则。通过选用 std::greater<string> 降序规则, 

个人:以上初始化的方式和其他的关联容器的方式一致) 

C++STLset容器包含的成员方法

表 1 C++ 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)

个人:关联容器map,set的主要作用都是查找,所以map和set有相同的查找接口)

在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。
#include <iostream>
#include <set>
#include <string>
using namespace std;

int main()
{
    //创建空set容器
    std::set<std::string> myset;
    //空set容器不存储任何元素
    cout << "1、myset size = " << myset.size() << endl;
    //向myset容器中插入新元素
    myset.insert("111");
    myset.insert("222");
    myset.insert("333");
    cout << "2、myset size = " << myset.size() << endl;
    //利用双向迭代器,遍历myset
    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
        cout << *iter << endl;
    }
    return 0;
}

C++ STL set容器迭代器用法

  • 和 map 容器不同,C++ STL 中的 set 容器类模板中未提供 at() 成员函数,也未对 [] 运算符进行重载。(个人:这是显然的,set只存储键值,at(),[]需要提供键值来访问,如果已经知道键值了,那么再去访问有什么意义),因此,要想访问 set 容器中存储的元素,只能借助 set 容器的迭代器。 
  • 值得一提的是,C++ STL 标准库为 set 容器配置的迭代器类型为双向迭代器。这意味着,假设 p 为此类型的迭代器,则其只能进行 ++p、p++、--p、p--、*p 操作,并且 2 个双向迭代器之间做比较,也只能使用 == 或者 != 运算符。 
  • 注意,C++ set 容器迭代器方法返回的迭代器,指向的只是 set 容器中存储的元素,而不再是键值对。
  • 另外,以上成员方法返回的迭代器,无论是 const 类型还是非 const 类型,都不能用于修改 set 容器中的值。 
  • 值得一提的是,虽然 C++ STL 标准中,set 类模板中包含 lower_bound()、upper_bound()、equal_range() 这 3 个成员函数,但它们更适用于 multiset 容器,几乎不会用于操作 set 容器。  

C++ STL set insert()方法

 

  • 只要给定目标元素的值,insert() 方法即可将该元素添加到 set 容器中,其语法格式如下:
//普通引用方式传参
pair<iterator,bool> insert (const value_type& val);
//右值引用方式传参
pair<iterator,bool> insert (value_type&& val);

可以看到,以上 2 种语法格式的 insert() 方法,返回的都是 pair 类型的值,其包含 2 个数据,一个迭代器和一个 bool 值:

    • 当向 set 容器添加元素成功时,该迭代器指向 set 容器新添加的元素,bool 类型的值为 true
    • 如果添加失败,即证明原 set 容器中已存有相同的元素,此时返回的迭代器就指向容器中相同的此元素,同时 bool 类型的值为 false
#include <iostream>
#include <set>
#include <string>
using namespace std;

int main()
{
    //创建并初始化set容器
    std::set<std::string> myset;
    //准备接受 insert() 的返回值
    pair<set<string>::iterator, bool> retpair;
    //采用普通引用传值方式
    string str = "111";
    retpair = myset.insert(str);
    cout << "iter->" << *(retpair.first) << " " << "bool = " << retpair.second << endl;
    //采用右值引用传值方式
    retpair = myset.insert("222");
    cout << "iter->" << *(retpair.first) << " " << "bool = " << retpair.second << endl;
    return 0;
}

  •  insert() 还可以指定将新元素插入到 set 容器中的具体位置,其语法格式如下:
//以普通引用的方式传递 val 值
iterator insert (const_iterator position, const value_type& val);
//以右值引用的方式传递 val 值
iterator insert (const_iterator position, value_type&& val);

以上 2 种语法格式中,insert() 函数的返回值为迭代器:

    • 当向 set 容器添加元素成功时,该迭代器指向容器中新添加的元素
    • 当添加失败时,证明原 set 容器中已有相同的元素,该迭代器就指向 set 容器中相同的这个元素

注意,使用 insert() 方法将目标元素插入到 set 容器指定位置后,如果该元素破坏了容器内部的有序状态,set 容器还会自行对新元素的位置做进一步调整。也就是说,insert() 方法中指定新元素插入的位置,并不一定就是该元素最终所处的位置。  

  • insert() 方法支持向当前set容器中插入其它set容器指定区域内的所有元素,只要这2个set 容器存储的元素类型相同即可。(个人:注意这个版本的insert返回值类型为void)  
template <class InputIterator>
void insert (InputIterator first, InputIterator last);

其中 first 和 last 都是迭代器,它们的组合 [first,last) 可以表示另一 set 容器中的一块区域,该区域包括 first 迭代器指向的元素,但不包含 last 迭代器指向的元素。  

  • 采用如下格式的 insert() 方法,可实现一次向 set 容器中添加多个元素:(个人:注意这个版本的insert返回值类型为void)  
void insert ( {E1, E2,...,En} );

其中,Ei 表示新添加的元素。  

#include <iostream>
#include <set>
#include <string>
using namespace std;

int main()
{
    //创建并初始化set容器
    std::set<std::string> myset;
    //向 myset 中添加多个元素
    myset.insert({ "111",
        "222",
        "333" });

    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
        cout << *iter << endl;
    }
    return 0;
} 

C++ STL set emplace()和emplace_hint()方法

emplace()和emplace_hint()是C++ 11标准加入到set类模板中的,相比具有同样功能的 insert() 方法,完成同样的任务,emplace() 和 emplace_hint() 的效率会更高。 

emplace() 方法的语法格式如下:

template <class... Args>
pair<iterator,bool> emplace (Args&&... args);

其中,参数 (Args&&... args) 指的是,只需要传入构建新元素所需的数据即可,该方法可以自行利用这些数据构建出要添加的元素。 

另外,该方法的返回值类型为pair类型,其包含2个元素,一个迭代器和一个 bool 值: 

  • 当该方法将目标元素成功添加到set容器中时,其返回的迭代器指向新插入的元素,同时 bool 值为 true
  • 当添加失败时,则表明原 set 容器中已存在相同值的元素,此时返回的迭代器指向容器中具有相同键的这个元素,同时 bool 值为 false。  
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main()
{
    //创建并初始化 set 容器
    std::set<string>myset;
    //向 myset 容器中添加元素
    pair<set<string, string>::iterator, bool> ret = myset.emplace("111");
    cout << "myset size = " << myset.size() << endl;
    cout << "ret.iter = <" << *(ret.first) << ", " << ret.second << ">" << endl;
    return 0;
}

emplace_hint() 方法的功能和 emplace() 类似,其语法格式如下: 

template <class... Args>
iterator emplace_hint (const_iterator position, Args&&... args);

和 emplace() 方法相比,有以下2点不同:

  • 该方法需要额外传入一个迭代器,用来指明新元素添加到 set 容器的具体位置(新元素会添加到该迭代器指向元素的前面);
  • 返回值是一个迭代器,而不再是 pair 对象。当成功添加元素时,返回的迭代器指向新添加的元素;反之,如果添加失败,则迭代器就指向set容器和要添加元素的值相同的元素
set<string>::iterator iter = myset.emplace_hint(myset.begin(), "111");

注意,和insert()方法一样,虽然emplace_hint()方法中指定了添加新元素的位置,但set容器为了保持数据的有序状态,可能会移动其位置。  

C++ STL set删除数据:erase()和clear()方法 

set 类模板中,erase() 方法有 3 种语法格式,分别如下:(个人:和map时的接口基本保持一致) 

//删除 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() 方法返回的迭代器)。

#include <iostream>
#include <set>
#include <string>
using namespace std;
int main()
{
    //创建并初始化 set 容器
    std::set<int>myset{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<int>::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<int>::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;
}

如果需要删除set容器中存储的所有元素,可以使用clear()成员方法。该方法的语法格式如下:

void clear();

显然,该方法不需要传入任何参数,也没有任何返回值。  

C++ STL multiset容器  

multiset 容器和 set 容器唯一的差别在于,multiset 容器允许存储多个值相同的元素,而 set 容器中只能存储互不相同的元素。 

和 set 类模板一样,multiset 类模板也定义在<set>头文件,并位于 std 命名空间中。这意味着,如果想在程序中使用 multiset 容器,该程序代码应包含如下语句: 

#include <set>
using namespace std;

multiset 容器类模板的定义如下所示:  

template < class T,                        // 存储元素的类型
           class Compare = less<T>,        // 指定容器内部的排序规则
           class Alloc = allocator<T> >    // 指定分配器对象的类型
           > class multiset;

创建C++ multiset容器的方法:  

multiset 类模板提供的构造函数,和 set 类模板中提供创建 set 容器的构造函数,是完全相同的。这意味着,创建 set 容器的方式,也同样适用于创建 multiset 容器

C++ multiset容器提供的成员方法

multiset容器提供的成员方法,和 set 容器提供的完全一样,如表 1 所示。 

表 1 C++ multiset 容器常用成员方法
成员方法功能
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 中大多数成员方法的用法,和 set 容器中相应成员方法的用法是完全一样的,只是调用者不同

自定义C++STL关联式容器的排序规则

总的来说,为关联式容器自定义排序规则,有以下 2 种方法

使用函数对象自定义排序规则: 

无论关联式容器中存储的是基础类型(如 int、double、float 等)数据,还是自定义的结构体变量或类对象(包括 string 类),都可以使用函数对象的方式为该容器自定义排序规则

#include <iostream>
#include <set>      // set
#include <string>       // 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::set<string, cmp>myset{"/computer/programme/stl/",
                               "/computer/programme/Python/",
                               "/computer/programme/java/"};
    //输出容器中存储的元素
    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
        cout << *iter << endl;
    }
    return 0;
}

需要注意的是,此程序中创建的myset容器,由于是以字符串的长度为准进行排序,因此其无法存储相同长度的多个字符串。 

另外,C++ 中的 struct 和 class 非常类似,前者也可以包含成员变量和成员函数。因此上面程序中,函数对象类 cmp 也可以使用 struct 关键字创建:  

//定义函数对象类
struct cmp {
    //重载 () 运算符
    bool operator ()(const string &a, const string &b) {
        //按照字符串的长度,做升序排序(即存储的字符串从短到长)
        return  (a.length() < b.length());
    }
};

值得一提的是,在定义函数对象类时,也可以将其定义为模板类。比如:

//定义函数对象模板类
template <typename T>
class cmp {
public:
    //重载 () 运算符
    bool operator ()(const T &a, const T &b) {
        //按照值的大小,做升序排序
        return  a < b;
    }
};

注意,此方式必须保证 T 类型元素可以直接使用关系运算符(比如这里的 < 运算符)做比较。  

重载关系运算符实现自定义排序

其实在 STL 标准库中,本就包含几个可供关联式容器使用的排序规则,如表 1 表示。

表 1 C++ STL标准库适用于关联式容器的排序规则
排序规则功能
std::less<T>    底层采用 < 运算符实现升序排序,各关联式容器默认采用的排序规则
std::greater<T> 底层采用 > 运算符实现降序排序,同样适用于各个关联式容器。
std::less_equal<T> 底层采用 <= 运算符实现升序排序,多用于 multimap 和 multiset 容器
std::greater_equal<T> 底层采用 >= 运算符实现降序排序,多用于 multimap 和 multiset 容器

值得一提的是,表 1 中的这些排序规则,其底层也是采用函数对象的方式实现的。以 std::less<T> 为例,其底层实现为:

template <typename T>
struct less {
    //定义新的排序规则
    bool operator()(const T &_lhs, const T &_rhs) const {
        return _lhs < _rhs;
    }
}

在此基础上,当关联式容器中存储的数据类型为自定义的结构体变量或者类对象时,通过对现有排序规则中所用的关系运算符进行重载,也能实现自定义排序规则的目的。注意,当关联式容器中存储的元素类型为结构体指针变量或者类的指针对象时,只能使用函数对象的方式自定义排序规则,此方法不再适用。  

#include <iostream>
#include <set>      // set
#include <string>       // string
using namespace std;
//自定义类
class myString {
public:
    //定义构造函数,向 myset 容器中添加元素时会用到
    myString(string tempStr) :str(tempStr) {};
    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<T> 排序规则
    std::set<myString>myset;
    //向 set 容器添加元素,这里会调用 myString 类的构造函数
    myset.emplace("/computer/programme/stl/");
    myset.emplace("/computer/programme/C_language/");
    myset.emplace("/computer/programme/Python/");
    //
    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
        myString mystr = *iter;
        cout << mystr.getStr() << endl;
    }
    return 0;
}

上面程序以全局函数的形式实现对 < 运算符的重载,还可以使用成员函数或者友元函数的形式实现。其中,当以成员函数的方式重载 < 运算符时,该成员函数必须声明为 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();
}

如何修改关联式容器中键值对的键?

对于如何修改容器中某个键值对的键,所有关联式容器可以采用同一种解决思路,即先删除该键值对,然后再向容器中添加修改之后的新键值对。  那么,是否可以不删除目标键值对,而直接修改它的键呢?  

  • 首先可以明确的是,map 和 multimap 容器只能采用“先删除,再添加”的方式修改某个键值对的键。原因很简单,C++ STL 标准中明确规定,map 和 multimap 容器用于存储类型为pair<const K, V>的键值对。显然,只要目标键值对存储在当前容器中,键的值就无法被修改。
  • 和 map、multimap 不同,C++ STL 标准中并没有用 const 限定 set 和 multiset 容器中存储元素的类型。换句话说,对于 set<T> 或者 multiset<T> 类型的容器,其存储元素的类型是 T 而不是 const T
    set<student>::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,我们可以直接修改 set 和 multiset 容器中元素的非键部分。  

#include<iostream>
#include<set>
#include<string>
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<student, cmp> mymap{ {"zhangsan",10,20},{"lisi",20,21},{"wangwu",15,19} };

    set<student>::iterator iter = mymap.begin();
    //直接将 {"zhangsan",10,20} 中的 "zhangsan" 修改为 "xiaoming"
    const_cast<student&>(*iter).setname("xiaoming");

    while (iter != mymap.end()) {
        (*iter).display();
        ++iter;
    }
    return 0;
}

 

  

posted on 2022-10-15 23:35  朴素贝叶斯  阅读(138)  评论(0编辑  收藏  举报

导航