您的位置:首页 > 编程语言 > C语言/C++

C++ Primer 第二遍阅读笔记(第十章)

2013-02-12 14:20 309 查看
关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。

表 10.1. 关联容器类型



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

表 10.2 pairs 类型提供的操作





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



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



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



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



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



顺序容器和关联容器公共的操作包括下面的几种:



>, <, ==, !=关系运算。

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

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



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

表 10.3. map 的构造函数



在使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。默认情况下,标准库使用键类型定义的 < 操作符来实现键(key type)的比较。

对于键类型,唯一的约束就是必须支持 < 操作符,至于是否支持其他的关系或相等运算,则不作要求。

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

表 10.4. map 类定义的类型



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

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



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

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

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





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

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



有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。

显然,map 迭代器返回 value_type 类型的值——包含 const key_type 和 mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。

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



这段程序创建一个 map 对象,用来记录每个单词出现的次数。while 循环每次从标准输入读取一个单词。如果这是一个新的单词,则在 word_count 中添加以该单词为索引的新元素。如果读入的单词已在 map 对象中,则将它所对应的值加 1。

其中最有趣的是,在单词第一次出现时,会在 word_count 中创建并插入一个以该单词为索引的新元素,同时将它的值初始化为 0。然后其值立即加 1,所以每次在 map 中添加新元素时,所统计的出现次数正好从 1 开始。

表 10.5. map 容器提供的 insert 操作





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

这个 insert 函数版本的实参:



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

传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair:



或使用 typedef



map 对象中一个给定键只对应一个元素。如果试图插入的元素所对应的键已在容器中,则 insert 将不做任何操作。含有一个或一对迭代器形参的 insert 函数版本并不说明是否有或有多少个元素插入到容器中。

带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 true。在这两种情况下,迭代器都将指向具有给定键的元素。下面是使用 insert 重写的单词统计程序:



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

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



但是,使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。

我们的单词统计程序的确是要通过下标引用一个不存在的元素来实现新元素的插入,并将其关联的值初始化为 0。然而,大多数情况下,我们只想知道某元素是否存在,而当该元素不存在时,并不想做做插入运算。对于这种应用,则不能使用下标操作符来判断元素是否存在。

map 容器提供了两个操作:count 和 find,用于检查某个键是否存在而不会插入该键。

表 10.6. 不修改 map 对象的查询操作



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



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



从 map 容器中删除元素的 erase 操作有三种变化形式(表 10.7)。与顺序容器一样,可向 erase 传递一个或一对迭代器,来删除单个元素或一段范围内的元素。其删除功能类似于顺序容器,但有一点不同:map 容器的 erase 操作返回 void,而顺序容器的 erase 操作则返回一个迭代器,指向被删除元素后面的元素。

除此之外,map 类型还提供了一种额外的 erase 操作,其参数是 key_type 类型的值,如果拥有该键的元素存在,则删除该元素。对于单词统计程序,可使用这个版本的 erase 函数来删除 word_count 中指定的单词,然后输出被删除的单词:



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

表 10.7. 从 map 对象中删除元素



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



这个单词统计程序依据字典顺序输出单词。在使用迭代器遍历 map 容器时,迭代器指向的元素按键的升序排列。

map 容器是键-值对的集合,好比以人名为键的地址和电话号码。相反地,set 容器只是单纯的键的集合。例如,某公司可能定义了一个名为 bad_checks 的 set 容器,用于记录曾经给本公司发空头支票的客户。当只想知道一个值是否存在时,使用 set 容器是最适合的。

除了两种例外情况,set 容器支持大部分的 map 操作,包括下面几种:



两种例外包括:set 不支持下标操作符,而且没有定义 mapped_type 类型。在 set 容器中,value_type 不是 pair 类型,而是与 key_type 相同的类型。它们指的都是 set 中存储的元素类型。这一差别也体现了 set 存储的元素仅仅是键,而没有所关联的值。与 map 一样,set 容器存储的键也必须唯一,而且不能修改。

为了使用 set 容器,必须包含 set 头文件。set 支持的操作基本上与 map 提供的相同。

与 map 容器一样,set 容器的每个键都只能对应一个元素。以一段范围的元素初始化 set 对象,或在 set 对象中插入一组元素时,对于每个键,事实上都只添加了一个元素:



首先创建了一个名为 ivec 的 int 型 vector 容器,存储 20 个元素:0-9(包括 9)中每个整数都出现了两次。然后用 ivec 中所有的元素初始化一个 int 型的 set 容器。则这个 set 容器仅有 10 个元素:ivec 中不相同的各个元素。

可使用 insert 操作在 set 中添加元素:



另一种用法是,调用 insert 函数时,提供一对迭代器实参,插入其标记范围内所有的元素。该版本的 insert 函数类似于形参为一对迭代器的构造函数——对于一个键,仅插入一个元素:



与 map 容器的操作一样,带有一个键参数的 insert 版本返回 pair 类型对象,包含一个迭代器和一个 bool 值,迭代器指向拥有该键的元素,而 bool 值表明是否添加了元素。使用迭代器对的 insert 版本返回 void 类型。

