用尺度模板库STL实现文件较量
副标题#e#
要阅读本文,你要熟悉C++,熟悉类模板和函数模板。本文搜集了大量有关的信息,指引你慢慢阅读。
本文用提问,设计息争决方案引导阅读。但愿你能喜欢。
问题提出:
有二篇文章都含有很多行文字。我们要成立一个措施来找出二者之间的差异之处并将这些差异内容的行显示出来。措施必需做成可反复利用的组件,就是说,这个组件可以或许未经修改地被其他措施利用。
设计:
假设这二个文件很是之大(每个文件都有数千行),我们这样设计有关办理方案:
将各个文件读进内存块,
在内存块中举办文件内容较量,
将差异之处放进一个新的第三个内存块。
设计方案还要思量到各个文件的元素位置大概差异,亦即沟通的元素不必然在同一行里。这意味着,必需在内存中遍历搜索不沟通的术语并将其存放在第三个内存块中。
思量到措施的可重用性,我们用类属编程技能来设计,让方案可以或许适应于存储介质的变革。
当文件很大时(每个文件有数千行),那么要把每个文件都存储进内存大概是不现实的。别的也给执行进程带来坚苦。
执行细节:
可以用容器来设计,好比用数组或行列,将字符数组存储到容器中。不外这会使得措施的可读性低落并导致组件的可重用性下降。
本文的办理方案用尺度模板库(Standard Template Library, STL)的容器来打点内存块。而且用STL的元素来打点将文件读进内存块。这样的设计方案使得措施具有模板容器级程度的可读性。
为到达互用水准的目标,就要利用C++的类模板和函数模板技能来实现。假如你不熟悉这些模板或要温习一下,可参看文末的链接。
#p#副标题#e#
方案与指南
你写的措施是给二部门人看的:最终用户和措施开拓人员。写给措施员是因为有人大概对你的措施作某些变动。他们必需花时间来领略你的措施。也大概就是你本身在今后的时间里要对措施作出修改 – 改进它的可读性而不低落运行效率,可能增加一系列注释。
举例来说,让我们看一下主函数main():
int main(int argc, char* argv[])
{
// 确认获得正确的参数数量
if(argc!=3)
{
cout << "compareFiles - copyright (c) Essam Ahmed 2000" << endl << endl;
cout << "This program compares the conents of two files and prints" << endl
<< "the differences between the files to the screen" << endl << endl;
cout << "Usage: compareFiles <file_name_1> <file_name_2>" << endl << endl;
return 1;
}
// 声明要利用的容器
typedef vector<string> stringSet;
stringSet s1, s2,s3;
// 将第一篇文章读进荟萃
populate_set_from_file(s1,argv[1]);
cout << "Contents of Set 1" << endl << endl;
for_each(s1.begin(),s1.end(),printElement);
// 将第二篇文章读进荟萃
populate_set_from_file(s2,argv[2]);
cout << endl << "Contents of Set 2" << endl << endl;
for_each(s2.begin(),s2.end(),printElement);
/// 较量荟萃,将差异之处存放到s3
Container_Differences< stringSet,string > (s1,s2,s3);
// 显示功效
cout << endl << "Difference is:" << endl;
for_each(s3.begin(),s3.end(),printElement);
return 0;
}
这里不外多阐述如何读文件和较量文件内容,这些都是封装的事情。这里体贴的是函数饰演的脚色。在本例中,main()函数饰演发报机的脚色,而由其他函数执行真正的事情。
可以看到函数的成果,好比populate_set_from_file()和Container_Differences()函数执行大大都焦点事情。for_each()函数则是STL的运算法则。
main()函数的英华在于:
typedef vector<string> stringSet;
它界说了一个向量的容器范例,用于存储字符串工具。假如不熟悉什么是向量,可参考文末链接有关于向量的指南。字符串集(stringSet)工具是STL数据范例,个中封装了各个字符串。范例界说typedef使它成为可反复利用的数据范例并使得代码可读性很强。
stringSet s1, s2,s3;
声明白3个容器,指向所含的字符串荟萃。前2个包括各个输入的文件内容,后头一个则存放差异的字符串。虽然变量名应该描写得矫正规些。
populate_set_from_file()函数将文件内容读进容器。它是个函数模板,可以利用差异范例的参数。它的组成如下:
#p#分页标题#e#
template<class T>
bool populate_set_from_file(T &s1,const char *file_name)
{
ifstream file_in;
string line_from_file;
file_in.open(file_name);
if(file_in.fail()){
cout << "Error opening file ["
<< file_name << "] - please check file name" << endl;
return false;
}
try{
getline(file_in,line_from_file);
while(file_in.good())
{
addElementToSet(s1,line_from_file);
getline(file_in,line_from_file);
}
}
catch(bad_alloc &e)
{
cout << "Error - Caught Exception: " << e.what() << endl;
throw e;
return false;
}
file_in.close();
return true;
}
这是一个函数模板,它将文件逐行读进它界说的容器范例里。函数打开给定的文件,逐行阅读(回车换行符末了)并插手到容器(容器可以是模板支持的任何范例)中去。用addElementToSet函数将每行文件插手到容器,这也是个函数模板。
用STL的文件流工具(ifstream)来读取文件。ifstream支持根基的文件I/O和堕落处理惩罚。当文件操纵失败时,它的fail()成员函数返回真(true)。文件全部正常读取完毕后,成员函数good()返回真。
getline()是STL函数,读取文件中的每一行字符直至读到行竣事符(行竣事符不读进字符串)。它的参数是源文件流和字符串工具。要留意,它在读取行字符串时不外滤头尾的空格字符。
其它是堕落处理惩罚进程 – 固然不是很抱负的方法,但本例照旧用它。当line_from_file工具中的字符串过长时将抛出bad_alloc堕落信息。
函数的文件名参数file_name是常量(const)参数,暗示该参数为只读,不被修改。利用常量参数让编译器发生一个只读的快速内存映象并使应用措施变得更小些。
addElementToSet也是一个模板函数。容器的利用有时显得很巨大。有些容器用insert()要领来添加成员,另一些容器却用push_back()[译者注:容器的种类许多,行列(list)用前者而仓库(stack)用后者]。更为巨大的是映象(map),它用insert()增加成员,带入的参数却是pair<>。固然可以重载容器的函数,但我选择利用模板。这样可以更为机动,甚至可以用于新的或未知的容器。
addElementToSet函数代码如下:
template<class C,class V >
void addElementToSet(C &c, const V &v)
{
c.insert(v);
}
模板的容器类是C,通报的参数是V(V被声明为常量参数,是只读的。记着,一个V的拷贝被插手到C)。用insert()函数将V插手到C。这对付支持insert()要领的容器是很利便的,但对其他一些容器就有问题了。
对付这样的容器,好比向量(vector)利用push_back()来添加成员,模板要举办特例化处理惩罚。C++模板支持类属理念,但类属执行时仍将优化成某种特定范例。模板的特例化与重载雷同。
下面代码将addElementToSet特例化为向量(vector):
template<> void addElementToSet<vector<string>,string>
(vector<string> &c, const string &v) {
c.push_back(v);
}
留意在"template"要害字的后头是一对空的尖括号,这样声明白一个类属的特例化。可以声明任意多个特例化。
Container_Differences函数模板
在把文件读进容器之后,就要用Container_Differences函数来举办较量。
这也是用模板写成的函数,可以用于其他应用。它挪用addElementToSet函数模板往容器里增加不沟通的字符串。函数固然不利用返回值,但容器的内容一直在产生变革。最后,假如容器里没有成员,意味着较量的文件是沟通的。下面是Container_Differences函数代码:
template<class container_type,class value_type>
void Container_Differences(const container_type &container1,
const container_type &container2,
container_type &result_grp)
{
container_type temp;
container_type::const_iterator iter_pos_grp, iter_found_at;
if(&container1 != &container2)
{
iter_pos_grp=container1.begin();
while(iter_pos_grp!=container1.end())
{
iter_found_at=find(container2.begin(),
container2.end(),
(*iter_pos_grp));
if(iter_found_at==container2.end())
addElementToSet(temp,
static_cast<value_type>((*iter_pos_grp)));
++iter_pos_grp;
}
iter_pos_grp=container2.begin();
while(iter_pos_grp!=container2.end())
{
iter_found_at=find(container1.begin(),
container1.end(),
(*iter_pos_grp));
if(iter_found_at==container1.end())
addElementToSet(temp,
static_cast<value_type>((*iter_pos_grp)));
++iter_pos_grp;
}
}
temp.swap(result_grp);
}
可以看到文件较量进程是相当简朴的,这是设计出发点。函数只作一件事,并且要做好。
#p#分页标题#e#
函数在对每个源文件容器的搜索轮回里重复挪用begin()和end()函数。end()函数在检测到零(null)字符(C字符串的末了)时竣事。用STL的find()函数寻找沟通字符串,假如没有找到,说明存在着差异的字符串,就返回end()并将字符串插手到功效容器。
函数的最后一行用swap()函数将姑且容器的内容拷贝到引用参数的功效容器,并释放姑且容器。
仔细看一下,可以看到迭代器用static_case<>指向值的范例,因为编译器有时无法处理惩罚addElementToSet()所需的数据范例。别的利用static_case<>能使代码看得更清楚些。
函数的参数中,前二者是常量(const)参数,最后一个长短常量参数,用于写入功效。这样可以使措施占用的内存较少。
模板支持的差异容器范例
上面的代码可以支持这些容器范例:
行列(list)
荟萃(set)
向量(vector)
只要在主函数main()里作一次窜改就能等闲地改变利用的容器范例。假如你要将荟萃范例改为向量范例,将:
typedef set<string> stringSet;
改成:
typedef vector<string> stringSet;
就行了。
虽然要从头编译一下(要确保包括文件中有所需要的容器范例)。
还可以对addEmenetToSet()函数模板举办特例化来支持其他范例的容器,好比映象(map)。只要利用的容器支持迭代操纵就能用于这段代码。假如要在你的应用里利用Container_Differences函数,要先对函数addEmenetToSet()作类属特例化处理惩罚。
结语
本文涉及内容许多。最主要的是相识如何利用C++模板来建设STL的类属元素。我们还先容了如何将应用分拆到几个专项函数中去,每个函数只作一件事并作得很好。从而使得整个执行进程显得简朴并易于领略和维护。
这里所做的事情都是为了成立柔性应用系统,充实操作现有元素的利益,淘汰设计、开拓和测试时间。发起阅读其他有关STL,C++模板及C++语言特征的资料,将你的应用措施改变得更有生命力。
代码
文末可供下载(http://www.designs2solutions.com/articles/dev/tcd/d2s_fc.zip)的代码文件合用于VC++ 6.0。内里尚有一个可执行文件可以直接利用。我还包括了二段随机语句的文件供作较量。