C++ Primer前12章总结 (文本查询程序,从功能分析到实现,一步步实现,请各位朋友指正哈哈哈哈)

终于学完了C++,前十二章基础知识(一些零散的知识点啊啊啊啊啊,不停的写题写题...),接下来就该学习C++程序设计核心部分,类设计者的工具,在此之前,我们可以用一个简单的程序来简单的总结一下。话不多说,开始正题。

文本查询程序

用户功能简述:给定一个文件,输入关键字,然后输出该关键字在文件中出现的个数、行数及该行字符串

<1>功能分析

需要三个个功能:

1.当程序读取文件是,它需要记住单词出现的每一行,因此逐行读取文件。

2.它把必须能提取每个单词所关联的行号,及该行的字符串,且行号升序且无重复 

3.将找出来行数、字符串、以及次数输出到控制台。

 <2>功能实现方案

1 2部分.实现:读取后需要一个数据结构去储存,考虑到需要逐行读取,且后期需要提取行号。因此这个结构最好是vector,它能够将行号和每一行关联在一起。

接下来找出单词所在行,这个实现有两种方案

(1)提前扫描文本所有的单词,并储存其行数,需要关联string和set类型,那无疑是map最合适,使用set的原因是考虑到一个string可能有多个行号,且可能出现一个单词在一行出现两次。用户输入单词时,我们直接在map中find即可

(2)在用户输入单词时,再在我们一二部分实现部分定义的vector中提取信息。

方案一,需求更大的空间,而方案二在运行时需求更多的时间。

我们先看看方案一

功能实现对于本方案而言,无疑包含两部分工作

一是读取文件,并作准备工作包括:读取文件位置,读取文件内容到vector,将文件内所有单词关联到行储存到map中。

二是读取用户输入单词,并作处理工作包括:在map中提取行号,打印出行号和对应的行。

那么,考虑到这些工作都是围绕数据进行,按照C的编程方式,我们会使用分文件的编写方案。但在数据共享和数据安全性方面,显然有更高效和理想的方式。我们将编写两个类来协助工作。

一个类是TextQuery,负责读取。另一个类是TextResult,负责打印结果。

在设计两个类的时候,大多数初学朋友都会,先行设计这两个类。往往更耗时间,我们不如,先看看这样一个文本查询程序需要怎样的操作,然后根据这样的操作,去一一填补。下面我们先进行进行设计程序流程图。

C++ Primer前12章总结 (文本查询程序,从功能分析到实现,一步步实现,请各位朋友指正哈哈哈哈)_第1张图片

这么,简单???对啊!就是这么简单,接下来,看我们初步建立的类和文件。

那么我们该先写那一部分勒,我这里打算先写TextResult,并行、相关性不大的类设计时,哪一个开始都好。而本例明显有因果关系。程序设计,我个人比较喜欢执果求因。

首先我需要这样的结果

给程序输入文本的地址(本例是C:/Users/me_wa/Desktop/TextTest.txt)

然后出现文字enter word to look for, or q to quit:

用户输入单词后,出现如上结果。

然后考虑怎样出现这样的结果。我们先编写TextResult,前面说了它是负责出结果的。对应如上结果,我需要的数据就有了。分别是输入的单词、前面功能实现方案提到保存文件的 vector或它的指针、map中单词对应的set或者set的指针。

因为TextQuery负责读取,它已经为vector和set分配了相应的内存,我们为了节省内存,就使用指针,指向TextQuery给vector和set分配的内存。再者拷贝实体也会花费时间,如果文件比较大,TextQuery读取文件分配了内存,TextResult为了获取三个数据就再分配内存然后拷贝,显然耗费空间时间。

当然为了防止TextQuery将内存意外释放,而我们TextResult访问空地址的情况出现。我们这里使用智能指针。

最后考虑一点,我们的结果TextResult数据结构已经搭建完成,那么我们如何从哪获取呢?

这里我们考虑通过TextResult的构造函数传进去,也就是说,当TextQuery读取完数据后,构造时TextResult把这三个参数传进来。

当然print函数可以作为TextResult的成员出现的,也可以定义为TextResult的友元函数也可以打印出相应结果。但是print函数和TextResult的功能性契合度不高,它并不承担着改变TextResult数据的任务,也不是TextResult的功能性必须函数。将TextResult对象当作参数传入print函数函数,打印出其数据成员即可,这样的灵活性也也更高。

因此我们的TextResult和print这样写

"TextResult.h"

#ifndef _TEXTRESULT_H__
#define _TEXTRESULT_H__
#include 
#include 
#include 
#include 
#include 
class TextResult
{
using line_no = std::vector::size_type;
friend std::ostream& print(std::ostream& , TextResult&);
public:
	TextResult(std::string w,
		std::shared_ptr> f,
		std::shared_ptr> l) :
		word(w), files(f), lines(l){};
	~TextResult() {};

private:
	std::string word;//查找的单词
	std::shared_ptr> files; //指向vector的智能指针
	std::shared_ptr> lines; //指向map中对应pair第二个元素:set的智能指针
};

