第1章 致读者

“是时候了,”海象说,“该谈谈各种各样的事情了。”

L. Carroll

本书的结构----怎样学习C++----C++的设计----效率和结构----哲学注记----历史注记----C++适合做什么----CC++----C程序员的建议----C++程序员的建议----有关在C++里编程的思考----忠告----参考文献

1.1 本书的结构

这本书包括六个部分:

导论:第13章给出的是有关C++ 语言,它所支持的关键性程序设计风格,以及有关C++标准库的一个综述。

第一部分:第49章是有关C++ 内部类型,以及由它们出发构造程序的基本功能的一个具有教材性质的介绍。

第二部分:第1015章是有关使用C++ 做面向对象和通用型程序设计的一个具有教材性质的介绍。

第四部分:第1622章介绍C++的标准库。

第五部分:第2325章讨论设计和软件开发方面的一些论题。

附录:附录AE提供了语言的技术性细节。

1章是对全书的综述。这里提供了一些有关如何使用C++的建议,以及一些有关C++ 及其应用的背景性信息。你应该大略地读一读这一章,先注意读那些看起来有意思的东西,在读了本书的其他一些部分之后再回来读一读。

2章和第3章是有关C++ 程序设计语言及其标准库的主要概念和特征的一个综述。这两章的目的是促使你在基础性概念和基本语言特征上用一些时间,在这里展示了利用完整的C++ 语言可以描述些什么。即使没有其他内容,这两章也会使你确信C++ 并不(只)是C,而且,在本书的第一版和第二版之后,C++ 已经走过了很长的一段路。第2章给出了有关C++ 的一个高层次的认识,其中的讨论集中在那些支持数据抽象、面向对象的程序设计和通用型程序设计的语言特征方面。第3章介绍了标准库的基本原理和主要功能,这也使我可以在随后的章节里使用标准库,也使你能在练习中利用各种库功能,而不是去直接依靠低级的内部特征。

作为导论的这些章也是一种具有一般性的技术的实例,这种技术将在这整本书中始终如一地运用:在展开对某些技术或者特征的更直接更实质性的讨论时,我有时会先简洁地说明一个概念,而到晚些时候再去深入地讨论它。这种方式使我可以在更一般性地处理一个题目之前给出很实际的例子。也就是说,本书的组织方式反映了这样的一种观点:我们通常的学习最好是从具体发展到抽象甚至是在那些抽象的东西本身很简单,回过头看非常明显的地方。

第一部分描述的是C++ 的一个子集,它支持传统上在CPascal里做的那种风格的程序设计。这里的内容覆盖了基本类型、表达式、以及C++ 程序的控制结构。也讨论了由名字空间、源程序文件和异常处理所支持的模块化问题。我假定你原已熟悉在第一部分中用到的那些基本程序设计概念。例如,我将解释C++ 中表述循环和递归的功能,但却不去花许多时间解释为什么这些概念非常有用。

第二部分描述C++ 里定义和使用新类型的功能。具体的和抽象的类(界面)都在这里讨论(第10和第12章),还有运算符重载(第11章),多态性,以及类层次结构的使用(第1215章)。第13章讨论模板,也就是在C++ 里定义一族类型或函数的机制。这里阐释了提供容器(例如表),以及支持通用型程序设计的那些基本技术。第14章描述异常处理,讨论对错误的处理,说明有关容错的策略等等。我假定你或者是不很熟悉面向对象程序设计和通用型程序设计,或者是能从有关C++ 怎样支持主要的数据抽象技术的解释中获益。正因为此,我将不仅描述支持这些抽象技术的语言特征,也要解释这些技术本身。第四部分将在这个方向上继续前进。

第三部分描述C++ 标准库。这里的目标是为如何使用这个库的提供一种理解,阐述一般性的设计和编程技术,也说明如何去扩充这个库。标准库提供了容器(如listvectormap;第1617章),标准算法(如sortfindmerge;第1819章),字符串(第20章),输入输出(第21章),以及对数值计算的支持(第22章)。

第四部分讨论的是在把C++ 用于大型软件系统的设计和实现时所引出的论题。第23章集中于设计和管理方面,第24章讨论C++ 程序设计语言和设计论题之间的关系,第25章给出将类应用于设计的一些方法。

附录AC++ 的语法描述,带有少量的标注。附录B讨论CC++,以及标准C++(也称为ISO C++ ANSI C++)和在此之前的C++ 版本之间的关系。附录C描述一些语言技术实例。附录D解释标准库中支持国际化的功能。附录E讨论标准库在异常时安全性方面的保证和要求。

1.1.1 例子和参考

本书强调的是程序的组织,而不是算法的书写。因此,我完全避免了巧妙的或难以理解的例子。一个平凡的程序往往更适于用于展示语言定义的某一个方面,或者程序结构中的某一点。例如,我可能在某个地方采用Shell排序,而在实际代码中用快速排序则更好一些。我也经常把用更合适的算法重新实现作为一道练习题。在实际代码中,调用一个库函数也常比使用在这里展示语言特征的代码更值得称道。

教科书上的例子必然会给人有关软件开发的一种经过包装的观点。由于小例子的清晰性和简单性,由规模而引起的复杂性消失了。依我看,没有什么东西能够代替去写一些实际大小的程序,只有那样才能真正感受程序设计和程序设计语言究竟是什么。这本书将集中关注语言特征,关注那些支持组合出各种程序的基本技术,以及有关组合的规则。