set 容器不提供下标操作符。为了通过键从 set 中获取元素,可使用 find 运算。如果只需简单地判断某个元素是否存在,同样可以使用 count 运算,返回 set 中该键对应的元素个数。当然,对于 set 容器,count 的返回值只能是 1(该元素存在)或 0(该元素不存在):



正如不能修改 map 中元素的键部分一样,set 中的键也为 const。在获得指向 set 中某元素的迭代器后,只能对其做读操作,而不能做写操作:



map 和 set 容器中,一个键只能对应一个实例。而 multiset 和 multimap 类型则允许一个键对应多个实例。例如,在电话簿中,每个人可能有单独的电话号码列表。在作者的文章集中,每位作者可能有单独的文章标题列表。multimap 和 multiset 类型与相应的单元素版本具有相同的头文件定义:分别是 map 和 set 头文件。

multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,只有一个例外:multimap 不支持下标运算。

由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素。例如,可如下定义一个 multimap 容器对象将作者映射到他们所写的书的书名上。这样的映射可为一个作者存储多个条目:



带有一个键参数的 erase 版本将删除拥有该键的所有元素,并返回删除元素的个数。而带有一个或一对迭代器参数的版本只删除指定的元素,并返回 void 类型:



注意到,关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和 multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放。

迭代遍历 multimap 或 multiset 容器时,可保证依次返回特定键所关联的所有元素。

count 函数求出某键出现的次数,而 find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例:



首先,调用 count 确定某作者所写的书籍数目,然后调用 find 获得指向第一个该键所关联的元素的迭代器。for 循环迭代的次数依赖于 count 返回的值。在特殊情况下,如果 count 返回 0 值,则该循环永不执行。

另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_bound 和 upper_bound。表 10.8 列出的这些操作适用于所有的关联容器,也可用于普通的 map 和 set 容器,但更常用于 multimap 和 multiset。所有这些操作都需要传递一个键,并返回一个迭代器。

表 10.8. 返回迭代器的关联容器操作



在同一个键上调用 lower_bound 和 upper_bound,将产生一个迭代器范围(第 9.2.1 节),指示出该键所关联的所有元素。如果该键在容器中存在,则会获得两个不同的迭代器:lower_bound 返回的迭代器指向该键关联的第一个实例,而 upper_bound 返回的迭代器则指向最后一个实例的下一位置。如果该键不在 multimap 中,这两个操作将返回同一个迭代器,指向依据元素的排列顺序该键应该插入的位置。

lower_bound 返回的迭代器不一定指向拥有特定键的元素。如果该键不在容器中,则 lower_bound 返回在保持容器元素顺序的前提下该键应被插入的第一个位置。

使用这些操作,可如下重写程序:



调用 lower_bound 定位 beg 迭代器,如果键 search_item 在容器中存在,则使 beg 指向第一个与之匹配的元素。如果容器中没有这样的元素,那么 beg 将指向第一个键比 search_item 大的元素。调用 upper_bound 设置 end 迭代器,使之指向拥有该键的最后一个元素的下一位置。

这两个操作不会说明键是否存在,其关键之处在于返回值给出了迭代器范围。

若该键没有关联的元素,则 lower_bound 和 upper_bound 返回相同的迭代器:都指向同一个元素或同时指向 multimap 的超出末端位置。它们都指向在保持容器元素顺序的前提下该键应被插入的位置。

事实上,解决上述问题更直接的方法是:调用 equal_range 函数来取代调用 upper_bound 和 lower_bound 函数。equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。

使用 equal_range 函数再次修改程序:



本程序的 pos.first 等价于前一方法中的 beg,而 pos.second 等价于 end。

文本查询程序

TextQuery类

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <stdexcept>
#include <fstream>
#include <sstream>
using namespace std;

class TextQuery
{
public:
typedef vector<string>::size_type line_no;

void read_file(ifstream &is)
{
store_file(is);
build_map();
}
set<line_no> run_query(const string&) const;
string text_line(line_no) const;
private:
void store_file(ifstream&);
void build_map();
vector<string> lines_of_text;
map< string, set<line_no> > word_map;

};
set<TextQuery::line_no> TextQuery::run_query(const string &query_word) const
{
map<string, set<line_no> >::const_iterator
loc = word_map.find(query_word);
if(loc == word_map.end())
return set<line_no>();
else
{
return loc->second;
}
}
string TextQuery::text_line(line_no line) const
{
if(line < lines_of_text.size())
return lines_of_text[line];
throw out_of_range("line number out of range");
}
void TextQuery::store_file(ifstream &is)
{
string textline;
while(getline(is, textline))
lines_of_text.push_back(textline);
}
void TextQuery::build_map()
{
for(line_no line_num = 0;
line_num != lines_of_text.size(); ++line_num)
{
istringstream line(lines_of_text[line_num]);
string word;
while(line >> word)
{
word_map[word].insert(line_num);
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: