《C++Primer》第十章-关联容器-学习笔记(1)

《C++Primer》第十章-关联容器-学习笔记(1)

日志:
1,2020-03-14 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

摘要

本章涵盖了关联容器(标准库容器类型)的相关内容,并完善和扩展了一个使用顺序容器和关联容器的例子。
关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。虽然关联容器的大部分行为与顺序容器相同,但其独特之处在于支持键的使用。
关联容器(Associative containers)支持通过键(key)来高效地查找和读取元素。
两个基本的关联容器类型是 mapset

  • map的元素以键-值(key-value)对的形式组织:键(key)用作元素在 map 中的索引,而值(value)则表示所存储和读取的数据。
  • set仅包含一个键(key),并有效地支持关于某个键是否存在的查询。

一般来说,如果希望有效地存储不同值的集合,那么使用 set 容器比较合适,而 map 容器则更适用于需要存储(乃至修改)每个键所关联的值的情况。
在做某种文本处理时,可使用 set 保存要忽略的单词。而字典则是 map 的一种很好的应用:单词本身是键,而它的解释说明则是值。
set 和 map 类型的对象所包含的元素都具有不同的键,不允许为同一个键添加第二个元素。如果一个键必须对应多个实例,则需使用 multimapmultiset,这两种类型允许多个元素拥有相同的键。
关联容器支持很多顺序容器也提供的相同操作,此外,还提供管理或使用键的特殊操作。下面将详细讨论关联容器类型及其操作,最后以一个用容器实现的小型文本查询程序结束本章。

表1 关联容器类型:

关联容器类型 作用
map 关联数组:元素通过键(key)来存储和读取
set 大小可变的集合,支持通过键实现的快速读取
multimap 支持同一个键多次出现的 map 类型
multiset 支持同一个键多次出现的 set 类型

引言:pair 类型

在开始介绍关联容器之前,必须先了解一种与之相关的简单的标准库类型——pair(表 2),该类型在 utility 头文件中定义。
表 2 pair类型提供的操作:

pair类型操作 作用
pair<T1, T2> p1; 创建一个空的 pair 对象,它的两个元素分别是 T1 和 T2类型,采用值初始化(第 3.3.1 节)
pair<T1, T2> p1(v1, v2); 创建一个 pair 对象,它的两个元素分别是 T1 和 T2 类型,其中 first 成员初始化为 v1,而 second 成员初始化为 v2
make_pair(v1,v2) 以 v1 和 v2 值创建一个新 pair 对象,其元素类型分别是v1 和 v2 的类型
p1 < p2 两个 pair 对象之间的小于运算,其定义遵循字典次序:如果 p1.first < p2.first 或者!(p2.first < p1.first) &&p1.second < p2.second,则返回 true
p1 == p2 如果两个 pair 对象的 first 和 second 成员依次相等,则这两个对象相等。该运算使用其元素的 == 操作符
p.first 返回 p 中名为 first 的(公有)数据成员
p.second 返回 p 的名为 second 的(公有)数据成员

pair 的创建和初始化

pair 只包含两个数据值与容器一样,pair 也是一种模板类型。但又与之前介绍的容器不同,在创建 pair 对象时,必须提供两个类型名:pair 对象所包含的两个数据成员各自对应的类型名字,这两个类型名字不必相同。

pair<string, string> anon; // holds two strings
pair<string, int> word_count; // holds a string and an int
pair<string, vector<int> > line; // holds string and vector<int>

如果在创建 pair 对象时不提供初始化式,则调用默认构造函数对其成员采用值初始化。于是,anon 是包含两空 string 类型成员的 pair 对象,line 则存储一个空的 string 类型对象和一个空的vector 类型对象。word_count 中的 int 成员获得 0 值,而 string 成员则初始化为空 string 对象。
当然,也可在定义时为每个成员提供初始化式:

pair<string, string> author("James", "Joyce");

创建一个名为 author 的 pair 对象,它的两个成员都是 string 类型,分别初始化为字符串 “James” 和 “Joyce”。