有关实例的选择反映了我在编译器、基础库和模拟方面的背景。这些例子都是在真实代码中能找到的东西的简化版本。这种简化是必需的,只有这样才能保证语言特征和设计观点不会被繁琐细节所淹没。不存在没有真实背景的“灵巧”实例。在任何地方,只要可能,我就把那一类实例移交给附录C,在那里的例子中总用变量名字xy,类型名AB,函数总是f() g()

在所有代码实例中,标识符用的是按比例宽度的字体。例如:

[5P1]

初看起来这种表示风格好像“不大自然”,因为程序员已经习惯于采用等宽字体的代码。但是,一般的认识是,比例宽度的字体在表现正文方面优于等宽字体。采用比例宽度字体也使我的代码里更少出现非逻辑的断行现象。进一步说,我的实际经验说明,大部分人经过一小段时间后就会觉得这种风格更容易读。

只要有可能,C++ 语言和库特征都在将使用它们的上下文中介绍,而不是在手册中以干巴巴的方式介绍。这里所描述的语言特征及其细节程度,也反映了我对于有效使用C++ 的需要的认识。与此伴生的另一本书,The Annotated C++ Language Standard(《带标注的C++ 语言标准》)将由Andrew Koenig和我合著,那是这个语言的一个完整定义,并附加一些注释,使之更容易理解。逻辑上说还应该有另一本伴生的书,The Annotated C++ Standard Library。但是,由于时间和写作能力的限制,我无法允诺去完成这件事情。

本书内的相互参考采用“2.3.4节”(表示第2章第3节第4小节),B.5.6节(表示附录B5节第6小节),以及6.6[10](第6章练习10)的形式。楷体用于表示强调(如,“一个字符串文字量是不能接受的”),重要概念的第一次出现(如,多态性),以及代码实例中的注释。C++ 语法中的非终结符用斜体(如,for-statement)。半黑斜体用于引用代码实例中的标识符、关键字和数值(如,classcounter1712)。

1.1.2 练习

练习可以在各章的最后找到。这些练习主要是写某个程序的变形。请读者一定写出有关一个解的充分的代码,经过编译并至少用一些测试情况去运行它。这些练习的难度有相当大的变化,因此为它们加了有关其难度估计的标记。度量方式是指数的,如果一个 (*1) 练习需要花掉你10分钟时间,那么一个 (*2) 就可能需要一个小时,而 (*3) 可能要用一天。写程序和测试程序所需要的时间将更多地依赖于你的个人经验,而不是练习本身。如果你第一次去熟悉一个计算机系统,并要设法让一个 (*1) 练习运行,它可能耗费你一天的时间。在另一方面,如果某人正好手头有一组合适的程序,完成一个 (*5) 练习或许只要一个小时就够了。

任何有关C程序设计的书籍都可以作为第一部分额外练习的来源。任何有关数据结构或者算法的书都可以作为第二部分和第三部分的练习来源。

1.1.3 有关实现的注记

本书中使用的语言是在C++标准 [C++, 1998] 中定义的“纯的C++”。因此,这里的例子应该能在每个C++ 实现上运行。书中主要的程序片段都在几个C++ 实现里测试过。那些用到了最近纳入C++ 的特征的程序未必能在每个C++ 实现上编译。当然,我看不出有什么必要去说明哪个实现无法编译哪些例子。这种信息将会迅速过时,因为实现者们正在努力工作,以保证他们的实现能够正确地接受每一个C++ 特征。参看附录B,那里有一些关于如何应付老的C++ 编译器和为C编译器写的代码的建议。

1.2 学习C++

在学习C++ 时,最重要的事情就是集中关注概念,不要迷失在语言的技术细节中。学习语言的目的是成为一个更好的程序员;也就是说,使自己在设计和实现新系统时,在维护老系统时,能够工作得更有成效。为此,对于程序设计和设计技术的理解远比对细节的理解更重要,而这种理解的根本是时间和实践。

C++ 支持多种不同的程序设计风格。所有这些的基础是强类型检查,大部分的目标都是要获得一种高层次的抽象,以直接表达程序员的思想。每种风格都可以在有效管理时间和空间的情况下达到它的目的。来自别的不同语言(比如说CFortranSmalltalkLispMLAdaEiffelPascal,或者Modula-2)的程序员应该认识到,要想从C++ 中获益,他们就必须花时间去学习,以使适合于C++ 的程序设计风格和技术真正变成自己的东西。这一建议同样也适用于那些熟悉C++ 早期的和表达能力较弱的版本的程序员们。

盲目地将一种在某个语言中很有效的技术应用到另一个语言,经常会导致笨拙的、性能低下的、难以维护的代码。在写这样的代码时也会备受挫折,因为每行代码,每条编译错误信息都在提醒程序员,正在使用的这个语言是与“那个老语言”不同的。你可以用FortranCSmalltalk等的风格写程序,可以在任何语言里这样做。但是在一个有着不同哲学观点的语言里,这样做既不愉快也不经济。每种语言都可以是如何写C++ 程序的一个丰富的思想源泉。但是,这些思想必须转化为某种能够适应C++ 的一般结构和类型系统的东西,以便能够有效地出现在这个不同的上下文中。去颠覆一个语言的基本类型系统,可能得到的至多是伊皮鲁斯式的胜利。