std::ostream& print(std::ostream& os, TextResult&qr);
#endif _TEXTRESULT_H__

TextResult.c

#include "TextQuery.h"

std::ostream& print(std::ostream& os, TextResult& qr)
{
	//输出内容xx单词出现x次
	os << qr.word << " occurs " << qr.lines->size()
		<< ((qr.lines->size()) > 1 ? " times." : " time.") << std::endl;
        //利用set村存的行号从vector中提取这一行的字符串
	for (auto& num : *(qr.lines))
	{
		os << "\t(line " << num + 1 << ")  "
			<< (*(qr.files))[num] << std::endl;
	}
	return os;
}

好了,我们的数据已经能输出了,现在就等TextQuery读入数据然后,构造TextResult输出了。工作已经完成了一半了,你看朕的半壁江山已经为你打下!!!哈!哈!哈!

 

TextQuery前面功能实现方案说了,我们需要两个数据结构一个是vector,另一个是map。当然为了数据内存共享,使用new分配内存。那么现在考虑如何获取文件数据到vector和map。既然最初给我们的是一个文件位置,那么我们就使用ifstream好了。当然最方便的方法就是在TextQuery构造之时,将绑定该文件的ifstream作为参数传进来,然后再构造函数中将文件数据读到vector和map。我们没必要另起一个成员函数去初始化数据,再者构造函数的意义就是初始化数据。

好了,最后一个问题,数据如何传给TextResult,当然我们可以在用户代码中构造TextResult,然后将TextQuery的成员作为参数传进去,当然在这之前需要声明友元。

但是,这样很麻烦,而TextResult和TextQuery关联性比较大,我们完全可以将这个过程定义为TextQuery的成员函数。然后return TextResult对象给print函数。一气呵成。

好了我们这样写

TextQuery.h

#ifndef _TEXTQUERY_H__
#define _TEXTQUERY_H__
#include "TextResult.h"
#include 
#include 
#include 
#include 
class TextResult;
class TextQuery
{
using line_no = std::vector::size_type;
public:
	TextQuery(std::ifstream&);//构造函数,输入绑定文件的文件流
	~TextQuery() {}; //由于类对象被销毁时智能指针也会被销毁,
	                 //因此智能指针指向的内存,会被程序自行释放,这里使用默认析构函数即可
	TextResult query(const std::string&) const;
private:
	std::shared_ptr> files; //指向vector的智能指针
	std::map < std::string,
		       std::shared_ptr>> wm;//指向set的智能指针

};

#endif _TEXTQUERY_H__

 

TextQuery.c

#include "TextQuery.h"
#include 
using namespace std;
TextQuery::TextQuery(std::ifstream& is):
	files(new std::vector)
{
	string s;
	while (getline(is,s))
	{
		//读取行到vector
		files->push_back(s);
		//记录当前行数
		auto n = files->size() - 1;
		//读取行信息到map
		istringstream line(s);
		string word;
		while (line >> word)
		{
			//添加word关键字的pair到map
			auto& linesSet = wm[word];
			//只有当前单词为新单词,才会new出set
			if (!linesSet)
				linesSet.reset(new set);
			//给set插入行号
			linesSet->insert(n);
		}	
	}
}

TextResult TextQuery::query(const std::string& sought) const // 这里const是防止有人修改类成员,隐含传入的this指针式是const的
{
	//如果找不到单词,就返回这个静态的空set。
	static shared_ptr> nodate(new set);
	auto loc = wm.find(sought);
	if(loc == wm.end())
		return TextResult(sought, files, nodate);
	return TextResult(sought, files, loc->second);
}

以上,我们完成了一套服务,最后就是完成用户代码,来使用这两个类,还记得我们的用户代码流程嘛,不记得向上翻翻。

#include 
#include "TextResult.h"
#include "TextQuery.h"

using namespace std;
class TextQuery;

void runQueries(ifstream& infile)
{
	TextQuery tq(infile);
	while (true)
	{
		cout << "enter word to look for, or q to quit: ";
		string s;
		if (!(cin >> s) || s == "q") break;
		print(cout, tq.query(s));
	}
}
int main(int argc, char**argv)
{
	if (argv[1] == NULL)
	{
		return 1;
	}	
	ifstream file(argv[1]);//"C:/Users/me_wa/Desktop/TextTest.txt"
	runQueries(file);
	system("pause");
	return 0;
}

以上结束。。。

哦,对了,这个参数需要从main输入,vs是无法完成的了。。。

步骤是

1、打开cmd

2、在你工程里找到exe文件然后拖到cmd界面(你会发现是一串地址)

3、在2步骤产生的字符串之后,输入你所要查询文件的位置。

4、回车

接下来就可以运行啦哈哈哈。

 

以上我可能忽略了一些东西,如果有问题,评论区提问我啊哈哈哈

 

 

 

 

 

 

 

你可能感兴趣的:(C++)