pair 类型的使用相当繁琐,因此,如果需要定义多个相同的 pair 类型对象,可考虑利用 typedef 简化其声明:

typedef pair<string, string> Author;
Author proust("Marcel", "Proust");
Author joyce("James", "Joyce");

pairs 对象的操作

与其他标准库类型不同,对于 pair 类,可以直接访问其数据成员其成员都是仅有的分别命名为 first 和 second。只需使用普通的点操作符——成员访问标志即可访问其成员:

string firstBook;
// access and test the data members of the pair
if (author.first == "James" && author.second == "Joyce")
firstBook = "Stephen Hero";

标准库只为 pair 类型定义了表 2 所列出的数量有限的操作。

生成新的 pair 对象

除了构造函数,标准库还定义了一个make_pair 函数,由传递给它的两个实参生成一个新的 pair 对象。可如下使用make_pair函数创建新的 pair 对象,并赋给已存在的 pair 对象:

pair<string, string> next_auth;
string first, last;
while (cin >> first >> last) {
// generate a pair from first and last
next_auth = make_pair(first, last);
// process next_auth...
}

这个循环处理一系列的作者信息:在 while 循环条件中读入的作者名字作为实参,调用 make_pair 函数生成一个新的 pair 对象。此操作等价于下面更复杂的操作:

// use pair constructor to make first and last into a pair
next_auth = pair<string, string>(first, last);

由于 pair 的数据成员是公有的,因而可如下直接地读取输入:

pair<string, string> next_auth;
// read directly into the members of next_auth
while (cin >> next_auth.first >> next_auth.second) {
// process next_auth...
}

关联容器

关联容器共享大部分但并非全部的顺序容器操作。关联容器不提供front、 push_front、 pop_front、back、push_back 以及 pop_back 操作。
顺序容器和关联容器公共的操作包括下面的几种:
• 表 2 描述的前三种构造函数:

C<T> c; // creates an empty container   创建空的容器
// c2 must be same type as c1
C<T> c1(c2); // copies elements from c2 into c1  
// b and e are iterators denoting a sequence
C<T> c(b, e); // copies elements from the sequence into c

• 第 9.3.4 节中描述的关系运算。
• 表 9.6 列出的 begin、end、rbegin 和 rend 操作。
• 表 9.5 列出的类型别名(typedef)。注意,对于 map 容器,value_type并非元素的类型,而是描述键及其关联值类型的 pair 类型。第 10.3.2节 将详细解释 map 中的类型别名。
• 表 9.11 中描述的 swap 和赋值操作。但关联容器不提供 assign 函数。
• 表 9.10 列出的 clear 和 erase 操作,但关联容器的 erase 运算返回void 类型。
• 表 9.8 列出的关于容器大小的操作。但 resize 函数不能用于关联容器。

根据键排列元素

除了上述列出的操作之外,关联容器还提供了其他的操作。而对于顺序容器也提供的相同操作,关联容器也重新定义了这些操作的含义或返回类型,其中的差别在于关联容器中使用了键
容器元素根据键的次序排列”这一事实就是一个重要的结论:在迭代遍历关联容器时,我们可确保按键的顺序的访问元素,而与元素在容器中的存放位置完全无关。

map 类型

map是键-值对的集合。map 类型通常可理解为关联数组(associative array):可使用键作为下标来获取一个值,正如内置数组类型一样。而关联的本质在于元素的值与某个特定的键相关联,而并非通过元素在数组中的位置来获取。

map 对象的定义

要使用 map 对象,则必须包含 map 头文件。在定义 map 对象时,必须分别指明键和值的类型(value type)(表 10.4):

// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int

这个语句定义了一个名为 word_count 的 map 对象,由 string 类型的键索引,关联的值则 int 型。

表 3. map 的构造函数

map 的构造函数 作用
map<k, v> m; 创建一个名为 m 的空 map 对象,其键和值的类型分别为 k 和 v
map<k, v> m(m2); 创建 m2 的副本 m,m 与 m2 必须有相同的键类型和值类型
map<k, v> m(b, e); 创建 map 类型的对象 m,存储迭代器 b 和 e 标记的范围内所有元素的副本。元素的类型必须能转换为 pair<const k, v>