C++ 支持一种逐步推进的学习方式。你学习一个新语言的方式依赖于你已经知道些什么,还依赖于你的学习目的。并没有一种适合于所有人的学习方式。我的假定你学习C++是为了成为一个更好的程序员和设计师。这就是说,我假定了你学习C++ 的意图并不简单地是为了再学一种新的语法形式,要去做某些已经习惯的事情,而是想学习一种构造系统的新的更好的方式。这件事只能逐渐完成,因为获得任何有意义的新技能都要花时间,需要经过实践。请想一想,要学会一种新的自然语言,或是学会演奏一种新的乐器要花多少时间。成为一个更好的系统设计师可能容易一些,也可能快一些,但绝不会容易到或者快到大部分人所希望的那种程度。

正因为这样,你将要在还没有理解所有语言特征和技术之前就去使用C++__常常是去构造实际的系统。通过支持多种程序设计范例(第2章),C++支持在不同掌握程度上进行生产性的程序设计。每一种新的程序设计风格将为你的工具箱增加一种新工具,而且每种风格本身都是有效的,每种都能提高你作为程序员的效率。C++ 的组织方式使你能大致线性地学习它的概念,并且一路上获得实际利益。这是非常重要的,因为这就使你得到的利益大致与你付出的努力成比例。

有关一个人是否应该在学习C++ 之前先学C的问题存在着长期争论。我坚定地认为最好的方式是直接学习C++C++ 更安全,表达能力更强,而且减少了关心低级技术的需要。在你已经掌握了CC++ 的公共子集和某些C++直接支持的高级技术之后,你会更容易去学习C中那些更诡秘的部分,而需要它们只是为了弥补C中高级功能的缺位。附录B是一个指南,可以帮助程序员从C++ 走向C,比如说,去处理那些作为遗产的代码。

存在着一些独立开发和发行的C++ 实现,有许多的工具、库和软件开发环境可以使用。成堆的教科书、手册、杂志、通讯、电子公告板、邮件列表、会议和课程不断通知你有关C++ 的最新开发情况,有关它的使用、工具、库和实现等等。如果你计划去认真地使用C++,我强烈地建议你去查阅这些资源。任何单独的东西都有其重点和倾向性,所以,请参考至少两个来源。例如,参考 [Barton, 1994][Booch, 1994][Henricson, 1997][Koenig, 1997][Martin, 1995]

1.3 C++ 的设计

简单性是一个重要设计准则:如果在某个地方有一个选择,简化语言的定义或者简化编译器,那么我们一定选前者。当然,还有一个最重要的考虑是保持与C的高度兼容性 [Koenig, 1989] [Stroustrup, 1994] (附录B),这也就排除了对C语法的清理。

C++ 没有内部的高级数据类型,也没有高级的基本操作。举例来说,C++ 没有提供带有求逆运算的矩阵类型,也没有带拼接运算的字符串类型。如果某个用户需要一个类型,那么可以在语言本身之中定义它。事实上,定义新的通用或者专用类型就是在C++ 里最基本的程序设计活动。一个设计良好的用户定义类型与一个内部类型之间的差异仅仅在于其定义的方式,而不在其使用方式。在第三部分描述的标准库提供了许多这样的类型及其使用的例子。从用户的观点看,在内部类型和由标准库提供的类型之间的差异非常小。

C++ 的设计中,极力避免了那些即使不用也会带来运行时间或者空间额外开销的特征。例如,要求必须在每个对象里存储某种“簿记信息”的结构被拒绝了,所以,如果你定义了一种由两个16位的量组成的结构,它将能放进一个32位的寄存器里。

C++ 被设计为能使用传统的编译和运行时的环境,也就是那种在UNIX上的C程序设计环境。幸运的是,C++ 从来没有被束缚于UNIX,它只是简单地采用UNIXC作为一种语言、库、编译器、连接器、执行环境等等之间关系的模型。这种最小化的模型帮助C++ 在几乎每个计算平台上取得了成功。当然,在那些提供了更多有意义的支持的环境里,使用C++ 当然就更好了,像动态装载、增量编译、类型定义数据库等都能导致更好的使用,又不会影响语言本身。

C++ 的类型检查和数据隐藏特征依赖于编译时对程序的分析,以防止因为意外而破坏数据的情况。它们并不提供系统安全性或防止某些人有意地打破这些规则。它们当然可以随意使用而不会带来运行时额外的时间或空间开销。这种想法是很有用的,一种语言特征必须不仅是优美的,还必须是在真实程序的环境中能够负担起的东西。

有关C++ 设计的更系统和详尽的描述,请看 [Stroustrup, 1994]

1.3.1 效率和结构

C++ 是从C程序设计语言出发开发出来的,除了少量例外,它继续维持了以C作为一个子集。作为基础语言,C++ C子集设计保证了在它的类型、运算、语句与计算机直接处理的对象(数、字符和地址)之间的紧密对应关系。除了newdeletedynamic_cast,以及throw运算符和try-块,C++ 的各种表达式和语句都不需要特殊的运行时支持。

C++ 可以使用与C一样的函数调用及返回序列或者其他效率更高的方式。在这种相对有效的机制仍然被认为是代价过高之处,C++ 函数还可以采用在线替换,这样就使我们能在享受到函数记法上方便的同时,又不必付出任何运行时的开销。

