STL 实践与分析
-- 容器的综合应用:文本查询程序
引言:
本章中最重点的实例,因为不需要用到 multiset 与 multimap 的内容,于是将这一小节提到了前面,通过这个实例程序,大师分析问题的智慧,大师的编程风格,大师对程序的控制能力,由此可见一斑。因此,我对这一小节的内容几乎不做修改,或只做很小的更改(因为有些东西不同人有不同的理解),搬出来,以供大家仔细品读。
要求:
我们的程序将读取用户指定的 任意文本文件 , 然后允许用户从该文件中查找单词。 查询的结果是该单词出现的次数 , 并列出每次出现所在的行。 如果某单词在同一行中多次出现 , 程序将只显示该行一次 。行号按升序显示。
样例:
以本章的内容作为文件输入 , 然后查找单词“ element” 。输出的前几行应为 :
element occurs 125 times (line 62) element with a given key. (line 64) second element with the same key. (line 153) element |==| operator. (line 250) the element type. (line 398) corresponding element. …
一、查询程序的设计
设计程序的一个良好习惯是首先将程序所涉及的操作先列出来,明确需要提供的操作有助于建立需要的数据结构和实现这些行为 。从需求出发,我们的程序需要支持以下任务:
1 )它必须允许用户指明要处理的文件名字。程序将存储该文件的内容 , 以便输出每个单词所在的原始行。
2 ) 它必须将每一行分解为各个单词 , 并记录每个单词所在的所有行 。在输出行号时 , 应保证以升序输出 , 并且不重复。
3 )对特定单词的查询将返回出现该单词的所有行的行号。
4 )输出某单词所在的行文本时 , 程序必须能根据给定的行号从输入文件中获取相应的行。
1 、数据结构
设计一个简单的 TextQuery 类来实现这个程序:
1 )使用一个 vector<string> 对象来存储整个输入文件。输入文件的每一行是该vector 对象的一个元素。因而 , 在希望输出某一行时 , 只需以行号为下标获取该行所在的元素即可。
2 )将每个单词所在的行号存储在一个 set 容器对象中。使用 set 就可确保每行只有一个条目 , 而且行号将自动按升序排列。
3 )使用一个 map 容器将每个 单词 与 一个 set 容器对象 关联起来 , 该 set 容器对象记录此单词所在的行号。
2 、操作
对于类还要求有良好的接口 。然而 , 一个重要的设计策略首先要确定 : 查询函数需返回存储一组行号的 set 对象。这个返回类型应该如何设计呢 ?
事实上 , 查询的过程相当简单 : 使用下标访问 map 对象获取关联的 set 对象即可。唯一的问题是如何返回所找到的 set 对象。安全的设计方案是返回该 set 对象的副本。但如此一来 , 就意味着要复制 set 中的每个元素。如果处理的是一个相当庞大的文件 ,则复制 set 对象的代价会非常昂贵。其他可行的方法包括 : 返回一个 pair 对象 , 存储一对指向 set 中元素的迭代器 ; 或者返回 set 对象的 const 引用。为简单起见 , 我们在这里采用返回副本的方法 , 但注意 : 如果在实际应用中复制代价太大 , 需要新考虑其实现方法。
第一、第三和第四个任务是使用这个类的程序员将执行的动作。第二个任务则是类的内部任务。将这四任务映射为类的成员函数 , 则类的接口需提供下列三个 public函数 :
1 ) read_file 成员函数 , 其形参为一个 ifstream& 类型对象。该函数每次从文件中读入一行 , 并将它保存在 vector 容器中。输入完毕后 ,read_file 将创建关联每个单词及其所在行号的 map 容器。
2 ) run_query 成员函数 , 其形参为一个 string 类型对象 , 返回一个 set 对 象 , 该set 对象包含出现该 string 对象的所有行的行号。
3 ) text_line 成员函数 , 其形参为一个行号 , 返回输入文本中该行号对应的文本行。
无论 run_query 还是 text_line 都不会修改调用此函数的对象 , 因此 , 可将这两个操作定义为 const 成员函数。
为实现 read_file 功能 , 还需定义两个 private 函数来读取输入文本和创建 map 容器:
1 ) store_file 函数读入文件 , 并将文件内容存储在 vector 容器对象中。
2 ) build_map 函数将每一行分解为各个单词 , 创建 map 容器对象 , 同时记录每个单词出现行号。
二、 TextQuery 类
//in TextQuery.h class TextQuery { public: typedef std::vector<std::string>::size_type line_no; void read_file(std::ifstream &is) { store_file(is); build_map(); } std::set<line_no> run_query(const std::string &) const; std::string text_line(line_no) const; private: void store_file(std::ifstream &); void build_map(); std::vector<std::string> line_of_text; std::map< std::string,std::set<line_no> > word_map; };
三、 TextQuery 类的使用
1 、主程序
//in main.cpp /**记得添加 *#include "TextQuery.h" 类的定义 *#include "other.h" 其他函数的声明 */ int main(int argc,char **argv) { ifstream inFile; if (argc != 2 || !open_file(inFile,argv[1])) { cerr << "No input file!" << endl; return EXIT_FAILURE; } TextQuery tq; tq.read_file(inFile); while (true) { cout << "enter word to look for,or q/Q to quit: "; string s; cin >> s; if (!cin || s == "q" || s == "Q") break; set<TextQuery::line_no> locs = tq.run_query(s); print_results(locs,s,tq); } return 0; }
2 、辅助函数
print_results 函数、 make_plural 函数、 open_file 函数的定义 :
//in other.cpp /**记得添加 *#include "TextQuery.h" 类的定义 *#include "other.h" 其他函数的声明 */ string make_plural(set<TextQuery::line_no size,const string &begin,const string &ends); ifstream &open_file(ifstream &in,const string &file) { in.close(); in.clear(); in.open(file.c_str()); return in; } void print_results(set<TextQuery::line_no> &locs, const string &sought, const TextQuery &file) { typedef set<TextQuery::line_no> line_nums; line_nums::size_type size = locs.size(); cout << "\n" << sought << " occurs " << size << " " << make_plural(size,"time","s") << endl; line_nums::iterator iter = locs.begin(); while (iter != locs.end()) { cout << "\t(line " << (*iter) + 1 << ")" << file.text_line(*iter) << endl; } } string make_plural(set<TextQuery::line_no size, const string &begin, const string &ends) { return (size <= 1 ? begin : begin + ends); }
四、编写成员函数 [inTextQuery.h]
1 、存储输入文件
void TextQuery::store_file(ifstream &is) { string textline; while (getline(is,textline)) { line_of_text.push_back(textline); } }
2 、建立单词 map 容器
void TextQuery::build_map() { for (line_no line_num = 0; line_num != line_of_text.size(); ++line_num) { istringstream line(line_of_text[line_num]); string word; while (line >> word) { word_map[word].insert(line_num); } } }
【分析:】
word_map[word].insert(line_num);
将 word 用做 map 容器的下标。如果 word 在 word_map 容器对象中不存在 , 那么下标操作符将该 word 添加到此容器中 , 并将其关联的值初始化为空的 set 。不管是否添加了 word, 下标运算都返回一个 set 对象 , 然后调用 insert 函数在该 set 对象中添加当前行号。如果某个单词在同一行中重复出现 , 那么 insert 函数的调用将不做任何操作。
3 、支持查询
set<TextQuery::line_no> TextQuery::run_query(const std::string &query_word) const { map<string,set<line_no> >::const_iterator loc = word_map.find(query_word); if (loc == word_map.end()) { return set<TextQuery::line_no>(); } else { return loc -> second; } }
run_query 函数带有指向 conststring 类型对象的引用参数 , 并以这个参数作为下标来访问 word_map 对象。假设成功找到这个 string, 那么该函数返回关联此 string 的 set对象 , 否则返回一个空的 set 对象。
4 、 run_query 返回值的使用
string TextQuery::text_line(line_no line) const { if (line < line_of_text.size()) { return line_of_text[line]; } throw out_of_range("line number out of range"); }