CSDN博客

img taodm

More Effective C++ Item M35:让自己习惯使用标准C++语言

发表于2002/3/31 15:44:00  1777人阅读

Item M35:让自己习惯使用标准C++语言
自1990年出版以来,《The Annotated C++ Reference Manual 》(见原书P285,附录:推荐读物)是最权威的参考手册供程序员们判断什么是C++拥有的而什么不是。在它出版后的这些年来,C++的ISO/ANSI标准已经发生了大大小小的变化了(主要是扩充)。作为权威手册,它已经不适用了。
在《ARM》之后C++发生的变化深远地影响了写出的程序的优良程度。因此,对程序员们来说,熟悉C++标准申明的与《ARM》上描述的关键性不同是非常重要的。
ISO/ANSI C++标准是厂商实现编译器时将要考虑的,是作者们准备出书时将要分析的,是程序员们在对C++发生疑问时用来寻找权威答案的。在《ARM》之后发生的最主要变化是以下内容:
* 增加了新的特性:RTTI、命名空间、bool,关键字mutable和explicit,对枚举的重载操作,已及在类的定义中初始化const static成员变量。
* 模板被扩展了:现在允许了成员模板,增加了强迫模板实例化的语法,模板函数允许无类型参数,模板类可以将它们自己作为模板参数。
* 异常处理被细化了:异常规格申明在编译期被进行更严格的检查,unexpected()函数现在可以抛一个bad_exception对象了。
* 内存分配函数被改良了:增加了operator new[]和operator delete[]函数,operator new/new[]在内存分配失败时将抛出一个异常,并有一个返回为0(不抛异常)的版本供选择。(见Effective C++ Item 7)
* 增加了新的类型转换形式:static_cast、dynamic_cast、const_cast,和reinterpret_cast。
* 语言规则进行了重定义:重定义一个虚函数时,其返回值不需要完全的匹配了(如果原来返回基类对象或指针或引用,派生类可以返回派生类的对象、指针或引用),临时对象的生存期进行了精确地定义。
绝大部分变化都描述于《The Design and Evolution of C++ 》(见原书P285,附录:推荐读物)。现在的C++教科书(那些写于1994年以后的)应该也都包含了它们。(如果发现哪本没有,那么扔掉它。)另外,本书包含了一些例子来展示任何使用这些新特性。欲知祥情,请看索引表。
C++的这些变化在标准运行库的变化面前将黯然失色。此外,标准运行库的演变从没象语言本身这样被宣扬过。例如,《The Design and Evolution of C++》几乎没有提及标准运行库。讨论运行库的书籍都有些过时,因为运行库在1994年后发生了非常巨大的变化。
标准运行库的功能分为下列类别(参见Effective C++ Item 49):
* 支持标准C运行库。不用担心,C++仍然记得它的根源。进行了一些细微的调整,使得C++版本的C运行库顺应了C++更严格的类型检查,但其功能和效果,在C运行库是怎么样的,在C++中还是怎么样。
* 支持string类型。标准C++运行库实现组的领衔人物Mike Vilot被告知“如果没有一个标准的string类型,那么将血流于街!”(有些人是如此激动。)平静一下,把板斧和棍子放下--标准C++运行库有了string类型。
* 支持本地化。不同的文化有不同的字符集,以及在显示日期和时间、排序字符串、输出货币值……等等时有不同的习惯。标准运行库对本地化的支持便于开发同时供不同文化地区使用的程序。
* 支持I/O操作。流运行库仍然有部分保留在C++标准中,但标准委员会作了小量的修补。虽然部分类被去除了(特别是iostram和fstram),部分类被替换了(例如,以string为基础的stringstream类替换了以char*为基础的strstream类,现在已不提倡使用strstream类了),但标准流类的基本功能含概了以前的实现的基本功能。
* 支持数学运算。复数,C++教课书必谈的东西,最终纳入了标准运行库。另外,运行库包含了特别的数组类(valarray)以免混淆。这些数组类比内建类型的数组有更好的性能,尤其在多CPU系统下。运行库也提供了部分常用的运算函数如加法和减法。
* 支持通用容器和运算。标准C++运行库带了一组模板类和模板函数,称为标准模板库(STL)。STL是标准C++运行库中最革命性的部分。我将在下面概述它的特性。
在介绍STL前,必须先知道标准C++运行库的两个特性。
第一,在运行库中的几乎任何东西都是模板。在本书中,我谈到过运行库中的string类,实际上没有这样的类。其实,有一个模板类叫basic_string来描述字符序列,它接受一个字符类型的参数来构造此序列,这使得它能表示char串、wide char串、Unicode char串等等。
我们通常认为的string类是从basic_string<char>实例化而成的。用于它被用得如此广泛,标准运行库作了一个类型定义:
typedef basic_string<char> string;
这其实仍然隐藏了很多细节,因为basic_string模板带三个参数;除了第一个外都有默认参数。要全面理解string类型,必须面对这个未经删节的basic_string:
template<class charT,
         class traits = string_char_traits<charT>,
         class Allocator = allocator>
  class basic_string;