键类型的约束

使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。默认情况下,标准库使用键类型(key type)定义的<操作符来实现键的比较。第 15.8.3 节将介绍如何重写默认的操作符,并提供自定义的操作符函数。
所用的比较函数必须在键类型上定义严格弱排序(strict weak ordering)。所谓的严格弱排序可理解为键类型数据上的“小于”关系,虽然实际上可以选择将比较函数设计得更复杂。但无论这样的比较函数如何定义,当用于一个键与自身的比较时,肯定会导致 false 结果。此外,在比较两个键时,不能出现相互“小于”的情况(即A<B和B<A是不能同时发生),而且,如果 k1“小于”k2,k2“小于”k3,则 k1 必然“小于”k3。对于两个键,如果它们相互之间都不存在“小于”关系,则容器将之视为相同的键。用做 map 对象的键时,可使用任意一个键值来访问相应的元素。

在实际应用中,键类型必须定义 < 操作符,而且该操作符应能“正确地工作”这一点很重要
例如,在书店问题中,可增加一个名为 ISBN 的类型,封装与国际标准图书编号(ISBN)相关的规则。在我们的实现中,国际标准图书编号是 string 类型,可做比较运算以确定编号之间的大小关系。因此,ISBN 类型可以支持 < 运算。假设我们已经定义了这样的类型,则可定义一个 map 容器对象,以便高效地查找书店中存放的某本书。

map<ISBN, Sales_item> bookstore;

该语句定义了一个名为 bookstore 的 map 对象,以 ISBN 类型的对象为索引,其所有元素都存储了一个关联的 Sales_item 类类型实例。对于键类型,唯一的约束就是必须支持 < 操作符,至于是否支持其他的关系或相等运算,则不作要求

map 定义的类型

map 对象的元素是键-值对,也即每个元素包含两个部分:键以及由键关联的值map 的 value_type 就反映了这个事实。该类型比前面介绍的容器所使用的元素类型要复杂得多:value_type 是存储元素的键以及值的 pair 类型,而且键为 const。例如,word_count 数组的 value_type 为 pair<const string,
int> 类型。
表 10.4. map 类定义的类型

map 类定义的类型 作用
map<K,V>::key_type 在 map 容器中,用做索引的键的类型
map<K,V>::mapped_type 在 map 容器中,键所关联的值的类型
map<K,V>::value_type 一个 pair 类型,它的first 元素具有 const map<K,V>::key_type 类型,而 second 元素则为 map<K,V>::mapped_type 类型

在学习 map 的接口时,需谨记 value_type 是 pair 类型,它的值成员可以修改,但键成员不能修改。

map 迭代器进行解引用将产生 pair 类型的对象

对容器迭代器进行解引用时,将获得一个引用,指向容器中一个 value_type 类型的值。对于 map 容器,其 value_type 是 pair 类型:

// get an iterator to an element in word_count
map<string, int>::iterator map_it = word_count.begin();
// *map_it is a reference to a pair<const string, int> object
cout << map_it->first; // prints the key for this element
cout << " " << map_it->second; // prints the value of the element
map_it->first = "new key"; // error: key is const
++map_it->second; // ok: we can change value through an iterator

对迭代器进行解引用将获得一个 pair 对象,它的 first 成员存放键,为const,而 second 成员则存放值。

map 容器额外定义的类型别名(typedef)

map 类额外定义了两种类型:key_type 和 mapped_type,以获得键或值的类型。对于 word_count,其 key_type 是 string 类型,而 mapped_type 则是int 型。如同顺序容器(第 9.3.1 节)一样,可使用作用域操作符(scope operator)来获取类型成员,如 map<string, int>::key_type。

map 添加元素

定义了 map 容器后,下一步工作就是在容器中添加键-值元素对。该项工作可使用 insert 成员实现;或者,先用下标操作符获取元素,然后给获取的元素赋值。在这两种情况下,一个给定的键只能对应于一个元素这一事实影响了这些操作的行为。

使用下标访问 map 对象

如下编写程序时:

map <string, int> word_count; // empty map
// insert default initialzed element with key Anna; then assign 1 to its value
word_count["Anna"] = 1;

将发生以下事情:

  1. 在 word_count 中查找键为 Anna 的元素,没有找到。
  2. 将一个新的键-值对插入到 word_count 中。它的键是 const string 类型的对象,保存 Anna。而它的值则采用值初始化,这就意味着在本例中值为 0。
  3. 将这个新的键-值对插入到 word_count 中。
  4. 读取新插入的元素,并将它的值赋为 1。

使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。

如同其他下标操作符一样,map 的下标也使用索引(其实就是键)来获取该键所关联的值

  • 如果该键已在容器中,则 map 的下标运算与 vector 的下标运算行为相同:返回该键所关联的值。
  • 只有在所查找的键不存在时,map 容器才为该键创建一个新的元素,并将它插入到此 map 对象中。此时,所关联的值采用值初始化:类类型的元素用默认构造函数初始化,而内置类型的元素初始化为 0。

下标操作符返回值的使用

通常来说,下标操作符返回左值。它返回的左值是特定键所关联的值。可如下读或写元素:

cout << word_count["Anna"]; // fetch element indexed by Anna; prints 1
++word_count["Anna"]; // fetch the element and add one to it
cout << word_count["Anna"]; // fetch the element and print it; prints 2

有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。
显然,map 迭代器返回 value_type 类型的值——包含 const key_type 和mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。

下标行为的编程意义

对于 map 容器,如果下标所表示的键在容器中不存在,则添加新元素,这一特性可使程序惊人地简练

// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int string word;
while (cin >> word)
	++word_count[word];
//文件计数程序map(如果下标所表示的键在容器中不存在,可行)

这段程序创建一个 map 对象,用来记录每个单词出现的次数。while 循环每次从标准输入读取一个单词。如果这是一个新的单词,则在 word_count 中添加以该单词为索引的新元素。如果读入的单词已在 map 对象中,则将它所对应的值加 1。
其中最有趣的是,在单词第一次出现时,会在 word_count 中创建并插入一个以该单词为索引的新元素,同时将它的值初始化为 0。然后其值立即加 1,所以每次在 map 中添加新元素时,所统计的出现次数正好从 1 开始。

map::insert 的使用

map 容器的 insert 成员与顺序容器的类似,但有一点要注意:必须考虑键的作用。键影响了实参的类型:插入单个元素的 insert 版本使用键-值 pair类型的参数。类似地,对于参数为一对迭代器的版本迭代器必须指向键-值pair 类型的元素。另一个差别则是:map 容器的接受单个值的 insert 版本的返回类型。本节的后续部分将详细阐述这一特性。
表 10.5. map 容器提供的 insert 操作

map 的 insert 操作 作用
m.insert(e) e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则插入一个值为 e.second 的新元素;如果该键在 m 中已存在,则保持 m 不变。该函数返回一个pair 类型对象包含指向键为 e.first 的元素的 map 迭代器,以及一个 bool 类型的对象,表示是否插入了该元素
m.insert(beg,end) beg 和 end 是标记元素范围的迭代器,其中的元素必须为m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在,则将该键及其关联的值插入到 m。(返回 void 类型)
m.insert(iter,e) e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则创建新元素,并以迭代器 iter 为起点搜索新元素存储的位置。(返回一个迭代器,指向 m 中具有给定键的元素)。

以 insert 代替下标运算

使用下标给 map 容器添加新元素时,元素的值部分将采用值初始化。通常,我们会立即为其赋值,其实就是对同一个对象进行初始化并赋值。而插入元素的另一个方法是:直接使用 insert 成员给 map 容器添加新元素,其语法更紧凑:

// if Anna not already in word_count, inserts new element with value 1
word_count.insert(map<string, int>::value_type("Anna", 1));

