作者: winter
简介
本文讨论如何使用标准模板库(STL),类模板和函数模板,以及其它编程技术来解决实际问题。本文涉及到STL包含的集合和向量,函数模板,类模板,常量检验,出错处理和使用STL的文件I/O。
本文用提问,设计和解决方案引导阅读。希望你能喜欢。
问题提出:
有二篇文章都含有许多行文字。我们要建立一个程序来找出二者之间的不同之处并将这些不同内容的行显示出来。程序必须做成可重复使用的组件,就是说,这个组件能够未经修改地被其他程序使用。
设计:
假设这二个文件非常之大(每个文件都有数千行),我们这样设计有关解决方案:
将各个文件读进内存块,
在内存块中进行文件内容比较,
将不同之处放进一个新的第三个内存块。
设计方案还要考虑到各个文件的元素位置可能不同,亦即相同的元素不一定在同一行里。这意味着,必须在内存中遍历搜索不相同的术语并将其存放在第三个内存块中。
考虑到程序的可重用性,我们用类属编程技术来设计,让方案能够适应于存储介质的变化。
当文件很大时(每个文件有数千行),那么要把每个文件都存储进内存可能是不现实的。另外也给执行过程带来困难。
执行细节:
可以用容器来设计,比如用数组或队列,将字符数组存储到容器中。不过这会使得程序的可读性降低并导致组件的可重用性下降。
本文的解决方案用标准模板库(Standard Template Library, STL)的容器来管理内存块。并且用STL的元素来管理将文件读进内存块。这样的设计方案使得程序具有模板容器级水平的可读性。
为达到互用水准的目的,就要使用C++的类模板和函数模板技术来实现。如果你不熟悉这些模板或要复习一下,可参看文末的链接。
方案与指南
你写的程序是给二部分人看的:最终用户和程序开发人员。写给程序员是因为有人可能对你的程序作某些更改。他们必须花时间来理解你的程序。也可能就是你自己在以后的时间里要对程序作出修改 - 改善它的可读性而不降低运行效率,或者增加一系列注释。
举例来说,让我们看一下主函数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()函数将文件内容读进容器。它是个函数模板,可以使用不同类型的参数。它的构成如下: 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); } 可以看到文件比较过程是相当简单的,这是设计出发点。函数只作一件事,而且要做好。 函数在对每个源文件容器的搜索循环里反复调用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++语言特征的资料,将你的应用程序改变得更有生命力。
代码
文末可供下载的代码文件适用于VC++ 6.0。里面还有一个可执行文件可以直接使用。我还包含了二段随机语句的文件供作比较。
点击源码下载
本文英文作者:Erik Westermann 原文:Erik Westermann's Website