使用string类型并不需要了解这个官方文章,因为类型定义使得它表现得如同不是模板类。无论你需要定制存入string的字符集,或调整这些字符的行为,或控制分配给string的内存,这个basic_string模板都能让你完成这些事。
设计string类型时采用的逼近法--归纳其行为然后推广为模板--被标准C++运行库广泛采用。IOstream?它们是模板;一个类型参数决定了这个流的特征。复数?也是模板;一个类型参数决定了数字被怎么样存储。Valarray?模板;一个类型参数表明了数组里面存了什么。如果你不熟悉模板,现在正是个熟悉的好机会。
另外需要知道的是:标准运行库将几乎所有内容都包含在命名空间std中。要想使用标准运行库里面的东西而无需特别指明运行库的名称,你可以使用using指示或使用(更方便的)using申明(参见Effective C++ Item 28)。幸运的是,这种重复工作在你#include恰当的头文件时自动进行。
* 标准模板库
标准C++运行库中最大的新闻就是STL,标准模板库。(因为C++运行库中的几乎所有东西都是模板,这个STL就不怎么贴切了。不过,运行库中包容器和算法的部分,其名字不管好还是坏,反正现在就叫了这个。)
STL很可能影响很多--恐怕是绝大多数--C++运行库的结构,所以熟悉它的基本原理是很重要的。它们并不难理解。STL基于三个基本概念:包容器(container)、选择子(iterator)和算法(algorithms)。包容器是被包容对象的封装;选择子是类指针的对象让你能如同使用指针操作内建类型的数组一样操作STL的包容器;算法是对包容器进行处理的函数,并使用选择子来实现的。
联想一下C++(和C)中数组的规则,就太容易理解STL的想法了。真的只需要明确一点:一个指向数组的指针可以正确地指出数组的任意元素或刚刚超出数组范围的那个元素。如果指向了那个超范围的元素,它将只能与其它指向此数组的指针进行地址比较;对其进行反引用,其结果为未定义。
我们可以利用这条规则来实现在数组中查找一个特定值的函数。对一个整型数组,函数可能是这样的:
int * find(int *begin, int *end, int value)
{
  while (begin != end && *begin != value) ++begin;
  return begin;
}
这个函数在begin与end之间(不包括end--end指向刚刚超出数组范围的元素)查找value,返回第一个值为value的元素;如果没有找到,它返回end。
返回end来表示没找到,看起来有些可笑。返回0(NULL指针)不是更好吗?确实NULL看起来更自然,但并不意味着更“好”。find()函数必须返回特别的指针值来表明查找失败,就此目的而言,end指针与NULL指针效果相同。但,如我们将要看到的,end指针在推广到其它包容器类型时比NULL指针好。
老实说,这可能不是你实现find()函数的方法,但它不是不切实际的,它的推广性很好。看完下面的例子,你将掌握STL的思想。
你可以这么使用find()函数:
int values[50];
...
int *firstFive = find(values,        // search the range
                      values+50,     // values[0] - values[49]
                      5);            // for the value 5
if (firstFive != values+50) {        // did the search succeed?
  ...                                // yes
}
else {
  ...                                // no, the search failed
}
你也可以只搜索数组的一部分:
int *firstFive = find(values,        // search the range
                      values+10,     // values[0] - values[9]
                      5);            // for the value 5
int age = 36;
...
int *firstValue = find(values+10,    // search the range
                       values+20,    // values[10] - values[19]
                       age);         // for the value in age