C的一个初始目标是在大部分苛刻的系统程序设计工作中代替汇编语言。在设计C++ 时,也特别注意了不在这些已经取得的领域中做出任何妥协。在CC++ 之间的差异,从根本上说,在于强调类型和结构的级别不同。C是有表达力的、宽容的,而C++ 的表达力更强。当然,为了获得这种表达能力的增加,你必须更多地关注对象的类型。知道了对象的类型,编译器就能正确处理表达式;如果不是这样,你就必须自己在令人难受的细节程度上去描述有关操作。知道了对象的类型,编译器就能检查错误;否则这些东西就会遗留下来直至测试阶段或许留到更晚的时候。请注意,使用类型系统去检查函数的参数,去保护数据不被意外地破坏,去提供新的类型,去提供新的操作,如此等等,这些在C++ 里都没有增加任何运行中的时间或者空间开销。

C++ 中特别强调程序的结构,这反映了自C设计以来程序规模增长的情况。你可以通过玩命干做出一个小程序(比如说,1000行),甚至是在你违反了所有有关好风格的规则的情况下。而对于更大的程序,情况就完全不同了。如果一个100000行的程序的结构极其糟糕,你就会发现,引进新错误的速度像清除老错误一样快。C++ 的设计就是为了使较大的程序能够以一种合理的方式构造出来,并因此使一个人也有可能对付相当大的一批代码。进一步的目标是使一个平均行的C++ 代码能够表述出远比一个平均行的CPascal代码更多的东西。C++ 已经证明它超过了这些目标。

并不是每块代码都可能是结构良好的,与硬件无关的,容易阅读的,如此等等。C++ 也拥有一些特征,其意图就是为了以一种直截了当和高效的方式去操纵硬件功能,而不顾安全性或者容易理解诸方面的问题。它还拥有一些特征,使得我们能够将这样的代码隐藏在优美和安全的界面之后。

很自然,对更大型的程序使用C++ 语言,将会导致由成组的程序员使用C++C++ 所强调的模块化,强类型的界面,以及灵活性在这里都能发挥作用。C++ 在各种为写大程序而提供的功能之间做了很好的平衡,像其他任何语言一样好。当然,随着程序变得更大,与它们的开发和维护相关的问题也会逐渐从语言的问题转移到更为全局性的工具和管理的问题。第四部分将探讨这方面的论题。

本书强调的是为提供通用功能、普遍有用的类型、库等等等的各种技术。这些技术能为写小程序的程序员服务,也能为写大程序的程序员服务。进一步说,由于任何非平凡的程序都是由许多半独立的部分组成,写这些部分的技术定能服务于开发所有应用的程序员。

你或许会怀疑,用更细节的类型结构去刻画大程序,会不会导致更大的程序正文?对于C++ 而言情况并不是这样。一个声明了函数参数类型、使用了类等等的C++ 程序,通常比没有使用这些功能的等价C程序短一点。在那些使用了库的地方,C++ 程序就要比等价的C程序短得多。当然了,这里还得假定那个功能等价的C程序确实能构造出来。

1.3.2 哲学注记

一个程序设计语言要服务于两个相互关联的目的:它要为程序员提供提供一种描述所需执行的动作的载体,还要为程序员提供一集概念,使他们能利用这些概念去思考什么东西是能够做的。在理想的情况下,第一个用途要求一种“尽可能接近机器的”语言,以使机器的所有重要方面,都能以一种对程序员相当明显的方式简单而有效地加以处理。C语言的基本设计就是基于这一观点。而第二个用途所要求的理想语言是“尽可能接近需要解决的问题”,这样才能使解决方案中的概念能够直接而紧凑地表达出来。被加入C语言,从而塑造出C++ 的那些概念,从根本上说,就是基于这个观点设计的。

在我们思考/编程所用的语言和我们能够设想的问题与解之间的联系非常紧密。正是由于这个因素,以避免程序员犯错误为目的而对语言的特征加以限制,这一做法至少也是很危险的。就像自然语言中的情况,掌握至少两种语言就非常有价值。一个语言为程序员提供了一集概念工具;如果它们不适合于某件工作,程序员将简单地放弃这个语言。好的设计和不出现错误都不能仅由某些语言特征的存在或者缺席来保证。

类型系统对于各种各样非平凡的工作都特别有帮助。事实上,C++ 的类概念已经被证明是一种极为强有力的概念工具。

1.4 历史注记

我发明了C++,写出了它的第一个定义,做出了它的第一个实现。我选择并整理出C++的设计准则,设计了它的所有主要特征,并在C++ 标准化委员会里负责处理扩充建议。

很清楚,C++ 大大地受惠于C [Kernighan, 1978]。除封闭了其类型系统中的少量严重漏洞之外(附录B),C++仍保留C为一个子集。我还保留了C在功能上的重点,能在足够低的层次上处理最苛刻的系统程序设计工作。C转而从其前驱BCPL [Richards, 1980] 受惠颇多;事实上,BCPL // 注释约定也被(重新)引进了C++。给C++ 以灵感的另一个主要来源是Simula67 [Dahl, 1970][Dahl, 1972];类的概念(包括派生类和虚函数)都是从那里搬过来的。C++ 有关重载运算符和自由地将声明放置在可以出现语句的任何位置的功能使人联想到Algol68 [Woodward, 1974]

在本书的第一版之后,这个语言已经经过广泛的审查和精炼。审查的主要部分是对于重载的解析,连接,以及存储管理功能。此外还做了许多小修改,以增强与C的兼容性。还加进了一些推广和若干主要的扩充,包括:多重继承、static成员函数、const成员函数、protected成员、模板、异常处理、运行时类型标识和名字空间。所有这些扩充和修订的主旨都是都是为了使C++ 能够成为一个写库、使用库的更好的语言。有关C++ 演化过程的描述参见 [Stroustrup, 1994]

