来源:天极网
2 牛刀小试:且看一个简单例程
大致功能是:从标准输入设备(一般是键盘)读入一些整型数据,然后对它们进行排序,最终将结果输出到标准输出设备(一般是显示器屏幕)。这是一种典型的处理方式,程序本身具备了一个系统所应该具有的几乎所有的基本特征:输入 + 处理 + 输出。你将会看到三个不同版本的程序。第一个是没有使用STL的普通C++程序,你将会看到完成这样看似简单的事情,需要花多大的力气,而且还未必没有一点问题(真是吃力不讨好)。第二个程序的主体部分使用了STL特性,此时在第一个程序中所遇到的问题就基本可以解决了。同时,你会发现采用了STL之后,程序变得简洁明快,清晰易读。第三个程序则将STL的功能发挥到了及至,你可以看到程序里几乎每一行代码都是和STL相关的。
2.2.1 第一版:史前时代--转木取火
在STL还没有降生的"黑暗时代",C++程序员要完成前面所提到的那些功能,需要做很多事情(不过这比起C程序来,似乎好一点),程序大致是如下这个样子的:
// name:example2_1.cpp // alias:Rubish #include <stdlib.h> #include <iostream.h> int compare(const void *arg1, const void *arg2); void main(void) { const int max_size = 10; // 数组允许元素的最大个数 int num[max_size]; // 整型数组 // 从标准输入设备读入整数,同时累计输入个数, // 直到输入的是非整型数据为止 int n; for (n = 0; cin >> num[n]; n ++); // C标准库中的快速排序(quick-sort)函数 qsort(num, n, sizeof(int), compare); // 将排序结果输出到标准输出设备 for (int i = 0; i < n; i ++) cout << num[i] << "/n"; } // 比较两个数的大小, // 如果*(int *)arg1比*(int *)arg2小,则返回-1 // 如果*(int *)arg1比*(int *)arg2大,则返回1 // 如果*(int *)arg1等于*(int *)arg2,则返回0 int compare(const void *arg1, const void *arg2) { return (*(int *)arg1 < *(int *)arg2) ? -1 : (*(int *)arg1 > *(int *)arg2) ? 1 : 0; }
这是一个和STL没有丝毫关系的传统风格的C++程序。因为程序的注释已经很详尽了,所以不需要我再做更多的解释。总的说来,这个程序看起来并不十分复杂。只是,那个compare函数,看起来有点费劲。指向它的函数指针被作为最后一个实参传入qsort函数,qsort是C程序库stdlib.h中的一个函数。以下是qsort的函数原型:
void qsort(void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) ); 看起来有点令人作呕,尤其是最后一个参数。大概的意思是,第一个参数指明了要排序的数组(比如:程序中的num),第二个参数给出了数组的大小(qsort没有足够的智力预知你传给它的数组的实际大小),第三个参数给出了数组中每个元素以字节为单位的大小。以下是某次运行的结果:
输入:0 9 2 1 5
输出:0 1 2 5 9有一个问题,这个程序并不像看起来那么健壮(Robust)。如果我们输入的数字个数超过max_size所规定的上限,就会出现数组越界问题。如果你在Visual C++的IDE环境下以控制台方式运行这个程序时,会弹出非法内存访问的错误对话框。这个问题很严重,严重到足以使你开始重新审视这个程序的代码。为了弥补程序中的这一缺陷。我们不得不考虑采用如下三种方案中的一种:
- 采用大容量的静态数组分配。
- 限定输入的数据个数。
- 采用动态内存分配。
第一种方案比较简单,你所做的只是将max_size改大一点,比如:1000或者10000。但是,严格讲这并不能最终解决问题,隐患仍然存在。假如有人足够耐心,还是可以使你的这个经过纠正后的程序崩溃的。此外,分配一个大数组,通常是在浪费空间,因为大多数情况下,数组中的一部分空间并没有被利用。
再来看看第二种方案,通过在第一个for循环中加入一个限定条件,可以使问题得到解决。比如:for (int n = 0; cin >> num[n] && n < max_size; n ++); 但是这个方案同样不甚理想,尽管不会使程序崩溃,但失去了灵活性,你无法输入更多的数。
看来只有选择第三种方案了。是的可以利用指针,以及动态内存分配妥善的解决上述问题,并且使程序具有良好的灵活性。这需要用到new,delete操作符,或者古老的malloc(), realloc()和free()函数。但是为此,你将牺牲程序的简洁性,使程序代码陡增,代码的处理逻辑也不再像原先看起来那么清晰了。一个 compare函数或许就已经令你不耐烦了,更何况要实现这些复杂的处理机制呢?很难保证你不会在处理这个问题的时候出错,很多程序的bug往往就是这样产生的。同时,你还应该感谢stdlib.h,它为你提供了qsort函数,否则,你还需要自己实现排序算法。如果你用的是冒泡法排序,那效率就不会很理想。……,问题真是越来越让人头疼了!
2.2.2 第二版:工业时代--组件化大生产
试着使用一下STL,看看效果如何。
// name:example2_2.cpp // alias:The first STL program #include <iostream> #include <vector> #include <algorithm> using namespace std; void main(void) { vector<int> num; // STL中的vector容器 int element; // 从标准输入设备读入整数, // 直到输入的是非整型数据为止 while (cin >> element) num.push_back(element); // STL中的排序算法 sort(num.begin(), num.end()); // 将排序结果输出到标准输出设备 for (int i = 0; i < num.size(); i ++) cout << num[i] << "/n"; }
这个程序的主要部分改用了STL的部件,看起来要比第一个程序简洁一点,你已经找不到那个讨厌的compare函数了。这个程序是足够健壮的。
程序的前三行是包含的头文件,它们提供了程序所要用到的所有C++特性(包括输入输出处理,STL中的容器和算法)。不必在意那个.h,并不是我的疏忽,程序保证可以编译通过,只要你的C++编译器支持标准C++规范的相关部分。
第四行加入那个声明只是为了表明程序引用到了std这个标准名字空间(namespace),因为STL中的那些玩意儿全都包含在那里面。只有通过这行声明,编译器才能允许你使用那些有趣的特性。
程序中用到了vector,它是STL中的一个标准容器,可以用来存放一些元素。你可以把vector理解为int [?],一个整型的数组。之所以大小未知是因为,vector是一个可以动态调整大小的容器,当容器已满时,如果再放入元素则vector会悄悄扩大自己的容量。push_back是vector容器的一个类属成员函数,用来在容器尾端插入一个元素。main函数中第一个while循环做的事情就是不断向 vector容器尾端插入整型数据,同时自动维护容器空间的大小。
sort是STL中的标准算法,用来对容器中的元素进行排序。它需要两个参数用来决定容器中哪个范围内的元素可以用来排序。这里用到了vector的另两个类属成员函数。begin()用以指向vector的首端,而end()则指向vector的末端。这里有两个问题,begin()和end()的返回值是什么?这涉及到STL的另一个重要部件--迭代器(Iterator),不过这里并不需要对它做详细了解。你只需要把它当作是一个指针就可以了,一个指向整型数据的指针。相应的sort函数声明也可以看作是void sort(int* first, int* last),尽管这实际上很不精确。另一个问题是和end()函数有关,尽管前面说它的返回值指向vector的末端,但这种说法不能算正确。事实上,它的返回值所指向的是vector中最末端元素的后面一个位置,即所谓pass-the-end value。这听起来有点费解,不过不必在意,这里只是稍带一提。总的来说,sort函数所做的事情是对那个准整型数组中的元素进行排序,一如第一个程序中的那个qsort,不过比起qsort来,sort似乎要简单了许多。
程序的最后是输出部分,在这里vector完全可以以假乱真了,它所提供的对元素的访问方式简直和普通的C++内建数组一模一样。那个size函数用来返回vector中的元素个数,就相当于第一个程序中的变量n。这两行代码直观的不用我再多解释了。
2.2.3 第三版:唯美主义的杰作
事态的发展有时候总会趋向极端,这在那些唯美主义者当中犹是如此。首先声明,我并不是一个唯美主义者,提供第二版程序的改进版,完全是为了让你更深刻的感受到STL的魅力所在。在看完第三版之后,你会强烈感受到这一点。或许你也会变成一个唯美主义者了,至少在STL方面。这应该不是我的错,因为决定权在你手里。下面我们来看看这个绝版的C++程序。
// name:example2_3.cpp // alias:aesthetic version #include <iostream> #include <vector> #include <algorithm> #include <iterator> using namespace std; void main(void) { typedef vector<int> int_vector; typedef istream_iterator<int> istream_itr; typedef ostream_iterator<int> ostream_itr; typedef back_insert_iterator< int_vector > back_ins_itr; // STL中的vector容器 int_vector num; // 从标准输入设备读入整数, // 直到输入的是非整型数据为止 copy(istream_itr(cin), istream_itr(), back_ins_itr(num)); // STL中的排序算法 sort(num.begin(), num.end()); // 将排序结果输出到标准输出设备 copy(num.begin(), num.end(), ostream_itr(cout, "/n")); }
2.3 如何运行
选用了目前在Windows平台下较为常见的Microsoft Visual C++ 6.0和Borland C++ Builder 6.0作为例子。
尽管Visual C++ 6.0对最新的ANSI/ISO C++标准支持的并不是很好。不过据称Visual C++ .NET(也就是VC7.0)
在这方面的性能有所改善。
你可以选用多种方式运行前面的程序,比如在Visual C++下,你可以直接在DOS命令行状态下编译运行,也可以在VC的
IDE下采用控制台应用程序(Console Application)的方式运行。对于C++ Builder,情况也类似。
对于Visual C++而言,如果是在DOS命令行状态下,你首先需要找到它的编译器。假定你的Visual C++装在
C:/Program Files/Microsoft Visual Studio/VC98下面,则其编译器所在路径应该是
C:/Program Files/Microsoft Visual Studio/VC98/Bin,
在那里你可以找到cl.exe文件。编译时请加上/GX和/MT参数。如果一切正常,结果就会产生一个可执行文件。如下所示:
cl /GX /MT example2_2.cpp
前一个参数用于告知编译器允许异常处理(Exception Handling)。在P. J. Plauger STL中的很多地方使用了
异常处理机制(即try…throw…catch语法),所以应该加上这个参数,否则会有如下警告信息:
warning C4530: C++ exception handler used, but unwind semantics are not enabled.
后一个参数则用于使程序支持多线程,它需要在链接时使用LIBCMT.LIB库文件。不过P. J. Plauger STL
并不是线程安全的(thread safety)。如果你是在VC环境下使用像STLport这样的STL实现版本,
则需要加上这个参数,因为STLport是线程安全的。
如果在IDE环境下,可以在新建工程的时候选择控制台应用程序.
至于那些参数的设置,则可以通过在Project功能菜单项中的Settings功能【Alt+F7】中设置编译选项来完成。
有时,在IDE环境下编译STL程序时,可能会出现如下警告信息(前面那几个示例程序不会出现这种情况):
warning C4786: '……' : identifier was truncated to '255' characters in the debug information
这是因为编译器在Debug状态下编译时,把程序中所出现的标识符长度限制在了255个字符范围内。如果超过最大长度,这些标识
符就无法在调试阶段查看和计算了。而在STL程序中大量的用到了模板函数和模板类,编译器在实例化这些内容时,展开之后所产
生的标识符往往很长(没准会有一千多个字符!)。如果你想认识一下这个warning的话,很简单,在程序里加上如下一行代码:
vector<string> string_array; // 类似于字符串数组变量
对于这样的warning,当然可以置之不理,不过也是有解决办法的。 你可以在文件开头加入下面这一行:
#pragma warning(disable: 4786)。
它强制编译器忽略这个警告信息,这种做法虽然有点粗鲁,但是很有效。