容器的综合应用:文本查询程序(摘自C++ Primer)
设计程序的一个良好习惯是首先将程序所涉及的操作列出来。明确需要提供的操作有助于建立需要的数据结构和实现这些行为。
本例有如下需求:
1.它必须允许用户指明处理的文件名字。程序将存储该文件的内容,以便输出每个单词所在的原始行。
2.它必须将每一行分解为各个单词,并记录每个单词所在的所有行。字输出行号时,应保证以升序输出,并且不重复。
3.对特定单词的查询将返回出现该单词的所有行的行号。
4.输出某单词所在的行文本时,程序必须能根据给定的行号从输入的文件中获取相应的行。
数据结构
TestQuery类
1.使用一个vector<string>类型的对象存储整个输入文件的副本。输入文件的每一行是该vector对象的一个元素。因而,在希望输出某一行时,只需以行号为下标获取该行所在的元素即可。
2.将每个单词所在的行号存储在一个set容器队形中。使用set就可确保每行只有一个条目,而且行号将自动按升序排列。
3.使用一个map容器将每个单词与一个set容器对象关联起来,该set容器对象记录此单词所在的行号。
综上,TextQuery类将有两个数据成员:存储输入文件的vector对象,以及一个map容器对象,该对象关联每个书u单词以及记录该单词所在行号的set容器对象。
操作
对于类还要求有良好的接口。然而,一个重要的设计策略首先要确定:查询函数需返回存储一组行号的set对象。这个返回类型应该如何设计呢?
事实上,查询的过程相当简单:使用下标访问map对象获取关联的set对象即可。唯一的问题是如何返回所找到的set对象。安全的设计方案是返回该set对象的副本。但如此一来,就以为着要赋值set中的每个元素。如果处理的是一个相当庞大的文件,则复制set对象的代价会非常昂贵。其他可行的方法包括:返回一个pair对象,存储一对指向set中元素的迭代器;或者返回set对象的const引用。为简单起见,我们在这里采用返回副本的方法,但注意:如果在实际应用中复制代价太大,需要新考虑其实现方法。
第一(指定文件名字)、第三(返回行号)和第四(输出所在行,根据行号输出内容)个任务是使用这个类的程序员将执行的动作。第二(分解每行的单词,记录所在行。升序输出行号)个任务则是类的内部任务。将这四个任务映射为类的成员函数,则类的接口需要提供下列三个public函数:
- read_file成员函数,其形参为一个ifstream& 类型对象。该函数每次从文件中输入一行,并将它保存在vector容器中。输入完毕后,read_file将创建关联每个单词及其所在行号的map容器。
- run_query成员函数,其形参为一个string类型对象,返回一个set对象,该set对象包含出现该string对象的所有行的行号。
- text_line成员函数,其形参为一个行号,返回输入文本中该行号对应的文本行。
无论run_query还是text_line都不会修改调用此函数的对象,因此,可将这两个操作定义为const成员函数。
为实现read_fie功能,还需定义两个private函数来读取输入文本和创建map容器:
store_file 函数读入文件,并将文件内容存储在vector容器对象中
build_map 函数将每一行分解为各个单词,创建map容器对象,同时记录每个单词出现行号。
源代码:
------------------------------------TextQuery.h-------------------------------------------
#include <vector>
#include <map>
#include <set>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <stdexcept>
using namespace std;
#ifndef TEXTQUERY_H
#define TEXTQUERY_H
//typedef to make declarations easier
typedef vector<string>::size_type line_no;
class TextQuery{
public:
/*interface:
*read_file builds internal data structures for the given file
*run_query finds the given word and returns set of lines on which it appears
*text_line returns a requested line from the input file
*/
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:
//utility functions used by read_file
void store_file(ifstream&); //store input file
void build_map(); //associated each word with a set of line numbers
//remember the whole input file
vector<string> lines_of_text;
//map word to set of the lines on which it occurs
map< string,set<line_no> > word_map;
};
#endif
---------------------------------------------TextQuery.cpp--------------------------------
//read input file :store each line as element in lines_of_text
void TextQuery::store_file(ifstream& is)
{
string textline;
while(getline(is, textline))
lines_of_text.push_back(textline);
}
//finds whitespace-separated words in the input vector
//and puts the word in word_map along with the line number
void TextQuery::build_map()
{
//process each line from the input vector
for(line_no line_num = 0;
line_num != lines_of_text.size();
++line_num)
{
//we'll use line to read the text a word at a time
istringstream line(lines_of_text[line_num]);
string word;
while(line >> word)
//add this line nmber to the set;
//subscript will add word to the map if it's not alread there
word_map[word].insert(line_num); //word_map[word]是一个set对象,该语句调用起insert函数,将行号插入。
}
}
set<TextQuery::line_no>
TextQuery::run_query(const string &query_word) const
{
//Note: must use find and not subscript the map directly
//to avoid adding words to word_map!
map<string, set<line_no> >::const_iterator
loc = word_map.find(query_word);
if(loc == word_map.end())
return set<line_no>();// not found return empty set
else
//fetch and return set of line numbers for this word
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");
}
----------------------------------------tq.cpp---------------------------------------
#include "TextQuery.h"
using namespace std;
//opens in binding it to the given file
ifstream& open_file(ifstream &in, const string &file)
{
in.close(); //close in case it was already open
in.clear(); //clear any existing errors
//if the open fails, the stream will be in an invalid state
in.open(file.c_str()); //open the file we were given
return in; //condition state is godd if open succeeded
}
//return plural version of word if ctr isn't 1
string make_plural(size_t ctr, const string &word, const string &ending)
{
return (ctr == 1)? word : word+ending;
}
void print_results(const set<TextQuery::line_no> &locs, const string& sought, const TextQuery &file)
{
//if the word was found, then print count and all occurrences
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;
//print each line in which the word appeared
line_nums::const_iterator it = locs.begin();
for(;it !=locs.end(); ++it){
cout<<"\t(line "
<<(*it) +1 <<") "
<<file.text_line(*it)<<endl;
}
}
//program takes single argument specifying the file to query
int main(int argc, char **argv)
{
//open the file from which user will query words
ifstream infile;
if(argc < 2 || !open_file(infile,argv[1])){
cerr<<"No input file!"<<endl;
return EXIT_FAILURE;
}
TextQuery tq;
tq.read_file(infile); //build query map
//iterate with the user: prompt for a word to find and printf results
//loop indefinitely: the loop exit is inside the while
while(true){
cout<<"enter word to look for, or q to quit:";
string s;
cin >> s;
//stop if hit eof on input or a 'q' is entered
if(!cin || s == "q" )break;
//get the set of line numbers on which this word appears
set<TextQuery::line_no> locs = tq.run_query(s);
//print count and all occurrences,if any
print_results(locs, s, tq);
}
return 0;
}
PS: 1.string对象的c_str,将string对象转化为char *
2.istringstream对象可以绑定一行字符串,然后以空格为分隔符把该行分隔开来。在头文件<sstream>中.
3.out_of_range一个异常类。在头文件<stdexcept>中。