模板功能的设计,从根本上说,是为了支持静态类型的容器(如表、向量和映射),以及幽雅有效地使用这些容器(通用型程序设计)。这里的一个关键目标是减少宏和强制(显式类型转换)的使用。模板机制部分地受到Ada中类属的启发(包括其威力及其弱点),部分地受到Clu语言参数模块的影响。与此类似,C++ 的异常处理机制部分地受到Ada [Ichbiah, 1979]Clu [Liskov, 1979] ML [Wikstr?m, 1987] 的影响。其他方面开发是在19851995的时间跨度中做出的,例如,多重继承、纯虚函数、以及名字空间,这些基本上是在C++ 使用经验推动下推广而来,而不是由其他语言引进的。

这个语言的早期版本是大家都知道的“带类的C[Stroustrup, 1994],它从1980年开始使用。初始发明这个语言,是因为我想去写某些事件驱动的模拟程序,Simula67可能对于它们是最理想的,除了效率考虑之外。“带类的C”被用在一些主要的项目上,这使其用于写那种使用最少的时间空间的程序的功能得到了严格的检验。这个语言缺乏运算符重载、引用、虚函数、模板、异常和许多细节。C++ 在研究组织之外的最初使用是在1983年。

名字C++(读作see plus plus。(中文一般读作“C加加”译者))是Rick Mascitti1983年夏天起的名字。这个名字象征着从C改变过来的演化性质;“++”是C的增量运算符。稍微短一点的名字“C+”则是个语法错误,它也曾被用于另一个与这里毫无关系的语言。C的语义权威们认为C++ 不如 ++C。它没有被称作D是因为它是C的一个扩充,而且也从未打算通过删除某些特征去修正一些问题。对于名字C++ 的另一种解释,请参看 [Orwell, 1949] 的附录。

C++ 原始的基本设计就是为了使我的朋友和我不必去用汇编、C或各种摩登的高级语言做程序。它的主要用途是为了那些作为个人的程序员,使他们能更容易、更愉快地写出好的程序来。在早期的那些年里根本就没有C++ 的纸面设计;设计、文档和实现是平行开展的。从来没有一个“C++”项目或者“C++设计委员会”。自始至终,C++的演化就是为了面对用户遇到的问题,也是在我的朋友、同事和我之间讨论的结果。

后来,C++ 使用的爆炸性增长导致情况产生了某些变化。在1987年的某个时候,情况已经很清楚,C++ 的标准化已是一件不可避免的事情了,我们需要开始去为标准化工作准备一个基础 [Stroustrup, 1994]。这导致了一种有意识的努力,去维持C++ 编译器的实现者们和主要用户之间的联系,通过文章和电子邮件,也通过在C++会议和其他各种场合中面对面的讨论。

AT&T的贝尔实验室对这个工作做出了主要的贡献,它允许我将C++ 参考手册的草稿和各种修订版本提供给实现者和用户共享。由于这些人中的许多是为那些可以看作是AT&T的竞争对手的公司工作的,这种分发的价值怎样估计都不过分。一个不那么光明磊落的公司可能早就导致了严重的语言分裂局面,为此它只要什么都不做也就够了。在那个时候,大约一百个个人,来自数十个组织,阅读并评述着那本被广泛接受的参考手册,它也被作为ANSI C++ 标准化工作的基础文件。这些人的名字可以在The Annotated C++ Reference Manual(《带标注的C++ 参考手册》)[Ellis, 1989] 中找到。最后,ANSIX3J16委员会于198912月在Hewlett-Pachard的建议下建立起来。到19917月,这个ANSI(美国国内的)C++标准化变成了ISO(国际)的C++ 标准化工作的一部分。自1990年起,这个联合的C++标准化委员会就已经成为有关C++ 的演化和精练其定义的主要论坛。我自始至终在这些委员会中服务,特别是作为有关扩充的工作组的主席,我对有关C++ 的重要修改以及增加新语言特征的建议的处理直接负责。最初的标准草案在19954月提供给公众审阅,ISO C++标准(ISO/IEC 14882)在1998年被批准。

C++ 是与本书中描述的某些关键性的类携手一起演化前进的。例如,我设计了复数、向量和堆栈类,以及运算符的重载机制;字符串和表类是Jonathan Shopiro和我一起开发的,作为同一个工作的一部分。Jonathan的字符串和表类作为库的部分最早得到广泛使用。标准C++ 库的字符串类就植根于这些早期工作。在 [Stroustrup, 1987] 12.7[11] 描述的作业库是曾经写出的最早“带类的C”程序。我写出了它及其一些相关的类,是为了支持具有Simula风格的模拟。这个作业库已经被修改并重新实现,主要是由Jonathan Shopiro完成,并一直被广泛使用着。在本书的第一版里描述的流库是我设计和实现的;Jerry Schwarz将其转变为iostream库(第21章),采用了Andrew Koenig的操作符技术(21.4.6节)和其他思想。标准化过程中又对这个iostream库做了进一步精练,其中的大量工作是由Jerry SchwarzNathan MyersNorihiro Kumagai完成的。模板功能的开发受到由Andrew KoenigAlex Stepanov和我设计的vectormaplistsort模板的影响。在另一方面,Alex Stepanov在使用模板的通用型程序设计方面的工作产生出标准C++ 库的容器和算法部分(16.3节,第17章,第18章,19.2节)。数值计算的valarray库的基础是Kent Budge的工作。

