使用标准库创建文本查询程序
(1)本程序允许用户在一个给定文件中查询单词,查询结果是单词在文件中出现的次数以及其所在行的列表。如果一个单词在一行中出现多次,此行只列出一次。行会按照升序输出。即,第7行会在第9行之前显示,以此类推。
(2)文本查询程序需要完成如下任务:当程序读取输入文件的时候,它必须记住单词出现的每一行。因此,程序需要逐行读取输入文件,并将每一行分解为独立的单词;等程序生成输出的时候,它必须能提取每个单词所关联的行号,行号必须按照升序出现且无重复,它必须能打印给定行号中的文本。
(3)利用多种标准库设施,我们可以漂亮实现这些需求。我们将使用一个vector<string>来保存整个输入文件的一个拷贝,输入文件中的每行保存为vector中的一个元素,当需要打印一行的时候可以用行号作为下标来提取行文本;我们使用一个istringstream来将每行分解为单词;我们使用一个set来保存每个单词在输入文本中出现的行号,这保证了每行只出现一次且行号按升序来保存;我们使用一个map来将每个单词和它出现的行号set关联起来,这样我们就可以方便提取任意单词的set。(解决方案还使用了shared_ptr,原因稍后进行解释)
(4)虽然我们可以用vector、set和map来直接编写文本查询程序,但如果定义一个更加抽象的解决方案会更加有效。我们将从定义一个保存输入文件的类开始,这会令文件查询更为容易。我们将这个类命名为TextQuery,它包含一个vector和一个map。vector用来保存输入文件的文本,map用来关联每个单词和它出现的行号的set,这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作。
(5)查询操作要完成的任务非常简单:查找map成员,检查给定单词是否出现。设计这个函数的难点就是确定应该返回什么内容。一旦找到了一个单词,我们需要知道它出现了多少次,它出现的行号以及每行的文本。返回这些所有内容的最简单的方法是定义另一个类,可以命名为QueryResult,来保存查询结果,这个类会有一个print函数,完成结果打印工作。
(6)我们的QueryResult类要表达查询的结果,这些结果包括和给定单词关联的行号的set和这些行对应的文本,这些数据都保存外TextQuery类型的对象中。由于QueryResult所需要的数据都保存在一个TextQuery对象中,我们就必须确定如何访问它们。我们可以拷贝行号的set,但这样做可能很耗时。而且,我们当然不希望拷贝vector,因为这可能会引起整个文件的拷贝,而目标只不过是为了打印文件的一小部分而已。
(7)通过返回指向TextQuery对象内部的迭代器(或指针),我们可以避免拷贝操作。但是,这种方法开启了一个陷阱,如果TextQuery对象在对应的QueryResult对象之前被销毁,会发生什么。在这种情况下,QueryResult就将引用一个不再存在的对象中的数据。对于QueryResult对象和对应的TextQuery对象的生存期应该同步这一观察结果,其实已经暗示问题解决方案,考虑到这两个类概念上共享了数据,可以使用shared_ptr反映数据结构中这种共享关系。
(8)当我们设计一个类的时候,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法,通过这种方法,可以看到类是否具有我们所需要的操作。例如,下面的程序使用了TextQuery和QueryResult类,这个函数接受一个指向要处理的文件的ifstream,并和用户交互,打印给定单词的查询结果。
void runQueries(ifstream &infile)
{
TextQuery tq(infile);
//保存文件并建立查询map
//交互,提示用户输入要查询的单词,完成查询并打印结果
while(true)
{
cout << "enter word to look for";
string s;
//若遇到文件尾或用户输入q时循环停止
if(!(cin >> s) || s == "q") break;
//指向查询并打印结果
print(cout, two.query(s)) << endl;
}
}
(9)我们以TextQuery类的定义开始,用户创建这个类的对象时会提供一个istream,用来读取输入文件,这个类还提供一个query操作,接受一个string,返回一个QueryResult表示string出现的那些行。
设计类的数据成员的时候,需要考虑和QueryResult对象共享数据的需求。QueryResult类需要共享保存输入文件的vector和保存单词关联的行号的set。因此,这个类应该有两个数据成员,一个指向动态分配的vector(保存输入文件)的shared_ptr,另一个string到shared_ptr<set>的map。map将文件中每个单词关联到一个动态分配的set上,而此set保存了该单词所出现的行号。
(10)
class QueryResult;
//为了定义函数query返回,这个定义是必须
class TextQuery
{
public:
using line_no=vector<string>::size_type;
TextQuery(ifstream&);
QueryResult query(const string&) const;
private:
shared_ptr<vector<string>> file;
//每个单词到它所在行号的集合映射
map<string, shared_ptr<set<line_no>>> wm;
}
(11)TextQuery构造函数接受ifstream
TextQuery::TextQuery(ifstream &is):file(new vector<string>)
{
string text;
while(getline(is, text))//对文件中每一行
{
file->push_back(text);//保存此行文本
int n=file->size() - 1;//当前行号
ifstringstream line(text);
//将行文本分解为单词
string word;
while(line >> word)//对行中每个单词
{
//如果单词不在wm中,以之为下标在wm添加一项
auto &lines=wm[word];
//lines是一个shared_ptr
if(!lines)//在我们第一次遇到这个单词时,指针为空
lines. reset(new set<line_no>);
//分配一个新的set
lines->insert(n);//将此行号输入set
}
}
}
(12)QueryResult类有三个数据成员,这个string保存查询单词,一个shared_ptr指向保存输入文件的vector,一个shared_ptr指向保存单词出现行号的set。它唯一的一个成员函数是一个构造函数,初始化这三个数据成员如下。
class QueryResult
{
friend ostream& print(ostream&, const QueryResult);
public:
QueryResult(string s, shared_ptr<set<line_no>>p,
shared_ptr<vector<string>> f):
sought(s), lines(p), file(f) { }
private:
string sought;//查询单词
shared_ptr<set<vector<string>> file>;
}
(13)query函数接受一个string参数,即查询单词,query用它来在map中定位对应的行号set,如果找到了这个string,query函数构造一个QueryResult,保存给定string、TextQuery的file成员以及从wm中提取的set
唯一的问题就是,如果给定string没有找到,我们应该返回什么。在这种情况下,没有可返回的set,为了解决这个问题,我们定义了一个局部static对象,它是一个指向空的行号set的shared_ptr。当没有找到给定单词的时候,我们返回此对象的一个拷贝。
QueryResult TextResult::query(const string &sought) const
{
//如果没有找到sought,我们将返回一个指向此set的指针
static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto loc=wn. find(sought);
if(loc==wm.end())
return QueryResult(sought, nodata, file);
else
return QueryResult(sought, loc->second, file);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下