这个 insert 函数版本的实参:

map<string, int>::value_type(anna, 1)

是一个新创建的 pair 对象,将直接插入到 map 容器中。谨记 value_type是 pair<const K, V> 类型的同义词,K 为键类型,而 V 是键所关联的值的类型。insert 的实参创建了一个适当的 pair 类型新对象,该对象将插入到 map容器。在添加新 map 元素时,使用 insert 成员可避免使用下标操作符所带来的副作用:不必要的初始化。
传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair:

word_count.insert(make_pair("Anna", 1));

或使用 typedef

typedef map<string,int>::value_type valType;
word_count.insert(valType("Anna", 1));

这两种方法都使用调用变得简单,提高了程序的可读性。

检测 insert 的返回值

map 对象中一个给定键只对应一个元素。如果试图插入的元素所对应的键已在容器中,则 insert 将不做任何操作。含有一个或一对迭代器形参的 insert函数版本并不说明是否有或有多少个元素插入到容器中。但是,带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。
如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 true。在这两种情况下,迭代器都将指向具有给定键的元素。

下面是使用 insert 重写的单词统计程序:

// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int string word;
while (cin >> word) {
// inserts element with key equal to word and value 1;
// if word already in word_count, insert does nothing
pair<map<string, int>::iterator, bool> ret =word_count.insert(make_pair(word, 1));
if (!ret.second) // word already in word_count
	++ret.first->second; // increment counter
}
//单词统计程序(键可能在容器中存在的)

对于每个单词,都尝试 insert 它,并将它的值赋 1。if 语句检测 insert函数返回值中的 bool 值。如果该值为 false,则表示没有做插入操作,按 word索引的元素已在 word_count 中存在。此时,将该元素所关联的值加 1。

语法展开

上面代码中ret 的定义和自增运算可能比较难解释:

pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1));

首先,应该很容易看出我们定义的是一个 pair 对象,它的 second 成员为bool 类型。而它的 first 成员则比较难理解,这是 map<string, int> 容器所定义的迭代器类型。
根据操作符的优先级次序(第 5.10.1 节),可如下从添加圆括号开始理解自增操作:

++((ret.first)->second); // equivalent expression

下面对这个表达式一步步地展开解释:
• ret 存储 insert 函数返回的 pair 对象。该 pair 的 first 成员是一个 map 迭代器,指向插入的键。
• ret.first 从 insert 返回的 pair 对象中获取 map 迭代器。
• ret.first->second 对该迭代器进行解引用,获得一个 value_type 类型的对象。这个对象同样是 pair 类型的,它的 second 成员即为我们所添加的元素的值部分。
• ++ret.first->second 实现该值的自增运算。
归结起来,这个自增语句获取指向按 word 索引的元素的迭代器,并将该元素的值加 1。

查找并读取 map 中的元素

下标操作符给出了读取一个值的最简单方法:

map<string,int> word_count;
int occurs = word_count["foobar"];

但是,使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。这样的行为是否正确取决于程序员的意愿
在这个例子中,如果“foobar”不存在,则在 map 中插入具有该键的新元素,其关联的值为 0。在这种情况下,occurs 获得 0 值。
我们的单词统计程序的确是要通过下标引用一个不存在的元素来实现新元素的插入,并将其关联的值初始化为 0。然而,大多数情况下,我们只想知道某元素是否存在,而当该元素不存在时,并不想做做插入运算。对于这种应用,则不能使用下标操作符来判断元素是否存在。
map 容器提供了两个操作:count 和 find,用于检查某个键是否存在而不会插入该键。
表 10.6. 不修改 map 对象的查询操作:

map查询操作 操作 作用
m.count(k) 返回 m 中 k 的出现次数
m.find(k) 如果 m 容器中存在按 k 索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器(第 3.4 节)

使用 count 检查 map 对象中某键是否存在