1.5 C++ 的使用

C++ 被数以十万计的程序员应用到几乎每个领域中。这种应用得到十几个相互独立的实现,数以百计的库,数以百计的教科书,几种技术杂志,许多会议,以及不计其数的顾问们的支持。在各种层次上的训练和教育到处都可以获得。

早期的应用趋向于具有很强的系统程序设计味道。例如,有几个主要操作系统是在C++ 里写出的 [Compbell, 1987] [Rozier, 1988] [Hamilton, 1993] [Berg, 1995] [Parrington, 1995],更多得多的系统用C++ 做了其中的关键部分。我认为C++不应在低层次的效率上妥协,这就使我们有可能用C++ 写设备驱动程序,或者其他需要在实时约束下直接操作硬件的软件。在这样的代码中,性能的可预见性至少也与粗略的速度同样重要。C++的设计使得它的每种特征都可以在严格的时间和空间约束下使用 [Stroustrup, 1994, 4.5]

在大多数应用中都存在一些代码片段,它们在性能上的可接受性是至关重要的。当然,最大部分的代码并不在这些片段里。对于大部分代码而言,可管理、容易扩充、容易测试是最关键的问题。C++ 对所有这些关注点的支持已使它被广泛应用于一些领域,在其中的一些领域里可靠性是最基本的要求,在另一些领域里需求在不断地随着时间而发生显著的变化。这方面的例子如银行、贸易、保险业、远程通讯,以及各种军事用途。已经有许多年了,美国的长途电话系统的核心控制依赖于C++,所有800电话(即那些由被叫方付款的电话)都由一个C++ 程序寻找路由 [Kamath, 1993]。许多这样的应用是大规模的,并且长期运行着。作为这种情况的结果,稳定性、兼容性和可伸缩性都成为C++ 开发中被始终关注的问题。成百万行的C++ 程序并不是罕见的情况。

C类似,C++ 在设计时并没有将数值计算放在心里。但是,确实有许多数值的、科学的、以及工程的计算是在C++ 里做的。产生这种情况的一个主要原因是,传统的数值性工作常常必须与图形以及基于数据结构的计算相结合,而这些很难融进传统Fortran的模型之中 [Budge, 1992] [Barton, 1994]。图形学和用户界面正是使用C++ 最深入的领域。任何人要是使用过Apple Macintosh或者运行着WindowsPC,也都间接地使用了C++,因为这些系统的基本用户界面都是C++程序。此外,一些最流行的支持UNIXX的库也是用C++ 写的。这样,C++ 就成了大量应用的最常见选择,只要用户界面是其中的主要部分。

所有这些都指向了或许是C++的最强之处:它能够有效地用到那些需要在各种各样的不同应用领域中工作的应用系统上。很容易找到一个应用系统,其中涉及到局域或者广域网络、数值处理、图形、与用户的交互、以及数据库访问等等。在传统上,这些应用领域被认为是分离的,最常见的情况是它们由不同的技术团体使用各自的程序设计语言去处理。然而,C++ 已经被应用于所有的这些领域。进一步说,它还能与用其他语言写出的代码片段或者程序共存。

C++ 被广泛应用于教学和研究。这很令一些人吃惊,因为有些人曾经正确地指出,C++ 并不是已经设计出的各种语言中最小和最清晰的。但不管怎样,它是:

对于教授基本概念而言足够清晰的,

对于深刻的项目而言足够现实、高效和灵活的,

对依赖各种不同开发和执行环境的组织或研究机构而言,使用起来足够方便,

对作为教高级概念和技术的媒介而言,足够的容易理解,还有

对作为从学习到非学术使用的工具而言,也足够的商业化。

C++ 是一个可以伴随你成长的语言。

1.6 CC++

C被选作C++ 的基础语言是因为它:

  1. 是通用的、简洁的、相对低级的;
  2. 适合用于大部分系统程序设计工作;
  3. 可以在每个地方的任何系统上运行;以及
  4. 适应于UNIX程序设计环境。
C有它的问题,但一个从空白出发设计的语言也必然会有一些问题,况且我们了解C的问题。更重要的是,从C出发已经使“带类的C”成了一个有用的工具(或许还比较笨拙),而且只是在开始想到要将类似Simula的类加到C上之后没几个月的时间。

随着C++ 的使用变得更广泛,以及它所提供的覆盖和超越C的功能变得更加重要,关于是否还应该保持兼容性的问题又一再地被提出来。很清楚,如果抛弃C的某些传统就可以避免一些问题(参见,例如 [Sethi, 1981])。这些都没有做,因为:

  1. 存在着成百万行的C代码可能从C++ 中获益,先决条件是不必将它们完全从C重新写成C++
  2. 存在着成百万行用C写出的库和功能软件代码可以从C++ 程序里使用,或者在C++ 程序之上使用;先决条件是C++ 能够与C连接兼容,且在语法上类似。
  3. 存在着数以十万计的程序员了解C语言,只需要去学习C++ 新特征的使用,而不想去重新学习基础;而且
  4. C++ C将在许多年中被同一些人用于同样的系统,因此其差异必须或者是很小,或者是很大,以最大限度地减少错误和混乱的发生。
C++ 的定义已经做了许多修订,以保证任何同时在CC++ 里合法的结构在两个语言中都具有同样的意义(除了少量例外;B.2节)。

