条款50: 提高对C++的认识
C++中有很多 "东西":C,重载,面向对象,模板,例外,名字空间。这么多东西,有时让人感到不知所措。怎么弄懂所有这些东西呢?
C++之所以发展到现在这个样子,在于它有自己的设计目标。理解了这些设计目标,就不难弄懂所有这些东西了。C++最首要的目标在于:
· 和C的兼容性。很多很多C还存在,很多很多C程序员还存在。C++利用了这一基础,并建立在 ---- 我是指 "平衡在" ---- 这一基础之上。
· 效率。作为C++的设计者和第一个实现者,Bjarne Stroustrup从一开始就清楚地知道,要想把C程序员争取过来,就要避免转换语言会带来性能上的损失,否则他们不会对C++再看第二眼。结果,他确信C++在效率上可以和C匹敌 ---- 二者相差大约在5%之内。
· 和传统开发工具及环境的兼容性。各色不同的开发环境到处都是,编译器、链接器和编辑器则无处不在。从小型到大型的所有开发环境,C++都要轻松应对,所以带的包袱越轻越好。想移植C++?你实际上移植的只是一种语言,并利用了目标平台上现有的工具。(然而,往往也可能带来更好的实现,例如,如果链接器能被修改,使得它可以处理内联和模板在某些方面更高的要求)
· 解决真实问题的可应用性。C++没有被设计为一种完美的,纯粹的语言,不适于用它来教学生如何编程。它是设计为专业程序员的强大工具,用它来解决各种领域中的真实问题。真实世界都有些磕磕碰碰,因此,程序员们所依赖的工具如果偶尔出点问题,也不值得大惊小怪。
以上目标阐明了C++语言中大量的实现细节,如果没有它们作指导,就会有摩擦和困惑。为什么隐式生成的拷贝构造函数和赋值运算符要象现在这样工作呢,尤其是指针(参见条款11和45)?因为这是C对struct进行拷贝和赋值的方式,和C兼容很重要。为什么析构函数不自动被声明为virtual(参见条款14),为什么实现细节必须出现在类的定义中(参见条款34)呢?因为不这样做就会带来性能上的损失,效率很重要。为什么C++不能检测非局部静态对象之间的初始化依赖关系(参见条款47)呢?因为C++支持单独编译(即,分开编译源模块,然后将多个目标文件链接起来,形成可执行程序),依赖现有的链接器,不和程序数据库打交道。所以,C++编译器几乎不可能知道整个程序的一切情况。最后一点,为什么C++不让程序员从一些繁杂事务如内存管理(参见条款5-10)和低级指针操作中解脱出来呢?因为一些程序员需要这些处理能力,一个真正的程序员的需要至关重要。
关于C++身后的设计目标如何影响语言行为的形成,以上介绍远远不够。要想覆盖所有的内容,将需要一整本书;方便的是,Stroustrup写了一本。这本书是 "The Design and Evolution of C++" (Addison-Wesley, 1994),有时简称为 "D&E"。读了它,你会了解到有哪些特性被增加到C++中,以什么顺序,以及为什么。你还会知道哪些特性被放弃了,以及为什么。你甚至可以了解到一些幕后故事,如dynamic_cast(参见条款39和M2)如何被考虑,被放弃,又被考虑,最后被接受 ---- 以及为什么。如果你理解C++有困难,D&E将为你驱散心头的疑云。
对于C++如何成为现在的样子,"The Design and Evolution of C++" 提供了丰富的资料和见解,但它绝对不是正式的语言规格说明。对此你得求助于C++国际标准,一本令人印象深刻的长达700多页的正式文本。在那儿你可以读到象下面这样刻板的句子:
一个虚函数调用所使用的缺省参数是表示对象的指针或引用的静态类型所决定的虚函数所声明的缺省参数。派生类中的重载函数不获取它重载的函数中的缺省值。
这段话是条款38("决不要重新定义继承而来的缺省参数值")的基础,但我期望我对这个论题的论述比上面的原文多少更让人容易理解一些。
C++标准不是临睡前的休闲读物,而是你最好的依靠 ---- 你的 "标准" 依靠 ---- 如果你和其他人(比如,编译器供货商,或采用其它工具编程的开发人员)对什么东西是或不是C++有分歧的话。标准的全部目的在于,为解决这类争议提供权威信息。
C++标准的官方名称很咬口,但如果你需要知道,就得知道。这就是:International Standard for Information Systems----Programming Language C++。它由International Organization for Standardization (ISO)第21工作组颁布。(如果你爱钻牛角尖,它实际上是由ISO/IEC JTC1/SC22/WG21颁布的----我没有添油加醋)你可以从你的国家标准机构(在美国,是ANSI,即American National Standards Institute)定购正式C++标准的副本,但C++标准的最新草稿副本 ---- 和最终文件十分相近(虽然不完全一样)---- 在互联网上是免费提供的。可以找到它的一个好地方是 "the Cygnus Solutions Draft Standard C++ Page" (http://www.cygnus.com/misc/wp/),互联网上变化速度很快,如果你发现这个网站不能连接也不要奇怪。如果是这样,搜索引擎一定会帮你找到一个正确的URL。
我说过,"The Design and Evolution of C++" 对于了解C++语言的设计思想很有好处,C++标准则明确了语言的具体细节;如果在 "D&E千里之外的视野" 和 "C++标准的微观世界" 之间存在承上启下的桥梁那就太好了。教程应当适合于这个角色,但它们的视角往往偏向于标准,更侧重于说明什么是语言,而没有解释为什么。
进入ARM吧。ARM是另一本书,"The Annotated C++ Reference Manual" (Addison-Wesley, 1990),作者是Margaret Ellis和Bjarne Stroustrup。这本书一出版就成为了C++的权威,国际标准就是基于ARM(和已有的C标准)开始制定的。这几年间,C++标准和ARM中的说明在某些方面有分歧,所以ARM不再象过去那样具有权威性了。但它还是很具参考价值,因为它所说的大多数还是正确的;所以,在C++领域中,有些厂家还是坚持采用ARM规范,这并不少见,毕竟,标准只是最近才定下来。
然而,使得ARM真正有用的不是它的RM部分(the Reference Manual),而是A部分(the annotations):注释。针对C++的很多特性 "为什么" 要象现在这样工作,ARM提供了全面的解释。这些解释D&E中也有一些,但大多数没有,你确实需要了解它们。例如,第一次碰到下面这段代码,大部分人会为它发疯:
class Base {
public:
virtual void f(int x);
};
class Derived: public Base {
public:
virtual void f(double *pd);
};
Derived *pd = new Derived;
pd->f(10); // 错误!
问题在于Derived::f隐藏了Base::f,即使它们取的是不同的参数类型;所以编译器要求对f的调用取一个double*,而10这个数字当然不行。
这不很合理,但ARM对这种行为提供了解释。假设调用f时,你真的是想调用Derived中的版本,但不小心用错了参数类型。进一步假设Derived是在继承层次结构的下层,你不知道Derived间接继承了某个基类BaseClass,而且BaseClass中声明了一个带int参数的虚函数f。这种情况下,你就会无意中调用了BaseClass::f,一个你甚至不知道它存在的函数!在使用大型类层次结构的情况下,这种错误会时常发生;所以,为了防患于未然,Stroustrup决定让派生类成员按名字隐藏掉基类成员。
顺便指出,如果想让Derived的用户可以访问Base::f,可以很容易地通过一个using声明来完成:
class Derived: public Base {
public:
using Base::f; // 将Base::f引入到
// Derived的空间范围
virtual void f(double *pd);
};
Derived *pd = new Derived;
pd->f(10); // 正确,调用Base::f
对于尚不支持using声明的编译器,另一个选择是采用内联函数:
class Derived: public Base {
public:
virtual void f(int x) { Base::f(x); }
virtual void f(double *pd);
};
Derived *pd = new Derived;
pd->f(10); // 正确,调用Derived::f(int),
// 间接调用了Base::f(int)
借助于D&E和ARM,你会对C++的设计和实现获得透彻理解,从而可能参悟到:有时候,看似巴洛克风格的建筑外观之后,是合理严肃的结构设计。(译注:巴洛克风格的建筑极尽富丽堂皇、粉装玉琢,因而结构复杂,甚至有点怪异)将这些理解和C++标准的具体细节结合起来,你就矗立于软件开发的坚实基础之上,从而走向真正有效的C++程序设计之路。