第十四篇:一个文本查询程序的实现
前言
本文将讲解一个经典的文本查询程序,对前面所学的容器相关知识进行一个从理论到实际的升华,同时也对即将学习的面向对象知识来一次初体验。
程序描述
要求实现这样一个程序:读取用户指定的文件,然后允许用户从中查找某个单词所在的位置。
一个面向过程的落后的设计思想
将待检索文件以行为单位存放到Vector容器中,然后遍历容器,将容器内元素依次转存到字符串流对象中,然后在内层遍历这个字符串流对象,检索是否存在与给定单词匹配的单词。如果有则输出该行内容以及该行序号。
落后的原因及先进的设计思想
这是我以前尝试解决这个问题的思路。其本质是一个典型的面向过程思想,只不过用容器简化了些操作罢了。
这种思路使得每次执行检索都要重新遍历一次文件。如果当文件比较大的时候,这样的检索效率是不能为用户所接受的。
最好的方法应该是采用面向对象的方法,设定一个类,该类封装一个数据结构专门记载关于单词与行号的信息,该类还同时封装初始化函数,查询函数等功能函数。
另外,如果我这里不设定一个类,而是直接全局定义一个数据结构来记载关于单词与行号的信息,那么当还要对文本实现一些其他功能的时候,程序将会变得杂乱无章,代码里到处都是乱七八糟的数据结构和全局变量。这就是类封装性的好处,也是面向对象的美妙之处之一。
下面,将用面向对象的思想“美妙”地设计出这个文本查询程序... ...
第一步:设计类
第1步:确定类所包含的方法
1. read_file 函数:将指定的待检索文件存入容器并初始化”单词-行号“数据结构
2. run_query 函数:获取待查询单词并返回单词在文本中的行号
3. text_line 函数:获取某个行号,返回文件中该行的内容。
第2步:确定类所包含的数据
1. 一个string对象存放要查询的单词
2. 一个vector容器存放待检索的文本
3. 一个map容器存放单词和它对应的行号
类定义如下():
1 class TextQuery { 2 public: 3 // 为行号类型取个别名( 行号的类型实在是太长了 ) 4 typedef std::vector<std::string>::size_type line_no; 5 // 将数据存入vector容器并初始化单词 - 行号数据结构 6 void read_file(std::ifstream &is) { 7 // 将待检索文件存入容器 8 store_file(is); 9 // 建立单词 - 行号数据结构 10 build_map(); 11 } 12 // 根据用户指定的单词执行查询并返回结果行号( 结果是放在一个set容器中的 )。 13 std::set<line_no> run_query(const std::string&) const; 14 // 根据行号返回该行内容 15 std::string text_line(line_no) const; 16 private: 17 // 下面这两个函数是上面read_file 函数的实现函数,是内部函数因此设为私有。 18 void store_file(std::ifstream&); // 将数据存入vector容器 19 void build_map(); // 初始化单词 - 行号数据结构 20 // 一个vector容器 21 std::vector<std::string> lines_of_text; 22 // 一个单词 - 行号数据结构 23 std::map< std::string, std::set<line_no> > word_map; // 注意这个是容器的容器 因此尖括号后面要留空格 24 };
如此一来,整个程序的框架就显得豁然开朗。可见设计类这一环节的重要性。事实上,工程中常用UML之类的技术专门处理这个环节。
第二步:实现类
1. 实现store_file 函数:
1 void TextQuery::store_file(ifstream & is) { 2 string textline; 3 while (getline(is, textline)) 4 lines_of_text.push_back(textline); 5 }
2. 实现bulid_map 函数:
1 void TextQuery::build_map() 2 { 3 for (line_no line_num = 0; line_num != lines_of_text.size()) { 4 istringstream line(lines_of_text[line_num]); 5 string word; 6 while (line >> word) 7 word_map[word].insert(line_num); 8 } 9 }
3. 实现run_query 函数:
1 set<TextQuery::line_no> 2 TextQuery::run_query(const string &query_word) const { 3 map< string, set<line_no> >::const_iterator loc = word_map.find(query_word); 4 if (loc == word_map.end()) 5 return set<line_no>(); 6 else 7 return loc->second; 8 }
4. 实现text_line 函数:
1 string TextQuery::text_line(line_no line) const { 2 if (line < lines_of_text.size()) { 3 return lines_of_text(line); 4 } 5 throw std::out_of_range("line number out of range"); 6 }
第三步:编写主函数( 其实可以和第二步同时进行 提高效率 )
1 void print_results(const set<TextQuery::line_no> & locs, const string &sought, const TextQuery &file); 2 ifstream & open_file(ifstream &in, const string &file); 3 string make_plural(size_t ctr, const string &word, const string &ending); 4 5 int main(int argc, char **argv) 6 { 7 ifstream infile; 8 if (argc < 2 || !open_file(infile, argv[1])) { 9 cerr << "No input file!" << endl; 10 return EXIT_FAILURE; 11 } 12 13 TextQuery tq; 14 tq.read_file(infile); 15 16 while (true) { 17 cout << "inter a word to query:" << endl; 18 string s; 19 cin >> s; 20 if (!cin || s == "q") break; 21 set<TextQuery::line_no> locs = tq.run_query(s); 22 print_results(locs, s, tq); 23 } 24 25 return 0; 26 } 27 28 void print_results(const set<TextQuery::line_no> & locs, const string &sought, const TextQuery &file) { 29 // 为了表示下面这个size_type类型,还真煞费苦心。 30 typedef set<TextQuery::line_no> line_nums; 31 line_nums::size_type size = locs.size(); 32 cout << endl << sought << " occurs " << size << " " << make_plural(size, "time", "s") << endl; 33 34 line_nums::const_iterator it = locs.begin(); 35 for (; it != locs.end(); ++it) { 36 cout << "\t(line " << (*it) + 1 << ")" << file.text_line(*it) << endl; 37 } 38 } 39 40 ifstream & open_file(ifstream &in, const string &file) 41 { 42 in.close(); 43 in.clear(); 44 45 in.open(file.c_str()); 46 47 return in; 48 } 49 50 string make_plural(size_t ctr, const string &word, const string &ending) 51 { 52 return (ctr == 1) ? word : word+ending; 53 54 }
说明
第一步,第二步,第三步的代码文件分别为主函数源代码文件( .cpp ),类定义头文件( .h )和类实现源代码文件( .cpp )。
在构建这种工程时,头文件和命名空间的设定是有讲究的( 若要生成真正的可执行程序,上面的代码还要根据一些原则进行改动 ),本文不进行详述。
小结
1. 体验面向对象”工程“ ( 这一部分目前也有点还没弄清楚 待深入学习 )
2. 灵活使用容器
3. 感受面向对象思想