C语言本身也在发展和演化,部分地是在C++ 开发的影响之下 [Rosler, 1984]ANSI C标准 [C, 1990] 就包含了从“带类的C”借去的函数声明语法。借鉴是双向的,例如void*是为ANSI C发明的东西,但却在C++ 里第一次实现。如本书的第一版所允诺的,C++ 的定义已经过修订,以去掉无缘无故的不兼容性。今天的C++比原来更加与C兼容了。这里的想法是让C++ 尽可能接近ANSI C__但又不过于接近 [Koenig, 1989]。百分之百的兼容性从来就不是目标,因为这将危害类型安全性以及用户类型与内部类型的平滑集成。

了解C并不是学习C++ 的先决条件。在C中编程序被鼓励使用的许多技术和诀窍由于C++ 的特征而变得多余了。例如,显式类型强制(casting)在C++ 里就没有在C里那么频繁(1.6.1节)。然而,C程序倾向于也是C++程序。例如,在KernighanRechieThe C Programming Language(第二版)[Kernighan, 1988] 里的每个程序都是C++ 程序。任何有关静态类型语言的经验对于学习C++也都能有所帮助。

1.6.1 C程序员的建议

一个人对C了解得越好,在写C++程序时大概就越难避免C的风格,并会因此丢掉某些潜在C++的优势。请看一看附录B,那里描述了CC++之间的差异。这里是几个有关的要点,在这些地方做同样的事情时,在C++ 里存在比C更好的方式:

  1. C++里几乎不需要用宏。用const5.4节)或enum4.8节)定义明显的常量,用inline7.1.1节)避免函数调用的额外开销,用template(第13章)去刻画一族函数或者类型,用namespace8.2节)去避免名字冲突。
  2. 不要在你需要变量之前去声明它,以保证你能立即对它进行初始化。声明可以出现在能出现语句的所有位置(6.3.1节),可以出现在for语句的初始化部分(6.3.3节),也可以出现在条件中(6.3.2.1节)。
  3. 不要用malloc()new运算符(6.2.6节)能将同样的事情做得更好。对于realloc(),请试一试vector()3.8节)。
  4. 试着去避免void*、指针算术、联合和强制,除了在某些函数或类实现的深层。在大部分情况下,强制都是设计错误的指示器。如果你必须使用某个显式的类型转换,请设法去用一个“新的强制”(6.2.7节),设法写出一个描述你想做的事情的更精确的语句。
  5. 尽量少用数组和C风格的字符串。与传统的C风格相比,使用C++标准库string3.5节)和vector3.7.1节)常常可以简化程序设计。
如果要符合C的连接规则,一个C++函数就必须被声明为具有C连接的(9.2.4节)。

最重要的是,试着将程序考虑为一集由类和对象表示的相互作用的概念,而不是一堆数据结构和一些去拨弄数据结构中二进制位的函数。

1.6.2 C++程序员的建议

到今天,许多人使用C++已经十几年了。大部分人是在某个单一的环境里使用C++,并已学会了在早期编译器和第一代的库所强加的束缚之下生存。经常可以看到这种情况,一个很有经验的C++程序员不仅很多年没有注意引进的新特征,也没有看到有关特征之间关系的变化,而这些情况已经使一些全新的程序设计技术变成可行的东西了。换句话说,你在第一次学习C++时没有想到或者认为不实际的东西,或许今天已经变成一种高明的方式。你只有通过重新考察基础的东西才能弄清楚它们。

请按顺序浏览各章,如果你已经知道了某一章的内容,你可以只用几分钟就翻过去;如果你还不知道其内容,那么你会学到一些不曾预料到的东西。我在写这本书时就学到了不少东西,而且我怀疑会有哪个C++程序员知道这里给出的所有特征和技术。进一步说,要用好这个语言,你需要一种观点,以便给这集特征和技术带来一种秩序。通过书中的组织结构和实例,本书就提供了一种这样的观点。

1.7 有关在C++里编程的思考

在理想情况下,你需要通过三个步骤完成设计一个程序的工作。首先,你取得对问题的一个清晰的理解(分析);而后你标识出在一个解决方案中所涉及的关键性概念(设计);最后,你用一个程序表达这个解决方案(编程)。然而,问题的细节和解决方案中的概念,常常只有通过在一个程序中描述它们,以及让程序以可接受的方式运行的努力之下,才能真正被清楚地理解。而这正是程序设计语言选择的关键之处。

在大部分应用中都存在一些概念,它们很不容易表示为某个基本类型,也不容易表述为没有与之关联的数据的函数。遇到一个这样的概念,请在程序里声明一个类去表示它。一个C++类就是一个类型,也就是说,它刻画了这个类的对象的行为:它们如何建立,可以被如何操作,以及它们如何销毁。一个类可能也刻画了这些对象如何表示,虽然在设计一个程序的早期阶段这并不是一个主要考虑。写出好的程序,最关键的就是去设计这些类,使它们中的每一个都能很清楚地表示某个概念。这经常意味着你必须集中注意一些这样的问题:这个类的对象应该如何建立?这个类的对象能够被复制/销毁吗?什么操作能够作用于这种对象?如果对这类问题不存在很好的回答,对应的概念或许是从一开始就很不“清楚”。这时再多想想有关的问题以及为它所设定的解决方案,而不是立即开始去围着那个问题编码,这样做可能是个很好的主意。

最容易处理的概念是那些有着传统数学形式的东西:各种各样的数,集合,几何形状等等。基于文本的I/O,字符串,基本容器,对这些容器的基本算法,以及一些数学类都是标准C++库的组成部分(第3章,16.1.2节)。除此之外,还存在着许多可以使用的支持通用的或者各种领域专用概念的令人眼花缭乱的库。