find()函数内部并没有限制它只能对int型数组操作,所以它可以实际上是一个模板:
template<class T>
T * find(T *begin, T *end, const T& value)
{
  while (begin != end && *begin != value) ++begin;
  return begin;
}
在转为模板时,注意我们由“传值”改为了“传const型引用”。因为现在可以处理任意类型了,我们不得不考虑传值的代价了。在每次调用过程中,每个传值的参数都要有构造函数和析构函数的开销。通过传引用(它不需要构造和析构任何对象),我们避免了这个开销(参见Effective C++ Item 22)。
这个模板很漂亮,但还可以推广得更远些。注意在begin和end上的操作。只有“不等于”比较、反引用、前缀自增(参见More Effective C++ Item M6)、拷贝(对函数返回值进行的,参见More Effective C++ Item M19)。这就是我们涉及到的全部操作,所以为什么限制find()只能使用指针呢?为什么不允许其它支持这些操作的对象呢?这样一来就可以将find()从内嵌类型的指针中解放出来。例如,我们可以为一个链表定义一个类指针对象,其前缀自增操作将使自己指向链表的下一个元素。
这就是STL的选择子的概念。选择子就是被设计为操作STL的包容器的类指针对象。它们是Item M28中讲的灵巧指针的堂兄,只是灵巧指针的功能更强大。但从技术角度看,它们的实现使用了同样的技巧。
有了作为类指针对象的选择子的概念,我们可以用选择子代替find()中的指针。改写后的find()类似于:
template<class Iterator, class T>
Iterator find(Iterator begin, Iterator end, const T& value)
{
  while (begin != end && *begin != value) ++begin;
  return begin;
}
恭喜!你恰巧写出了标准模板库的一部分。STL中包含了很多使用包容器和选择子的算法,find()是其中之一。
STL中的包容器有bitset、vector、list、deque、queue、priority-queue、stack、set和map,你可以在其中任一类型上使用find(),例如:
list<char> charList;                  // create STL list object
                                      // for holding chars
...
// find the first occurrence of 'x' in charList
list<char>::iterator it = find(charList.begin(),
                               charList.end(),
                               'x');
 “嗨!”,我听到你大叫,“这和前面使用数组的例子看起来一点都不象!”哈,但它是一样的;你只是必须知道find()期望什么输入。
要对list对象调用find(),你必须提供一个指向list中的第一个元素的选择子和一个越过list中最后一个元素的选择子。如果list类不提供帮助,这将有些难,因为你无法知道list是怎么实现的。幸好,list(和其它所有STL的包容器一样)被责令提供成员函数begin()和end()。这些成员函数返回你所需要的选择子,供你传给find()用。
当find()执行完成时,它返回一个选择子对象指向找到的元素(如果元素有的话)或charList.end() (如果没有的话)。因为你不知道list是怎么实现的,所以也无法知道list内部的选择子是怎么实现的。于是,你怎么知道find()返回什么类型的对象?如其它所有STL的包容器一样,list再次提供了帮助:它提供了一个类型重定义,iterator就是list内部使用的选择子的类型。既然charList是一个包容char的list,它内部的选择子类型就是list<char>::iterator(也就是上面的例子中使用的)。(每个STL包容器类实际上定义了两个选择子类型,iterator和const_iterator。前一个象普通指针,后一个象指向const对象的指针。)
同样的方法也完全适用于其它STL包容器。此外,C++指针也是STL选择子,所以,最初的数组的例子也能适用STL的find()函数:
int values[50];
...
int *firstFive =   find(values, values+50, 5);     // fine, calls
                                                   // STL find
STL其实非常简单。它只是收集了遵从同样规则的类模板和函数模板。STL的类提供如同begin()和end()这样的函数,这些函数返回类型定义在此类内的选择子对象。STL的算法函数使用选择子对象操作这些类。STL的选择子类似于指针。这就是STL的全部。它没有庞大的继承层次,也没有虚函数。只是影响类模板和函数模板,及它们所遵守的规则。
这导致了另外一个发现:STL是可扩充的。你可以在STL家族中加入自己的包容器,算法和选择子。总要你遵守STL的规则,STL中的包容器将可以使用你的算法,你的包容器上也可使用STL的算法。当然,你的模板不会成为标准C++运行库的一部分,但它们将用同样的规则来编译,并可被重用。
C++运行库中还有更多我没有写出来的东西。在你能高效使用运行库前,你必须跳出我在这儿画的框去学更多的东西;在你能写自己的STL兼容模板前,你必须掌握更多的STL规则。标准C++运行库比C运行库丰富得太多了,花时间来熟悉它是值得的(参见Item E49)。此外,掌握设计运行库的原则(通用、可扩充、可定制、高效、可复用)也是值得的。通过学习标准C++运行库,你不光涨了知识知道有什么现成的可用在自己的软件中,你也将学到如何更高效地使用C++的特性,也能掌握如何设计更好的代码库。
0 0

相关博文

我的热门文章

img
取 消
img