对于 map 对象,count 成员的返回值只能是 0 或 1。因为map 容器只允许一个键对应一个实例,所以 count 可有效地表明一个键是否存在
而对于multimaps容器,count 的返回值将有更多的用途,相关内容将会在之后介绍。如果返回值非 0,则可以使用下标操作符来获取该键所关联的值,而不必担心这样做会在 map 中插入新元素:

int occurs = 0;
if (word_count.count("foobar"))
	occurs = word_count["foobar"];

当然,在执行 count 后再使用下标操作符,实际上是对元素作了两次查找。如果希望当元素存在时就使用它,则应该用 find 操作。

读取元素而不插入该元素

find 操作返回指向元素的迭代器,如果元素不存在,则返回 end 迭代器:

int occurs = 0;
map<string,int>::iterator it = word_count.find("foobar");
if (it != word_count.end())
	occurs = it->second;

如果希望当具有指定键的元素存在时,就获取该元素的引用,否则就不在容器中创建新元素,那么应该使用 find。

从 map 对象中删除元素

map 容器中删除元素erase 操作三种变化形式(表 10.7)。与顺序容器一样,可向 erase 传递一个或一对迭代器,来删除单个元素或一段范围内的元素。其删除功能类似于顺序容器,但有一点不同:map 容器的 erase 操作返回 void,而顺序容器的 erase 操作则返回一个迭代器,指向被删除元素后面的元素。
除此之外,map 类型还提供了一种额外的 erase 操作其参数是 key_type类型的值,如果拥有该键的元素存在,则删除该元素。对于单词统计程序,可使用这个版本的 erase 函数来删除 word_count 中指定的单词,然后输出被删除的单词:

// erase of a key returns number of elements removed
if (word_count.erase(removal_word))
	cout << "ok: " << removal_word << " removed\n";
else 
	cout << "oops: " << removal_word << " not found!\n";

erase 函数返回被删除元素的个数。对于 map 容器,该值必然是 0 或 1。如果返回 0,则表示欲删除的元素在 map 不存在。
表 10.7. 从 map 对象中删除元素

map查询操作 操作 作用
m.erase(k) 删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数
m.erase( p ) 从 m 中删除迭代器 p 所指向的元素。p 必须指向 m 中确实存在的元素,而且不能等于 m.end()。返回 void
m.erase(b,e) 从 m 中删除一段范围内的元素,该范围由迭代器对 b 和 e 标记。
b 和 e 必须标记 m 中的一段有效范围:即 b 和 e 都必须指向m 中的元素或最后一个元素的下一个位置。而且,b 和 e 要么相等(此时删除的范围为空),要么 b 所指向的元素必须出现在 e 所指向的元素之前。返回 void 类型

map 对象的迭代遍历(迭代器)

与其他容器一样,map 同样提供 begin 和 end 运算,以生成用于遍历整个容器的迭代器。例如,可如下将 map 容器 word_count 的内容输出:

// get iterator positioned on the first element
map<string, int>::const_iterator map_it = word_count.begin();
// for each element in the map
while (map_it != word_count.end()) {
// print the element key, value pairs
	cout << map_it->first << " occurs "<< map_it->second << " times" << endl;
	++map_it; // increment iterator to denote the next element
}

while 循环的条件判断以及循环体中迭代器的自增都与输出 vector 或string 容器内容的程序非常相像。首先,初始化 map_it 迭代器,使之指向word_count 的第一元素。只要该迭代器不等于 end 的值,就输出当前元素并给迭代器加 1。这段程序的循环体要比前面类似的程序更加复杂,原因在于对于
map 的每个元素都必须分别输出它的键和值。
这个单词统计程序依据字典顺序输出单词。在使用迭代器遍历map 容器时,迭代器指向的元素按键的升序排列。

“单词转换” map 对象

下面的程序说明如何创建、查找和迭代遍历一个 map 对象,我们将以此结束本节内容。这个程序求解的问题是:给出一个 string 对象,把它转换为另一个 string 对象。
本程序的输入是两个文件。第一个文件包括了若干单词对,每对的第一个单词将出现在输入的字符串中,而第二个单词则是用于输出。本质上,这个文件提供的是单词转换的集合——在遇到第一个单词时,应该将之替换为第二个单词。
第二个文件则提供了需要转换的文本。如果单词转换文件的内容是:

'em them
cuz because
gratz grateful
i I
nah no
pos supposed
sez said
tanx thanks
wuz was

而要转换的文本是:

nah i sez tanx cuz i wuz pos to
not cuz i wuz gratz

则程序将产生如下输出结果:

no I said thanks because I was supposed to
not because I was grateful

单词转换程序

下面给出的解决方案是将单词转换文件的内容存储在一个 map 容器中,将被替换的单词作为键,而用作替换的单词则作为其相应的值。接着读取输入,查找输入的每个单词是否对应有转换。若有,则实现转换,然后输出其转换后的单词,否则,直接输出原词。

该程序的 main 函数需要两个实参(第 7.2.6 节):单词转换文件的名字以及需要转换的文件名。程序执行时,首先检查实参的个数。第一个实参 argv[0]是命令名,而执行该程序所需要的两个文件名参数则分别存储在 argv[1] 及argv[2] 中
如果 argv[1] 的值合法,则调用 open_file(第 8.4.3 节)打开单词转换文件。假设 open 操作成功,则读入“单词转换对”。以“转换对”中的第一个单词为键,第二个为值,调用 insert 函数在容器中插入新元素。while 循环结束后,trans_map 容器对象包含了转换输入文本所需的数据。而如果该实参有问
题,则抛出异常(第 6.13 节)并结束程序的运行。

接下来,调用 open_file 打开要转换的文件。第二个 while 循环使用getline 函数行读入文件。因为程序每次读入一行,从而可在输出文件的相同位置进行换行。然后在内嵌的 while 循环中使用stringstream将每一行中的单词提取出来。这部分程序与第 8.5 节的程序框架类似。
内层的 while 循环检查每个单词,判断它是否在转换的 map 中出现。如果在,则从该 map 对象中取出对应的值替代此单词。最后,无论是否做了转换,都输出该单词。同时,程序使用 bool 值 firstword 判断是否需要输出空格。如果当前处理的是这一行的第一个单词,则无须输出空格。

/*
* A program to transform words.
* Takes two arguments: The first is name of the word transformation
file
* The second is name of the input to transform
*/
int main(int argc, char **argv)
{
// map to hold the word transformation pairs:
// key is the word to look for in the input; value is word to use in the output
	map<string, string> trans_map;  //用于记录转换映射文件的容器
	string key, value; 
	if (argc != 3)  //如果输入参数不等于三 抛出错误
		throw runtime_error("wrong number of arguments");
// open transformation file and check that open succeeded
	ifstream map_file; //文件流
	if (!open_file(map_file, argv[1]))  //打开转换文件的文件流
		throw runtime_error("no transformation file");
// read the transformation map and build the map
	while (map_file >> key >> value)  //这个输入厉害啊!
		trans_map.insert(make_pair(key, value));
// ok, now we're ready to do the transformations
// open the input file and check that the open succeeded
	ifstream input;
	if (!open_file(input, argv[2]))
		throw runtime_error("no input file");
	string line; // hold each line from the input
// read the text to transform it a line at a time
	while (getline(input, line))
	 {
		istringstream stream(line); // read the line a word at a time 它的作用是从string对象str中读取字符。
		//istringstream的构造函数原形如下:istringstream::istringstream(string str);
		string word;
		bool firstword = true; // controls whether a space is printed
		while (stream >> word) 
		{
// ok: the actual mapwork, this part is the heart of the program
			map<string, string>::const_iterator map_it =trans_map.find(word);
// if this word is in the transformation map
			if (map_it != trans_map.end())
// replace it by the transformation value in the map
				word = map_it->second;
			if (firstword)
				firstword = false;
			else
				cout << " "; // print space between words
			cout << word;
		}
		cout << endl; // done with this line of input
	}
return 0;
}

参考资料

【1】C++ Primer 中文版(第四版·特别版)

注解

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104779786