《C++ Primer 5th》笔记(11 / 19):关联容器 一时失言乱红尘 2022-10-14 05:41 386阅读 0赞 ### 文章目录 ### * * 使用关联容器 * * 使用map * 使用set * 关联容器概述 * * 定义关联容器 * * 初始化multimap 或 multiset * 关键字类型的要求 * * 有序容器的关键字类型 * 使用关键字类型的比较函数 * pair类型 * * 创建pair对象的函数 * 关联容器操作 * * 关联容器迭代器 * * set的迭代器是const的 * 遍历关联容器 * 关联容器和算法 * 添加元素 * * 向map添加元素 * 检测insert的返回值 * 展开递增语句 * 向multiset或multimap添加元素 * 删除元素 * map的下标操作 * * 使用下标操作的返回值 * 访问元素 * * 对map使用find代替下标操作 * 在multimap或multiset中查找元素 * 在multimap或multiset中查找元素,另一种不同的,面向迭代器的解决方法 * 在multimap或multiset中查找元素的另一种解法:equal\_range函数 * 例程:一个单词转换的map * * 单词转换程序word\_transform * 建立转换映射buildMap * 生成转换文本transform * 无序容器 * * 使用无序容器 * * 管理桶 * 无序容器对关键字类型的需求 关联容器支持高效的关键字查找和访问。两个主要的**关联容器**(associative-container)类型是**map**和**set**。 * map中的元素是一些**关键字-值**(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。字典则是一个很好的使用map 的例子:可以将单词作为关键字,将单词释义作为值。 * set中每个元素只包含一个关键字;set支持高效的关键字查询操作——检查一个给定关键字是否在set中。例如,在某些文本处理过程中,可以用一个set来保存想要忽略的单词。 标准库提供8个关联容器,如下表所示。这8个容器间的不同体现在三个维度上:每个容器 1. 或者是一个set,或者是一个map; 2. 或者要求不重复的关键字,或者允许重复关键字; 3. 按顺序保存元素,或无序保存。 <table> <thead> <tr> <th>关联容器</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td><strong>按关键字有序保存元素</strong></td> <td></td> </tr> <tr> <td>map</td> <td>关联数组;保存关键字-值对</td> </tr> <tr> <td>set</td> <td>关键字即值,即只保存关键字的容器</td> </tr> <tr> <td>multimap</td> <td>关键字可重复出现的map</td> </tr> <tr> <td>multiset</td> <td>关键字可重复出现的set</td> </tr> <tr> <td><strong>无序集合</strong></td> <td></td> </tr> <tr> <td>unordered_map</td> <td>用哈希函数组织的map</td> </tr> <tr> <td>unordered_set</td> <td>用哈希函数组织的set</td> </tr> <tr> <td>unordered_multimap</td> <td>哈希组织的map;关键字可以重复出现</td> </tr> <tr> <td>unordered_multiset</td> <td>哈希组织的set;关键字可以重复出现</td> </tr> </tbody> </table> (MyNote:与Java的TreeMap和HashMap比较学习。) 允许重复关键字的容器的名字中都包含单词multi;不保持关键字按顺序存储的容器的名字都以单词unordered开头。因此一个unordered\_multi\_set是一个允许重复关键字,元素无序保存的集合,而一个set则是一个要求不重复关键字,有序存储的集合。无序容器使用哈希函数来组织元素。 * 类型map和**multimap**定义在头文件map中; * set和multiset定义在头文件set中; * 无序容器则定义在头文件unordered\_map和unordered\_set中。 ## 使用关联容器 ## map是关键字-值对的集合。例如,可以将一个人的名字作为关键字,将其电话号码作为值。我们称这样的数据结构为“将名字映射到电话号码”。map类型通常被称为**关联数组**(associative array)。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。我们通过一个关键字而不是位置来查找值。给定一个名字到电话号码的map,我们可以使用一个人的名字作为下标来获取此人的电话号码。 与之相对,set就是关键字的简单集合。当只是想知道一个值是否存在时,set是最有用的。例如,一个企业可以定义一个名为bad\_checks的set来保存那些曾经开过空头支票的人的名字。在接受一张支票之前,可以查询bad\_checks来检查顾客的名字是否在其中。 ### 使用map ### 一个经典的使用关联数组的例子是单词计数程序: //统计每个单词在输入中出现的次数 map<string, size_t> word_count; // string到size_t的空map string word; while (cin >> word) ++word_count[word]; //提取word的计数器并将其加1 for (const auto &w : word_count)//对map中的每个元素 //打印结果 cout<< w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << endl; 此程序读取输入,报告每个单词出现多少次。 类似顺序容器,关联容器也是模板。为了定义一个map,我们必须指定关键字和值的类型。在此程序中,map保存的每个元素中,关键字是string类型,值是size\_t类型。当对word\_count进行下标操作时,我们使用一个string作为下标,获得与此string相关联的size\_t类型的计数器。 > C++语言既有类模板(class template),也有函数模板,其中 vector是一个类模板。只有对C++有了相当深入的理解才能写出模板,事实上,我们直到第16章才会学习如何自定义模板。幸运的是,即使还不会创建模板,我们也可以先试着用用它。 > > **模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化**(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。 > > 对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。 > > 第6章 while循环每次从标准输入读取一个单词。它使用每个单词对word\_count进行下标操作。如果word还未在map中,下标运算符会创建一个新元素,其关键字为word,值为0。不管元素是否是新创建的,我们将其值加1。 一旦读取完所有输入,范围for语句就会遍历map,打印每个单词和对应的计数器。 当从map中提取一个元素时,**会得到一个pair类型的对象**。简单来说,pair是一个模板类型,保存两个名为first和second的(公有)数据成员。map所使用的pair用first成员保存关键字,用second成员保存对应的值。因此,输出语句的效果是打印每个单词及其关联的计数器。 ### 使用set ### 上一个示例程序的一个合理扩展是:忽略常见单词,如"the"、“and”、"or"等。我们可以使用set保存想忽略的单词,只对不在集合中的单词统计出现次数: //统计输入中每个单词出现的次数 map<string,size_t> word_count; //string 到 size_t的空map //想忽略的单词 set<string> exclude = { "The", "But", "And", "Or", "An", "A", "the", "but", "and", "or", "an", "a"}; string word; while (cin >> word) //只统计不在exclude中的单词 if (exclude.find(word) == exclude.end()) ++word_count[word] ;//获取并递增word的计数器 与其他容器类似,set也是模板。为了定义一个set,必须指定其元素类型,本例中是string。与顺序容器类似,可以对一个关联容器的元素进行列表初始化。集合exclude中保存了12个我们想忽略的单词。 此程序与前一个程序的重要不同是,在统计每个单词出现次数之前,我们检查单词是否在忽略集合中,这是在if语句中完成的: //只统计不在exclude中的单词 if(exclude.find(word) == exclude.end()) **find调用返回一个迭代器**。如果给定关键字在set中,迭代器指向该关键字。否则,find返回尾后迭代器。在此程序中,仅当word不在exclude中时我们才更新word的计数器。 ## 关联容器概述 ## 关联容器(有序的和无序的)都支持第9章中介绍的普通容器操作。 关联容器不支持顺序容器的位置相关的操作,例如 push\_front或push\_back。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。 除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作和类型别名。此外,无序容器还提供一些用来调整哈希性能的操作,随后介绍。 关联容器的迭代器都是双向的。 ### 定义关联容器 ### 如前所示, * 当定义一个map时,必须既指明关键字类型又指明值类型; * 而定义一个set 时,只需指明关键字类型,因为set中没有值。 每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。我们也可以将关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型就可以。在新标准下,我们也可以对关联容器进行值初始化: map<string, size_t> word_count; //空容器 //列表初始化 set<string> exclude = { "the", "but", "and", "or", "an", "a", "The", "But", "And", "or", "An", "A"}; //三个元素; authors将姓映射为名 map<string, string> authors = { { "Joyce","James"}, { "Austen", "Jane"}, { "Dickens", "Charles"}}; 与以往一样,初始化器必须能转换为容器中元素的类型。对于set,元素类型就是关键字类型。 当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中: { key, value} 来指出它们一起构成了map中的一个元素。在每个花括号中,关键字是第一个元素,值是第二个。因此,authors 将姓映射到名,初始化后它包含三个元素。 #### 初始化multimap 或 multiset #### 一个map或set 中的关键字必须是唯一的,即,对于一个给定的关键字,只能有一个元素的关键字等于它。**容器multimap和multiset没有此限制**,它们都允许多个元素具有相同的关键字。例如,在我们用来统计单词数量的map中,每个单词只能有一个元素。另一方面,在一个词典中,一个特定单词则可具有多个与之关联的词义。 下面的例子展示了具有唯一关键字的容器与允许重复关键字的容器之间的区别。首先,我们将创建一个名为ivec的保存int 的vector,它包含20个元素:0到9每个整数有两个拷贝。我们将使用此vector初始化一个set和一个multiset: //定义一个有20个元素的vector,保存0到9每个整数的两个拷贝 vector<int> ivec; for (vector<int>::size_type i = 0; i != 10 ; ++i){ ivec.push_back(i); ivec.push_back(i); //每个数重复保存一次 } // iset包含来自ivec的不重复的元素; miset包含所有20个元素 set<int> iset (ivec.cbegin(), ivec.cend()) ; multiset<int> miset (ivec.cbegin(), ivec.cend()); cout << ivec.size() << endl;//打印出20 cout << iset.size() << endl;//打印出10 cout << miset.size() << endl;//打印出20 即使我们用整个ivec容器来初始化iset,它也只含有10个元素:对应ivec中每个不同的元素。另一方面,miset有20个元素,与ivec 中的元素数量一样多。 ### 关键字类型的要求 ### 关联容器对其关键字类型有一些限制。对于无序容器中关键字的要求,随后介绍。对于有序容器——map、multimap、set 以及multiset,**关键字类型必须定义元素比较的方法**。 默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。因此,上例中word\_count 的关键字类型是string。类似的,exclude 的关键字类型也是string。 传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。 #### 有序容器的关键字类型 #### 可以向一个算法提供我们自己定义的比较操作,与之类似,也可以提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型上定义一个**严格弱序**(strict weak ordering)。**可以将严格弱序看作“小于等于”**,虽然实际定义的操作可能是一个复杂的函数。无论我们怎样定义比较函数,它必须具备如下基本性质: * 两个关键字不能同时“小于等于”对方;如果k1“小于等于”k2,那么k2绝不能“小于等于”k1。 * 如果k1“**小于等于**”k2,且 k2“小于等于”k3,那么k1必须“小于等于”k3。(传递性) * 如果存在两个关键字,任何一个都不“小于等于”另一个,那么我们称这两个关键字是“等价”的。如果k1“**等价于**”k2,且k2“等价于”k3,那么k1必须“等价于”k3。(传递性) 如果两个关键字是等价的(即,任何一个都不“小于等于”另一个),那么容器将它们视作相等来处理。当用作map 的关键字时,只能有一个元素与这两个关键字关联,我们可以用两者中任意一个来访问对应的值。 **Note**:在实际编程中,重要的是,如果一个类型定义了“行为正常”的 < 运算符,则它可以用作关键字类型。 #### 使用关键字类型的比较函数 #### 用来组织一个容器中元素的操作的类型也是该容器类型的一部分。\*\*为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。\*\*如前所述,用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出。 在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合)。 例如,我们不能直接定义一个Sales\_data的multiset,因为Sales\_data没有<运算符。但是,可以定义一个名叫compareIsbn函数来定义一个multiset。此函数在Sales\_data对象的 ISBN成员上定义了一个严格弱序。函数compareIsbn应该像下面这样定义: bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs){ return lhs.isbn() < rhs.isbn(); } 为了使用自己定义的操作,在定义multiset时我们必须提供两个类型:关键字类型Sales\_data,以及比较操作类型——应该是一种函数指针类型,可以指向compareIsbn。当定义此容器类型的对象时,需要提供想要使用的操作的指针。 在本例中,我们提供一个指向compareIsbn的指针: // bookstore中多条记录可以有相同的ISBN // bookstore中的元素以 ISBN的顺序进行排列 multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn); 此处,我们使用decltype来指出自定义操作的类型。记住,当用decltype来获得一个函数指针类型时,必须加上一个\*来指出我们要使用一个给定函数类型的指针。**用compareIsbn来初始化bookstore对象,这表示当我们向bookstore添加元素时,通过调用compareIsbn来为这些元素排序**。即,bookstore中的元素将按它们的ISBN成员的值排序。 可以用compareIsbn代替&compareIsbn作为构造函数的参数,因为当我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针。当然,使用&compareIsbn的效果也是一样的。 ### pair类型 ### 在介绍关联容器操作之前,我们需要了解名为**pair**的标准库类型,它定义在头文件utility 中。 一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair 的数据成员将具有对应的类型。两个类型不要求一样: pair<string, string> anon; //保存两个string pair<string, size_t> word_count; //保存一个string和一个size_t pair<string, vector<int>> line; //保存string和vector<int> pair的默认构造函数对数据成员进行值初始化。因此, * anon是一个包含两个空string 的 pair, * word\_count 中的size\_t成员值为0,而string成员被初始化为空。 * line保存一个空 string和一个空vector。 我们也可以为每个成员提供初始化器: pair<string, string> author{ "James", "Joyce"}; 这条语句创建一个名为author的 pair,两个成员被初始化为"James"和"Joyce"。 与其他标准库类型不同,pair的数据成员是public的。两个成员分别命名为first和 second。我们用普通的成员访问符号来访问它们,例如,上面的单词计数程序的输出语句中我们就是这么做的: //打印结果 cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") <<endl; 此处,w是指向map中某个元素的引用。map 的元素是pair。在这条语句中,我们首先打印关键字——元素的first成员,接着打印计数器——second成员。标准库只定义了有限的几个pair操作,下表列出了这些操作。 <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>pair<T1, T2> p;</td> <td>p是一个pair,两个类型分别为T1和T2的成员都进行了值初始化</td> </tr> <tr> <td>pair<T1, T2> p(v1, v2)</td> <td>p是一个成员类型为T1和T2的pair; first和second成员分别用v1和v2进行初始化</td> </tr> <tr> <td>pair<Tl,T2> p = {v1,v2};</td> <td>等价于p(v1, v2)</td> </tr> <tr> <td>make_pair(v1,v2)</td> <td>返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来</td> </tr> <tr> <td>p.first</td> <td>返回p的名为first的(公有)数据成员</td> </tr> <tr> <td>p.second</td> <td>返回p的名为second的(公有)数据成员</td> </tr> <tr> <td>pl relop p2</td> <td>关系运算符(<、>、<=、>=)按字典序定义:例如,当p1.first < p2.first或! (p2.first < p1.first) && pl.second < p2.second 成立时,p1 < p2为 true。关系运算利用元素的 < 运算符来实现</td> </tr> <tr> <td>pl == p2</td> <td>当first和second成员分别相等时,两个pair相等。相等性判断利用元素的==运算符实现</td> </tr> <tr> <td>p1 != p2</td> <td>共享上一条说明</td> </tr> </tbody> </table> #### 创建pair对象的函数 #### 想象有一个函数需要返回一个pair。在新标准下,我们可以对返回值进行列表初始化 pair<string, int> process (vector<string> &v){ //处理v if(!v.empty()) return { v.back(), v.back().size()};//列表初始化 else return pair<string, int>();//隐式构造返回值 } 若v不为空,我们返回一个由v中最后一个string及其大小组成的pair。否则,隐式构造一个空pair,并返回它。 在较早的C++版本中,不允许用花括号包围的初始化器来返回pair这种类型的对象,必须显式构造返回值: if (!v.empty()) return pair<string,int>(v.back(), v.back().size()); 我们还可以用make\_pair来生成pair对象,pair的两个类型来自于make\_pair的参数: if (!v.empty()) return make_pair(v.back(), v.back().size()); ## 关联容器操作 ## 除了第9章的容器库概览中列出的类型,关联容器还定义了下表中列出的类型。这些类型表示容器关键字和值的类型。 <table> <thead> <tr> <th>关联容器额外的类型别名</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>key_type</td> <td>此容器类型的关键字类型</td> </tr> <tr> <td>mapped_type</td> <td>每个关键字关联的类型;只适用于map</td> </tr> <tr> <td>value_type</td> <td>对于set,与key_type相同<br>对于map,为pair<const key_type, mapped_type></td> </tr> </tbody> </table> * 对于set类型,key\_type和value\_type是一样的; set中保存的值就是关键字。 * 在一个map中,元素是关键字-值对。即,每个元素是一个pair对象,包含一个关键字和一个关联的值。 由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的: set<string>::value_type vl; //v1是一个string set<string>::key_type v2; //v2是一个string map<string, int>::value_type v3; //v3是一个pair<const string, int> map<string, int>::key_type v4; //v4是一个string map<string, int>::mapped_type v5; //v5是一个int 与顺序容器一样,我们使用作用域运算符来提取一个类型的成员——例如,map<string,int>::key\_type。 只有map类型(unordered\_map、unordered\_multimap、multimap和map)才定义了**mapped\_type**。 ### 关联容器迭代器 ### 当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value\_type的值的引用。对map而言,value\_type是一个pair类型,其first成员保存const的关键字,second成员保存值: //获得指向word_count中一个元素的迭代器 auto map_it = word_count.begin(); //*map_it是指向一个pair<const string,size_t>对象的引用 cout << map_it->first; //打印此元素的关键字 cout << " " <<map_it->second;//打印此元素的值 map_it->first = "new key"; //错误:关键字是const的 ++map_it->second; //正确:我们可以通过迭代器改变元素 **Note**:必须记住,一个map的value\_type是一个pair,我们可以改变pair的值,但**不能改变关键字成员的值**。 #### set的迭代器是const的 #### 虽然set类型同时定义了iterator和const\_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改: set<int> iset = { 0,1,2,3,4,5,6,7,8,9} ; set<int>::iterator set_it = iset.begin(); if (set_it != iset.end()) { *set_it = 42; //错误:set中的关键字是只读的 cout<< *set_it <<endl; //正确:可以读关键字 } #### 遍历关联容器 #### map和set类型都支持的begin和end操作。与往常一样,我们可以用这些函数获取迭代器,然后用迭代器来遍历容器。例如,我们可以编写一个循环来打印本文初的中单词计数程序的结果,如下所示: //获得一个指向首元素的迭代器 auto map_it = word_count.cbegin(); //比较当前迭代器和尾后迭代器 while (map_it != word_count.cend()) { //解引用迭代器,打印关键字-值对 cout << map_it->first << " occurs " << map_it->second << " times" << endl; ++map_it; //递增迭代器,移动到下一个元素 } Note:**本程序的输出是按字典序排列的**。当使用一个迭代器遍历一个map、multimap、set或multiset时,迭代器按关键字升序遍历元素。 #### 关联容器和算法 #### 我们通常不对关联容器使用泛型算法(参见第10章)。**关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法**,因为这类算法需要向元素写入值,而set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。 关联容器可用于只读取元素的算法。但是,很多这类算法都要搜索序列。**由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型搜索算法几乎总是个坏主意**。例如,我们将在第11章中看到,关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素。我们可以用泛型find算法来查找一个元素,但此算法会进行顺序搜索。使用关联容器定义的专用的find成员会比调用泛型find快得多。 在实际编程中,如果我们真要对一个关联容器使用算法, * 要么是将它当作一个源序列。例如,可以用泛型copy算法将元素从一个关联容器拷贝到另一个序列。 * 要么当作一个目的位置。例如,可以调用inserter将一个插入器绑定(参见第10章)到一个关联容器。通过使用inserter,我们可以将关联容器当作一个目的位置来调用另一个算法。 ### 添加元素 ### <table> <thead> <tr> <th>关联容器insert操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>c.insert(v)<br>c.emplace(args)</td> <td>v是value_type类型的对象;args用来构造一个元素<br>对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。<br>函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。<br>对于multimap和multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器</td> </tr> <tr> <td>c.insert(b, e)<br>c.insert(il)</td> <td>b和e是迭代器,表示一个c::value_type类型值的范围; il是这种值的花括号列表。<br>函数返回void。<br>对于map和set,只插入关键字不在c中的元素。对于multimap和multiset,则会插入范围中的每个元素</td> </tr> <tr> <td>c.insert(p, v)<br>c.emplace(p, args)</td> <td>类似insert(v)(或emplace(args)),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素</td> </tr> </tbody> </table> 关联容器的insert成员向容器中添加一个元素或一个元素范围。由于 map和set(以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响: vector<int> ivec = { 2,4,6,8,2,4,6,8 }; //ivec有8个元素 set<int> set2; //空集合 set2.insert(ivec.cbegin(), ivec.cend()); //set2有4个元素 set2.insert({ 1,3,5,7,1,3,5,7}); //set2又加入4个元素,现在已有8个元素 insert有两个版本,分别接受一对迭代器,或是一个初始化器列表,这两个版本的行为类似对应的构造函数——对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中。 #### 向map添加元素 #### 对一个map进行 insert 操作时,必须记住元素类型是pair。通常,对于想要插入的数据,并没有一个现成的pair对象。可以在insert的参数列表中创建一个pair: //向word_count插入word的4种方法 word_count.insert({ word, 1}); word_count.insert(make_pair(word, 1)); word_count.insert(pair<string,size_t>(word, 1)); word_count.insert(map<string,size_t>::value_type(word, 1)); 如我们所见,在新标准下,创建一个pair最简单的方法是在参数列表中使用花括号初始化。也可以调用make\_pair或显式构造pair。最后一个insert调用中的参数: map<string, size_t>::value_type(s, 1) 构造一个恰当的pair类型,并构造该类型的一个新对象,插入到map中。 #### 检测insert的返回值 #### insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和 emplace版本返回一个pair,告诉我们插入操作是否成功。 pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。 * 如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。 * 如果关键字不存在,元素被插入容器中,且bool值为true。 作为一个例子,我们用insert重写单词计数程序: // more verbose way to count number of times each word occurs in the input map<string, size_t> word_count; // empty map from string to size_t string word; while (cin >> word) { // inserts an element with key equal to word and value 1; // if word is already in word_count, insert does nothing auto ret = word_count.insert({ word, 1}); if (!ret.second) // word was already in word_count ++ret.first->second; // increment the counter } #### 展开递增语句 #### 在这个版本的单词计数程序中,递增计数器的语句很难理解。**通过添加一些括号来反映出运算符的优先级**,会使表达式更容易理解一些: ++((ret.first)->second); //等价的表达式 下面我们一步一步来解释此表达式: * `ret`保存insert返回的值,是一个pair。 * `ret.first`是pair的第一个成员,是一个map迭代器,指向具有给定关键字的元素。 * `ret.first->`解引用此迭代器,提取map中的元素,元素也是一个pair。 * `ret.first->second` map 中元素的值部分。 * `++ret.first->second`递增此值。 再回到原来完整的递增语句,它提取匹配关键字word的元素的迭代器,并递增与我们试图插入的关键字相关联的计数器。 如果读者使用的是旧版本的编译器,或者是在阅读新标准推出之前编写的代码,ret的声明和初始化可能复杂些: pair<map<string, size_t>::iterator, bool> ret = word_count.insert(make_pair(word, 1)); 应该容易看出这条语句定义了一个pair,其第二个类型为 bool类型。第一个类型理解起来有点儿困难,它是一个在map<string, size\_t>类型上定义的iterator类型。 #### 向multiset或multimap添加元素 #### 我们的单词计数程序依赖于这样一个事实:一个给定的关键字只能出现一次。这样,任意给定的单词只有一个关联的计数器。**我们有时希望能添加具有相同关键字的多个元素**。 例如,可能想建立作者到他所著书籍题目的映射。在此情况下,每个作者可能有多个条目,因此我们应该使用multimap而不是map。由于一个multi容器中的关键字不必唯一,在这些类型上调用insert总会插入一个元素: multimap<string, string> authors; //插入第一个元素,关键字为Barth,John authors.insert({ "Barth,John", "Sot-weed Factor"}); //正确:添加第二个元素,关键字也是Barth,John authors.insert({ "Barth,John", "Lost in the Funhouse")); 对允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器。这里无须返回一个bool值,因为insert总是向这类容器中加入一个新元素。 ### 删除元素 ### 关联容器定义了三个版本的erase,如下表所示。与顺序容器一样,我们可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或者一个元素范围。这两个版本的erase与对应的顺序容器的操作非常相似:指定的元素被删除,函数返回void。 <table> <thead> <tr> <th>关联容器erase操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>c.erase(k)</td> <td>从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量</td> </tr> <tr> <td>c.erase§</td> <td>从c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end()</td> </tr> <tr> <td>c.erase(b, e)</td> <td>删除迭代器对b和e所表示的范围中的元素。返回e</td> </tr> </tbody> </table> 关联容器提供一个额外的erase操作,它接受一个key\_type参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。我们可以用此版本在打印结果之前从word\_count中删除一个特定的单词: //删除一个关键字,返回删除的元素数量 if (word_count.erase(removal_word)) cout << "ok: " << removal_word << " removed\n"; else cout << "oops: " << removal_word << " not found!\n"; 对于保存不重复关键字的容器,erase的返回值总是0或1。若返回值为0,则表明想要删除的元素并不在容器中。 对允许重复关键字的容器,删除元素的数量可能大于1: auto cnt = authors.erase("Barth, John"); 如果authors是我们在向multiset或multimap添加元素中创建的multimap,则cnt 的值为2。 ### map的下标操作 ### map和 unordered\_map容器提供了下标运算符和一个对应的at函数,如下表所示。 <table> <thead> <tr> <th>map和unordered_map的下标操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>c[k]</td> <td>返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化</td> </tr> <tr> <td>c.at(k)</td> <td>访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常</td> </tr> </tbody> </table> set类型不支持下标,因为set中没有与关键字相关联的“值”。元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。 我们不能对一个multimap或一个unordered\_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。 类似我们用过的其他下标运算符,map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。 例如,如果我们编写如下代码 map<string, size_t> word_count; //empty map //插入一个关键字为Anna的元素,关联值进行值初始化;然后将1赋予它 word_count["Anna"] = 1; 将会执行如下操作: * 在word\_count中搜索关键字为Anna的元素,未找到。 * 将一个新的关键字-值对插入到word\_count中。关键字是一个const string,保存Anna。值进行值初始化,在本例中意味着值为0。 * 提取出新插入的元素,并将值1赋予它。 由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。 **Note**:对一个map使用下标操作,其行为与数组或vector上的下标操作很不相同:使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中。 #### 使用下标操作的返回值 #### map的下标运算符与我们用过的其他下标运算符的另一个不同之处是其**返回类型**。 * 通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。 * 但对map则不然,当对一个map进行下标操作时,会获得一个mapped\_type对象;但当解引用一个map迭代器时,会得到一个value\_type对象。 与其他下标运算符相同的是,map 的下标运算符返回一个左值。由于返回的是一个左值,所以我们既可以读也可以写元素: cout << word_count["Anna"]; //用Anna 作为下标提取元素;会打印出1 ++word_count["Anna"]; //提取元素,将其增1 cout << word_count["Anna"];//提取元素并打印它;会打印出2 **Note**:与vector与string不同,map 的下标运算符返回的类型 与 解引用map迭代器得到的类型不同。 如果关键字还未在map 中,下标运算符会添加一个新元素,这一特性允许我们编写出异常简洁的程序。另一方面,有时只是想知道一个元素是否已在 map 中,但在不存在时并不想添加元素。在这种情况下,就**不能使用下标运算符**。 ### 访问元素 ### 关联容器提供多种查找一个指定元素的方法,如下表所示。应该使用哪个操作依赖于我们要解决什么问题。 * 如果我们所关心的只不过是一个特定元素是否已在容器中,可能find是最佳选择。 * 对于不允许重复关键字的容器,可能使用find还是count没什么区别。但对于允许重复关键字的容器,count还会做更多的工作: * 如果元素在容器中,count还会统计有多少个元素有相同的关键字。 * 如果不需要计数,最好使用find: set<int> iset = { 0,1,2,3,4,5,6,7,8,9 }; iset.find(1) ;//返回一个迭代器,指向key == 1的元素 iset.find(11);//返回一个迭代器,其值等于iset.end() iset.count(1); //返回1 iset.count(11) ; //返回0 #### 对map使用find代替下标操作 #### 对map和 unordered\_map类型,下标运算符提供了最简单的提取元素的方法。但是,如我们所见,**使用下标操作有一个严重的副作用:如果关键字还未在map中,下标操作会插入一个具有给定关键字的元素**。这种行为是否正确完全依赖于我们的预期是什么。例如,单词计数程序依赖于这样一个特性:使用一个不存在的关键字作为下标,会插入一个新元素,其关键字为给定关键字,其值为0。也就是说,下标操作的行为符合我们的预期。 但有时,我们**只是想知道一个给定关键字是否在map 中,而不想改变map**。这样就不能使用下标运算符来检查一个元素是否存在,因为如果关键字不存在的话,下标运算符会插入一个新元素。在这种情况下,应该使用find: if (word_count.find("foobar") == word_count.end()) cout << "foobar is not in the map" << endl; #### 在multimap或multiset中查找元素 #### 在一个不允许重复关键字的关联容器中查找一个元素是一件很简单的事情——元素要么在容器中,要么不在。但**对于允许重复关键字的容器来说,过程就更为复杂**:在容器中可能有很多元素具有给定的关键字。如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。 例如,给定一个从作者到著作题目的映射,我们可能想打印一个特定作者的所有著作。可以用三种不同方法来解决这个问题。最直观的方法是使用find和count: string search_item("Alain de Botton");//要查找的作者 auto entries = authors.count(search_item);//元素的数量 auto iter = authors.find(search_item) ;//此作者的第一本书 //用一个循环查找此作者的所有著作 while(entries) { cout << iter->second <<endl; //打印每个题目 ++iter;//前进到下一本书 --entries;//记录已经打印了多少本书 } 首先调用count确定此作者共有多少本著作,并调用find 获得一个迭代器,指向第一个关键字为此作者的元素。for 循环的迭代次数依赖于count 的返回值。特别是,如果count返回0,则循环一次也不执行。 **Note**:当我们遍历一个multimap或multiset时,保证可以得到序列中所有具有给定关键字的元素。 #### 在multimap或multiset中查找元素,另一种不同的,面向迭代器的解决方法 #### 我们还可以用lower\_bound和upper\_bound来解决此问题。这两个操作都接受一个关键字,返回一个迭代器。 如果关键字在容器中, * lower\_bound返回的迭代器将指向第一个具有给定关键字的元素, * upper\_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。 如果元素不在multimap中,则lower\_bound和upper\_bound 会返回相等的迭代器——指向一个不影响排序的关键字插入位置。 因此,用相同的关键字调用lower\_bound和upper\_bound 会得到一个迭代器范围,表示所有具有该关键字的元素的范围。 (MyNote:与begin()、end()类似。) 当然,这两个操作返回的迭代器可能是容器的尾后迭代器。如果我们查找的元素具有容器中最大的关键字,则此关键字的upper\_bound返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower\_bound返回的也是尾后迭代器。 **Note**:lower\_bound返回的迭代器可能指向一个具有给定关键字的元素,但也可能不指向。如果关键字不在容器中,则lower\_bound会返回关键字的第一个安全插入点——不影响容器中元素顺序的插入位置。 使用这两个操作,我们可以重写前面的程序: // authors和search_item的定义,与前面的程序一样 // beg和end表示对应此作者的元素的范围 for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg) cout << beg->second << endl; //打印每个题目 此程序与使用count和 find的版本完成相同的工作,但更直接。 * 对lower\_bound 的调用将beg定位到第一个与search\_item 匹配的元素(如果存在的话)。如果容器中没有这样的元素,beg将指向**第一个关键字大于search\_item**的元素,有可能是尾后迭代器。 * upper\_bound调用将end指向最后一个匹配指定关键字的元素之后的元素。这两个操作并不报告关键字是否存在,重要的是它们的返回值可作为一个迭代器范围。 如果没有元素与给定关键字匹配,则lower\_bound和upper\_bound会返回相等的迭代器——都指向给定关键字的插入点,能保持容器中元素顺序的插入位置。 假定有多个元素与给定关键字匹配,beg将指向其中第一个元素。我们可以通过递增beg来遍历这些元素。end中的迭代器会指出何时完成遍历——当beg 等于end时,就表明已经遍历了所有匹配给定关键字的元素了。 由于这两个迭代器构成一个范围,我们可以用一个 for 循环来遍历这个范围。循环可能执行零次,如果存在给定作者的话,就会执行多次,打印出该作者的所有项。如果给定作者不存在,beg和end相等,循环就一次也不会执行。否则,我们知道递增beg最终会使它到达end,在此过程中我们就会打印出与此作者关联的每条记录。 **Note**:如果lower\_bound和upper\_bound返回相同的迭代器,则给定关键字不在容器中。 #### 在multimap或multiset中查找元素的另一种解法:equal\_range函数 #### 解决此问题的最后一种方法是三种方法中最直接的:不必再调用upper\_bound和lower\_bound,直接调用equal\_range即可。此函数接受一个关键字,返回一个迭代器pair。 * 若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。 * 若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。 可以用equal\_range来再次修改我们的程序: // authors和search_item的定义,与前面的程序一样 // pos 保存迭代器对,表示与关键字匹配的元素范围 for (auto pos = authors.equal_range (search_item); pos.first != pos.second; ++pos.first) cout<< pos.first->second << endl;//打印每个题目 **此程序本质上与前一个使用upper\_bound和 lower\_bound的程序是一样的。** 不同之处就是,没有用局部变量beg和end 来保存元素范围,而是使用了equal\_range返回的pair。此pair的first成员保存的迭代器与lower\_bound返回的迭代器是一样的,second保存的迭代器与upper\_bound 的返回值是一样的。因此,在此程序中,pos.first等价于beg,pos.second等价于end。 ### 例程:一个单词转换的map ### 我们将以一个程序结束本节的内容,它将展示 map 的创建、搜索以及遍历。 **这个程序的功能**是这样的:给定一个string,将它转换为另一个string。 程序的输入是两个文件: * 第一个文件保存的是一些规则,用来转换第二个文件中的文本。每条规则由两部分组成:一个可能出现在输入文件中的单词和一个用来替换它的短语。表达的含义是,每当第一个单词出现在输入中时,我们就将它替换为对应的短语。 * 第二个输入文件包含要转换的文本。 如果单词转换文件的内容如下所示: brb be right back k okay? y why r are u you pic picture thk thanks! 18r later 我们希望转换的文本为 where r u y dont u send me a pic k thk 18r 则程序应该生成这样的输出: where are you why dont you send me a picture okay? thanks! later #### 单词转换程序word\_transform #### 我们的程序将使用三个函数。 1. 函数word\_transform管理整个过程。它接受两个ifstream参数: 1. 第一个参数应绑定到单词转换文件, 2. 第二个参数应绑定到我们要转换的文本文件。 2. 函数buildMap会读取转换规则文件,并创建一个map,用于保存每个单词到其转换内容的映射。 3. 函数transform接受一个string,如果存在转换规则,返回转换后的内容。 我们首先定义 word\_transform函数。最重要的部分是调用buildMap和transform: void word_transform(ifstream &map_file, ifstream &input) { auto trans_map = buildMap(map_file); // 1.store the transformations //2. string text; // hold each line from the input while (getline(input, text)) { // 3.read a line of input istringstream stream(text); // 4.read each word string word; bool firstword = true; // controls whether a space is printed while (stream >> word) { if (firstword)//5. firstword = false; else cout << " "; // print a space between words // 6.transform returns its first argument or its transformation cout << transform(word, trans_map); // print the output } cout << endl; // done with this line of input } } 1. 函数首先调用buildMap来生成单词转换map,我们将它保存在trans\_map中。 2. 函数的剩余部分处理输入文件。 3. while循环用getline一行一行地读取输入文件。这样做的目的是使得输出中的换行位置能和输入文件中一样。 4. 为了从每行中获取单词,我们使用了一个嵌套的while循环,它用一个istringstream(参见第8章)来处理当前行中的每个单词。 5. 在输出过程中,内层 while循环使用一个 bool变量firstword来确定是否打印一个空格。它通过调用transform来获得要打印的单词。 6. transform的返回值或者是word中原来的string,或者是trans\_map中指出的对应的转换内容。 #### 建立转换映射buildMap #### 函数buildMap读入给定文件,建立起转换映射。 map<string, string> buildMap(ifstream &map_file)//1. { map<string, string> trans_map; // holds the transformations string key; // a word to transform string value; // phrase to use instead // read the first word into key and the rest of the line into value while (map_file >> key && getline(map_file, value))//2. 3. if (value.size() > 1) // check that there is a transformation//4. trans_map[key] = value.substr(1); // skip leading space//5. else throw runtime_error("no rule for " + key); return trans_map;//6. } 1. map\_file中的每一行对应一条规则。每条规则由一个单词和一个短语组成,短语可能包含多个单词。 2. 我们用>>读取要转换的单词,存入key中,并调用getline读取这一行中的剩余内容存入value。 3. 由于getline不会跳过前导空格,需要我们来跳过单词和它的转换内容之间的空格。 4. 在保存转换规则之前,检查是否获得了一个以上的字符。如果是,调用substr来跳过分隔单词及其转换短语之间的前导空格,并将得到的子字符串存入trans\_map。 5. 注意,我们使用下标运算符来添加关键字-值对。我们隐含地忽略了一个单词在转换文件中出现多次的情况。如果真的有单词出现多次,循环会将最后一个对应短语存入trans\_map。 6. 当while循环结束后,trans\_map中将保存着用来转换输入文本的规则。 #### 生成转换文本transform #### 函数transform进行实际的转换工作。其参数是需要转换的string的引用和转换规则map。如果给定string在map 中,transform返回相应的短语。否则,transform直接返回原string: const string & transform(const string &s, const map<string, string> &m) { // the actual map work; this part is the heart of the program auto map_it = m.find(s);//1. // if this word is in the transformation map if (map_it != m.cend())//2. return map_it->second; // use the replacement word else return s; // otherwise return the original unchanged } 1. 函数首先调用find来确定给定string是否在map中。如果存在,则find返回一个指向对应元素的迭代器。否则,find返回尾后迭代器。 2. 如果元素存在,我们解引用迭代器,获得一个保存关键字和值的pair,然后返回成员second,即用来替代s的内容。 ## 无序容器 ## C++11新标准定义了4个**无序关联容器**(unordered associative container)。**这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function)和关键字类型的==运算符**。在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的。在某些应用中,维护元素的序代价非常高昂,此时无序容器也很有用。 虽然理论上哈希技术能获得更好的平均性能,但在实际中想要达到很好的效果还需要进行一些性能测试和调优工作。因此,使用无序容器通常更为简单(通常也会有更好的性能)。 (MyNote: 与Java的HashMap类比学习。) **Tip:如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。** ### 使用无序容器 ### 除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find、insert等)。这意味着我们曾用于map和set的操作也能用于unordered\_map 和unordered\_set。类似的,无序容器也有允许重复关键字的版本。 因此,通常可以用一个无序容器替换对应的有序容器,反之亦然。但是,由于元素未按顺序存储,一个使用无序容器的程序的输出(通常)会与使用有序容器的版本不同。 例如,可以用unordered\_map重写最初的单词计数程序: // count occurrences, but the words won't be in alphabetical order unordered_map<string, size_t> word_count; string word; while (cin >> word) ++word_count[word]; // fetch and increment the counter for word for (const auto &w : word_count) // for each element in the map // print the results cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << endl; #### 管理桶 #### 无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。 对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。 (MyNote:上述为无序容器的基本原理。) (MyNote:哈希函数三个指标:高效,平均,唯一) 无序容器提供了一组管理桶的函数,如下表所示。这些成员函数允许我们查询容器的状态以及在必要时强制容器进行重组。 <table> <thead> <tr> <th>无序容器管理操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td><strong>桶接口</strong></td> <td></td> </tr> <tr> <td>c.bucket_count()</td> <td>正在使用的桶的数目</td> </tr> <tr> <td>c.max_bucket_count()</td> <td>容器能容纳的最多的桶的数量</td> </tr> <tr> <td>c.bucket_size(n)</td> <td>第n个桶中有多少个元素</td> </tr> <tr> <td>c.bucket(k)</td> <td>关键字为k的元素在哪个桶中</td> </tr> <tr> <td><strong>桶迭代</strong></td> <td></td> </tr> <tr> <td>local_iterator</td> <td>可以用来访问桶中元素的迭代器类型</td> </tr> <tr> <td>const_local_iterator</td> <td>桶迭代器的const版本</td> </tr> <tr> <td>c.begin(n), c.end(n)</td> <td>桶n的首元素迭代器和尾后迭代器</td> </tr> <tr> <td>c.cbegin(n), c.cend(n)</td> <td>与前两个函数类似,但返回const_local_iterator</td> </tr> <tr> <td><strong>哈希策略</strong></td> <td></td> </tr> <tr> <td>c.load_factor()</td> <td>每个桶的平均元素数量,返回float值</td> </tr> <tr> <td>c.max_load_factor()</td> <td>c试图维护的平均桶大小,返回float值。c会在需要时添加新的桶,以使得load_factor<=max_load_factor</td> </tr> <tr> <td>c.rehash(n)</td> <td>重组存储,使得bucket_count >= n 且 bucket_count > size / max_load_factor</td> </tr> <tr> <td>c.reserve(n)</td> <td>重组存储,使得c可以保存n个元素且不必rehash</td> </tr> </tbody> </table> #### 无序容器对关键字类型的需求 #### 默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key\_type>类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了**hash**模板。还为一些标准库类型,包括string和我们将要在第12章介绍的智能指针类型定义了hash。因此,我们可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针类型的无序容器。 但是,我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本。我们将在第16章中介绍如何做到这一点。 我们不使用默认的hash,而是使用另一种方法,类似于为有序容器重载关键字类型的默认比较操作使用关键字类型的比较函数。 为了能将Sale\_data用作关键字,我们需要提供函数来替代==运算符和哈希值计算函数。我们从定义这些重载函数开始: size_t hasher(const sales_data &sd) { return hash<string>(0)(sd.isbn()); } bool eqOp(const sales_data &lhs,const sales_data &rhs){ return lhs.isbn() == rhs.isbn (); } 我们的hasher函数使用一个标准库hash类型对象来计算ISBN成员的哈希值,该hash类型建立在 string 类型之上。类似的,eqOp函数通过比较ISBN号来比较两个Sales\_data。 我们使用这些函数来定义一个unordered\_multiset using SD_multiset = unordered_multiset<sales_data, decltype(hasher)* , decltype(eqOp)*>; //参数是桶大小、哈希函数指针和相等性判断运算符指针 SD_multiset bookstore(42, hasher, eqOp); 为了简化bookstore的定义,首先为unordered\_multiset定义了一个类型别名,此集合的哈希和相等性判断操作与hasher和eqOp函数有着相同的类型。通过使用这种类型,在定义bookstore时可以将我们希望它使用的函数的指针传递给它。 如果我们的类定义了==运算符,则可以只重载哈希函数: //使用FooHash生成哈希值;Foo必须有==运算符 unordered_set<Foo,decltype(FooHash)*> fooSet(10,FooHash); (MyNote:类似于Java类使用HashMap前,需要override hashCode()、equals()。)
相关 《C++ Primer 5th》笔记(10 / 19):泛型算法 文章目录 概述 算法如何工作 迭代器令算法不依赖于容器 但算法依赖于元素类型的操作 我会带着你远行/ 2023年01月22日 08:53/ 0 赞/ 28 阅读
相关 《C++ Primer 5th》笔记(9 / 19):顺序容器 文章目录 顺序容器概述 确定使用哪种顺序容器 容器库概览 迭代器 迭代器范围 淩亂°似流年/ 2023年01月21日 02:21/ 0 赞/ 29 阅读
相关 《C++ Primer 5th》笔记(8 / 19):IO库 文章目录 IO类 IO类型间的关系 IO对象无拷贝或赋值 条件状态 素颜马尾好姑娘i/ 2023年01月19日 08:19/ 0 赞/ 32 阅读
相关 《C++ Primer 5th》笔记(7 / 19):类 文章目录 定义抽象数据类型 设计Sales\_data类 关键概念:不同的编程角色 缺乏、安全感/ 2023年01月19日 04:47/ 0 赞/ 28 阅读
相关 《C++ Primer 5th》笔记(5 / 19):语句 文章目录 简单语句 空语句 别漏写分号,也别多写分号 复合语句(块) 语句作用域 墨蓝/ 2023年01月18日 04:28/ 0 赞/ 142 阅读
相关 《C++ Primer 5th》笔记(4 / 19):表达式 文章目录 基础 基本概念 组合运算符和运算对象 运算对象转换 缺乏、安全感/ 2023年01月17日 15:00/ 0 赞/ 170 阅读
相关 《C++ Primer 5th》笔记(1 / 19):C++基础 文章目录 编写一个简单的C++程序 编译、运行程序 初识输入输出 注释简介 控制流 爱被打了一巴掌/ 2023年01月14日 15:56/ 0 赞/ 179 阅读
相关 《C++ Primer 5th》笔记(12 / 19):动态内存 文章目录 动态内存与智能指针 shared\_ptr类 make\_shared函数 痛定思痛。/ 2022年10月16日 07:33/ 0 赞/ 231 阅读
相关 《C++ Primer 5th》笔记(11 / 19):关联容器 文章目录 使用关联容器 使用map 使用set 关联容器概述 定义关联容器 一时失言乱红尘/ 2022年10月14日 05:41/ 0 赞/ 387 阅读
还没有评论,来说两句吧...