概念不会存在于真空之中;总是存在着相互关联的一簇簇的概念。将程序里各个类按它们之间的关系组织起来即,确定一个解决方案所涉及到的不同概念之间的准确关系常常比首先单独地列出一些类更困难。最好别把结果弄成了一锅浆糊,其中的每一个类(概念)都相互依赖。考虑两个类,AB。像“A调用B的函数”,“A建立起一些B”,“A包含一个B成员”之类的关系很少会造成重要的问题。而像“A使用B的数据”之类的关系通常应该能清除掉。

威力最大的一种管理复杂性的智力工具就是某种层次性的序关系,也就是说,将相互有关的概念组织到一个树形结构中,使最一般的概念成为树根。在C++里,派生类表示的就是这种结构。一个程序常常能组织为一集类的树,或者一集有向无环图。这时,程序员刻画一组基类,每个都有它的一集派生类。虚函数(2.5.5节,12.2.6节)常常能被用于为一个概念的最一般版本(一个基类)定义操作。如果有必要,可以针对特定的类(派生类),对这些操作的解释做进一步的精确化。

有时,甚至一个有向无环图看起来也不足以组织起一个程序里的概念;有些概念似乎具有内在的相互依赖性。在这种情况下,我们应设法将这种循环依赖关系局部化,使它们不会影响程序的整体结构。如果你无法清除这种相互依赖,也无法将其局部化,那么你很可能进入了一个困境,没有一种程序设计语言能够帮你跳出来。除非你能设想出在基本概念之间的某种很容易陈述的关系,否则那个程序多半会变得无法管理。

解开依赖图的一种最好的工具就是界面和实现的清晰分离。抽象类(2.5.4节,12.3节)是C++处理这种问题的基本工具。

共性的另一种形式可以通过模板(2.7节,第13章)表示。一个类模板刻画了一族类。例如,一个表模板刻画了“T的表”,其中T可以是任何类型。这样,模板就是这样一种机制,它刻画的是如何通过给定另一个类作为参数,就可以生成出一个新的类来。最常见的模板是容器类,例如表、数组和关联数组,以及使用这些容器的基本算法。将一个类及其相关函数的参数化通过一个使用继承的类型表达通常是个错误,这件事最好是用模板做。

应该记住,许多程序设计工作能够仅用基本类型、数据结构、普通函数和若干库类完成,这样做既简单又清晰。涉及到定义新类型的全套装备都不应该使用,除了在那些确实需要它们的地方。

问题“一个人怎样才能在C++里写出好的程序?”与问题“一个人怎样才能写出好的英语散文”类似。存在着两个回答:“了解你想说的是什么”以及“实践,模仿好的作品”。两者都适用于C++,就像它们适用于英语一样去实践这一想法也同样不容易。

1.8 忠告

这里是一集在你学习C++的过程中或许应该考虑的“规则”。随着你变得更加熟练,你将能把它转化为某种更适合你的那类应用系统或者你自己的程序设计风格的东西。它们有意被写得很简单,因此都缺乏细节。请不要太顾及它们的文字。要写出一个好程序需要智慧、品味和耐性。你不会第一次就能把它搞好的。试验!

  1. 在编程序时,你就是为你针对某个问题的解决方案中的思想建立起一种具体表示。让程序的结构尽可能地直接反映这些思想:
  1. 如果你能把“它”看成一个独立的概念,就把它做成一个类。
  2. 如果你能把“它”看成一个独立的实体,就把它做成某个类的一个对象。
  3. 如果两个类有共同的界面,将此界面做成一个抽象类。
  4. 如果两个类的实现有某些显著的共同东西,将这些共性做成一个基类。
  5. 如果一个类是一种对象的容器,将它做成一个模板。
  6. 如果一个函数实现对某容器的一个算法,将它实现为对一族容器可用的模板函数。
  7. 如果一集类、模板等等互相之间有逻辑关系,将它们放进一个名字空间里。
  1. 在你定义一个并不是实现某个像矩阵或复数这样的数学对象的类时,或者定义一个低层的类型如链接表的时候:
  1. 不要使用全局数据(使用成员)。
  2. 不要使用全局函数。
  3. 不要使用公用数据成员。
  4. 不要使用友元,除非为了避免 [a] [c]
  5. 不要在一个类里面放“类型域”;采用虚函数。
  6. 不要使用在线函数,除非是效果显著的优化。
更特殊或更详尽的实用规则可以在每章最后的“忠告”一节里找到。请记住,这些忠告只是粗略的实用规则,而不是万古不变的定律。它们只应使用在“合理的地方”。从来就没有任何东西能够替代智慧、经验、常识和好的鉴赏力。

我发现具有“绝不要做这个”形式的规则不大有帮助。因此,大部分忠告被写成应该做什么的建议,而否定性的建议也倾向于不采用绝对禁止的短语。我不知道有任何一种主要的C++特征,对于它我没看见过良好的使用。在有关“忠告”的节里不包括解释,相反,每条忠告都引用了本书中某些适当的章节。在给出否定性的忠告时,对应章节里通常都提供了有关其他替代方式的建议。

1.9 参考文献

正文中有一些直接写出的参考文献,这里是一个不长的有关书籍和文章的列表,它们都直接或者间接地被提到过。

[参考文献列表]

关于设计和大型软件开发问题的参考文献和书籍,可以在第23章的最后找到。