您的位置:首页 > 编程语言 > C语言/C++

[读书笔记]C++语言的设计与演化[The Design and Evolution of C++]

2013-08-22 16:14 507 查看
[第1章 C++的史前时代]
C++的许多设计决策根源于我对强迫人按某种特定方式行事的极度厌恶。

在历史上,一些最坏的灾难就起因于理想主义者们试图强迫人们“做某些对他们最好的事情”。

这种理想主义不仅导致了对无辜受害者的伤害,也迷惑和腐化了施展权利的理想主义者们。

我还发现,对于与其教义或理论出现不寻常的冲突的经验和实验,理想主义者往往有忽略它们的倾向。

在理想出问题的地方,甚至当空谈家也要赞成的时候,我宁愿提供一些支持,给程序员以选择的权利。
不同的人们确实会按不同的方式思考,喜欢按不同的方式做事情,对于这些情况的高度容忍和接受是我最愿意的事情。
这样,C++被有意地设计成能够支持各种各样的风格,而不是强调“一条真理之路”。

[第2章 带类的C]
带类的C和后来的C++仍然追寻着同一条路,维持了C的低级操作不安全特性

与C不同的是,C++系统化地清除了使用这些操作的必要性,除了在那些必须使用它们的地方,而且只是在程序员明确要求时才使用不安全操作。
用户定义类型和内部类型与语法法则的关系应该是一样的,能够从语言及其相关工具方面得到同样程度的支持。
带类的C始终被作为一种就要在当时或下一个月里使用的东西,而不是一个在几年之后有可能发布某种东西的研究项目。

[第3章 C++的诞生]
在X3J16 ANSI委员会的组织会议上,Larry Rosler,原来ANSI C委员会的编辑,对抱怀疑态度的TomPlum解释说,“C++就是我们想做但却无法做成的那个C语言”。

这可能是有点夸大其词,但是对于C和C++的共同子集而言,这个说法与真理相距并不远。
一个好语言不是设计出来的,而是成长起来的。

这种修炼与工程、社会学和哲学的关系比与数学的关系更密切些。
良好设计的关键是对问题的深入认识,而不是提供了多少最高级的特征。
该书(C++程序设计语言,书生注)的开篇语是“C++是一种通用程序设计语言,其设计就是为了使认真的程序员能够觉得编程序变得更愉快了。”这句话被审阅者删掉了两次,他们拒绝相信程序语言的设计除了对生产率、管理和软件工程的那些严肃的唧咕声之外还能够有什么。

。。。

实际情况就是这样,无论审阅的人愿不愿意相信。

我把工作的注意力集中于人,个人(无论他是否在一个小组里),程序员。

[第4章 C++语言设计规则]
目标:

C++应该使认真的程序员能够觉得编程序变得更愉快

C++是一种通用的程序设计语言,它应该

——是一种更好的C

——支持数据抽象

——支持面向对象的程序设计
一般性规则:

C++的发展必须由设计问题推动

不被牵涉到无益的对完美的追求之中

C++必须现在就是有用的

每个特征必须存在一种合理的明显实现方式

总提供一条转变的通路

C++是一种语言,而不是一个完整的系统

为每种应该支持的风格提供全面支持

不试图去强迫人做什么
设计支持规则:

支持一致的设计概念

为程序的组织提供各种机制

直接说出你的意思

所有特征都必须是能够负担的

允许一个有用的特征比防止各种错误使用更重要

(例如,按照默认方式,所有的类成员都是私用的。

  当然,一个系统设计语言不应该禁止程序员有意识地去打破系统的限制,所以设计的努力

  应该更多地放在提供机制,帮助人写出好的程序方面,而不是放在禁止不可避免的坏程序方面。

  在长期的过程中程序员必然会学习。

  这种观点也是C语言传统上“相信程序员”口号的一种变形。)

支持从分别开发的部分出发进行软件的组合
语言的技术性规则:

不隐式地违反静态类型系统

为用户定义类型提供与内部类型同样好的支持

局部化是好事

避免顺序依赖性

如果有疑问,就选择有关特征的那种最容易教给人的形势

语法是重要的(常以某写我们不希望的方式起作用)

应该清除使用预处理程序的必要性

语言的技术性规则:

使用传统的(dumb)连接程序

没有无故的与C的不兼容性

(在过去这些年里,C++最强的地方和最弱的地方都在于它与C的兼容性。)

在C++下面不为更低级的语言留下空间(除汇编语言之外)

对不用的东西不需要付出代价(0开销规则)

(。。。都是到了我自己已经确性能够构造出遵守0开销原则的实现方式时,这些特征才被接受进来。)

遇到有疑问的地方就提供手工控制的手段

[第7章 关注和使用]
我把C++程序里的大部分强制转换看成是设计拙劣的标志。有些强制转换时很基本的,但大部分都不是。

按照我的经验,传统C程序员使用C++,通过Smalltalk理解OPP的C++程序员是使用强制转换最多的人,

而所用的那些种类的转换完全可以通过更仔细的设计而得以避免。

[第8章 库]
设计一个库,实际上经常能成为追求新机制狂热的一种最具建设性的发泄方式。

只有在迈向库的道路真正走不通的情况下,才应该踏上语言扩充之路。

[第9章 展望]
C++能产生运行时间和空间特性极好的代码,这种代码能与在本领域中广泛认可的领先者C语言比赛。

。。。

C++不但能从按传统方式组织的代码中产生这种性能,对基于数据抽象和面向对象技术的代码也一样。
C++使人可以将这种代码继承到常规系统里,并能在传统系统上生成。
C++允许人们逐步转移到新的程序设计技术上来。
从某种观点看,C++实际上是将三个语言和为一体

——一个类似C的语言(支持低级程序设计)

——一个类似Ada的语言(支持抽象数据类型程序设计)

——一个类似Simula的语言(支持面向对象的程序设计)

——将上述特征综合成一个有机整体所需要的东西
C++的长处,更多的在于它对许多语言都是很好的解决途径,而不在于对某个特定问题是最好的解决途径。

。。。

这样,大部分通用程序设计语言最希望做的也就是成为“每个人的第二选择”。
[第10章 存储管理]

在operator new()和operatordelete()之间有一种明显的、经过深思熟虑的不对称性。前者能够被重载而后者不能。这与建构函数和析构函数之间的不对称性也是互相匹配的。因此,在构建对象的时候你可能可以在四个分配器和五个建构函数中做选择,而当它到了被销毁的时候,则只存在着唯一的一种选择:deletep;这样做的理由是,在建构对象的那一点,在原则上你知道所有的东西,而当到了删除它的时候你剩下的不过是一个指针,这个指针可以正好是也可以不是该对象的类型。

此外,在这里并不存在一种语言特征,使得我们能像分配函数的选择机制那样去选择释放函数:问题仍然是删除的点,我们不能期望用户知道有关对象是如何分配的。当然最理想的情况是用户根本就不必去释放对象,这也是特殊分配场地的一种用途。也可以定义一种分配场地,令它在程序执行的某个特定点以整体的方式进行释放,或许还可以为某个分配场地定义一个特定的废料收集系统。

更常见的情况是编好函数operator new(),让它在对象里留下一个指示,以使对应的operatordelete()能够知道这些对象应该如何释放。请注意,这也是在做存储管理,与由建构函数建立和析构函数销毁的对象相比,这是在更低的概念层次上做。因此,包含这种信息的存储位置不应该在实际对象的内部,而应该在与之相关的某个地方。例如某个operatornew()可能把存储管理信息放在作为它返回值的指针在前面的一个字里。作为另一种方式,operatornew()也可以把信息存放到另一个位置,使建构函数和其他函数能够找到,从而确定是否已经在某个自由存储里分配了一个对象。

不允许用户对delete进行重载是不是一个错误?如果是,那么保护人们以提防他们自己就是一种误导。我无法作出决定,但是我也十分确认这是个很难对付的情况,采取任何解决办法都会引出许多问题。

Release2.0引进了显式调用析构函数的可能性,以对付某些不大常见的情况,在那里存储分配和释放是相互完全分离的。一个实际例子是某种容器,它需要为自己所包含的对象完成所有的存储分配工作。
要找到某种需要的资源而又无法得到,这是一个普遍的而又很难对付的问题。我曾经确定(在2.0之前)异常处理是一个方向,应该到那里去寻找针对这类问题的一个具有普遍意义的解决方法。但异常处理在那时还是很远的将来的事情。而自由存储耗尽这个特殊的问题是无法等待的。

立即需要解决的问题有两个:

1)在任何情况下,当一个库调用因为存储耗尽而失败时(更一般的,在任何库调用失败时),用户都应该能够取得控制。

2)普通用户不需要在每次分配操作之后去测试存储耗尽的情况。从C得到的经验告诉我们,用户一般都去做这种测试,即使是人们认为应该这样做。

对于第一个的问题的处理要求当operatornew()返回0时建构函数根本就不执行。在这种情况下,new表达式本身也应该产生0值。这就使关键性的软件在出现存储分配问题时能够保护自己。(现在C++中operatornew的缺省行为已经改为异常处理,即std::bad_alloc。operator new()nothrow保持上述行为。书生注)

满足第二个需要的方法是用一个称为new_handler的东西,它是一个由用户提供的函数,如果运算符不能找到存储,就保证去调用这个函数。这种技术已经成为处理资源需要偶然失败时被普遍采用的一种模式。简单的说,new_handler可以:

——寻找更多的资源(也就是说,去寻找更多的自由存储);或者

——产生一个错误信息,并退出(也是个办法)。

有了异常处理,“退出”可能比直接终止程序稍微平和一点。
我有意地这样设计C++,使它不依赖于自动废料收集(通常就是直接说废料收集)。这是基于自己对废料收集系统的经验,我很害怕那种严重的空间和时间开销,也害怕由于实现和移植废料收集系统而带来的复杂性。还有,废料收集将使C++不合适做许多低层次的工作,而这却是它的一个设计目标。我喜欢废料收集的思想,它是一种机制,能够简化设计、排除掉许多产生错误的根源。

我的结论是,从原则上和可行性上说,废料收集都是需要的。但是对今天的用户、普遍的使用和硬件而言,我们还无法承受将C++的语义和它的基本库定义在废料收集之上的负担。

理想情况是,带有废料收集的系统实现应该有这样一种性质:当你不使用废料收集时,它应该工作的像没有废料收集的系统一样好。

与此相反,废料收集系统的实现者可能需要从用户得到某些“提示”,以便使程序的执行达到可以接受的水平。

我想可能调制出一种废料收集模型,它能够对应(几乎)所有的合法C++程序,而对那些没有不安全操作的程序,它能够工作的更有效。

[第11章 重载]
对于表达方式灵活和自由的欲望,与对安全性、可预见性以及简单性的追求是互相冲突的。
经验说明,在函数匹配时,还应该考虑通过公用类派生建立起来的分层结构的情况,以便使存在选择的时候,到“最终的派生类”的转换能够被选中。仅在没有其他指针参数能够匹配时才会选中void*参数。(也就是说,在存在基类和派生类指针的重载函数时,从下至上离自己继承关系最近的类会被选中。书生注)
我当时总结出,我们需要某种“更好匹配”规则的概念,这将使我们认为一个精确的匹配胜过一个需要转换的匹配,一个安全转换的匹配(例如从float到double)胜过一个不安全的(缩窄的、破坏值的等等)转换(例如从float到int)。

C++区分了5种“匹配”:

1)匹配中不转换或者只使用不可避免的转换(例如,从数组名到指针,函数名到函数指针,以及T到const T)。

2)使用了整数提升的匹配(像ANSIC标准所定义的。也就是说从char到int、short到int以及它们对应的unsigned类型)以及float到double。

3)使用了标准转换的匹配(例如从int到double、derived*到base*、unsigned int到int)。

4)使用了用户定义转换的匹配(包括通过建构函数和转换操作)。

5)使用了在函数声明里的省略号...的匹配。

。。。,这里的想法是总选择‘最好的’匹配,也就是说在上面表里最高的匹配。如果存在着两个最好匹配(在上表中的同一级,书生注),这个调用就是有歧义的,将产生一个编译错误。

请注意这里对兼容性的考虑有多么重要。

。。。

如果我们所做过的那些设计决策一直系统地将简单和优雅置于兼容性之上的话,今天的C++必然会更小,也会更清晰,但它也就会是一个很不重要的异教语言。
我个人认为,将复制操作定义为默认的也是一种不幸。我对自己的许多类都禁止了复制操作。但无论如何,C++是从C那里继承来的默认赋值和复制建构函数,它们也确实经常是需要用的东西。
这种例子通常更多的是一种智力嗜好,而不是什么具有重要实际意义的技术。这可能也是为什么对于它们的讨论如此广泛的根本原因。
为什么C++不能有一个指数运算符?本来的原因就是C语言里没有。C运算符的语义被假定说应该足够简单,以使它们中的每一个在典型的计算机上都能对应于一条机器指令。指数运算符不能满足这个准则。

[第12章 多重继承]

选择普通基类作为默认情况,只是因为它们的实现比虚基类更节约运行的时间和空间。

。。。

在另一方面,我认为多重继承的那些简单而又不出奇的应用,将一个类定义为两个原本无关的类的属性之和,是最有用的东西。
就我的评价,多重继承最成功的使用具有如下几种简单的模式:

1)归并相互独立的,或者基本上互相独立的几个类层次;task和display是这方面的例子。

2)界面(interface,书生注)的组合;I/O流是这方面的例子。

3)从一个界面(interface,书生注)和一个实现综合出一个类(类似于Java的继承机制,书生注);slist_set是这方面的例子。

[第13章 类概念的精炼]
一个抽象类表示了一个界面(interface,书生注)。直接支持抽象类将能够:

——有助于捕捉由于混淆了类作为界面的角色和它们表示对象的角色而引起的错误。

——支持一种基于将界面的描述和实现区分开来的设计风格。
如常,C++关心的是检查偶然的错误,而不是防止刻意的欺骗。对我来说,这也就意味着可以允许一个函数通过“强制去掉const”来做出一个“骗局”。我们不认为编译系统有责任去防止程序员明确地做突破类型系统的事情。
这个观点意味着强制去掉一个原来就定义为const的对象的const,而后对它做写操作最好的情况就是无定义;而对一个原来并没有定义为const的对象同样做这些事情则是合法的,有清除定义的。
人们还是在不断地将各种没有必要的东西装入他们的头文件,从而受到过长编译时间的伤害。因为,每一种能够帮助减少用户和实现者之间不必要的耦合的技术都是非常重要的。
那时我很喜欢说我们是发现了指向成员的指针概念,而不是我们设计了它。对2.0的大部分东西都有这种感觉。

在很长一段时间,我都认为指向数据成员的指针是为了推广而做出来的一种人造物件,而不是什么真正有用的东西。我又一次被证明是错的。特别是实践已经证明,指向数据成员的指针是一种表达C++的类布局方式的与实现无关的方式。

[第14章 强制(转换;书生注)]
明白事理的人,不会去改变世界。 ——萧伯纳
又一次的,我对新特征辩护还是站在这样的基础上:它对于一些人是有用的,而对不使用它的另一些人则是无害的;如果我们不直接支持它,人们也会去模拟它。
运行时类型信息机制包括3个主要部分:

——一个运算符dynamic_cast,给它一个指向某对象的基类指针,它能得到一个到这个对象的派生类指针。

——一个运算符typeid,它对一个给定的基类指针识别出被指对象的确切类型。

——一个结构type_info,作为与有关类型的更多运行时类型信息的挂接点(hook)。
每个有用的特征都有可能被误用,所以,问题并不在于一种特征能不能被误用(当然能),或者说是否将被误用(当然会)。真正的问题是,对一种特征的正确应用是否充分重要,值得为提供它去花费那些精力;用语言的其他机制来模拟这种机制是不是容易处理;通过正确的教育,能不能把它的错误使用控制在合理的范围之内。

。。。

最后,在C++设计中有一条指导原则,那就是,无论做什么事情都必须相信程序员。与可能出现什么样的错误相比,更重要的多的是能做出什么好事情。C++程序员总被看作是成年人,只需要最好的“看护”。
人们希望知道对象的确切类型,通常是他们因为想对这个对象的整体执行某种标准服务。在理想的情况下,这种服务是通过虚函数提供的,这时就不必知道对象的确切类型了;但是,如果因为某些原因没有这样的函数可用,那么就必须找到对象的确切类型,而后再执行有关的操作。

。。。

在这些情况下,根本无法假定对每种对象的操作能够有一个公共的界面,所以只能另辟蹊径,利用对象的确切类型就变得不可避免了。另一种更简单的使用是取得类的名字,以便产生某些诊断输出。
当然,对于所有情况,最好是所有的强制——无论是老的新的——都能删除掉。

[第15章 模板]
在我的心里,模板和异常是一个硬币的两面:模板通过扩展静态类型检查所能处理的问题的范围,能够减少运行时错误出现的数量;异常就是为处理这些错误(运行期错误,书生注)而提供的一种机制。
在早期的C++里没有模板,这对C++的使用方式产生了重要的负面影响。现在模板已经有了,我们可能把哪些事情做得更好一些呢?

因为缺乏模板,在C++里就无法实现容器类,除非是广泛地使用强制(转型,书生注),并且总通过通用基类的指针或者void*去操作各种对象。原则上说,这些现在都可以删去了。但是我想,无论如何,由于误导,将Smalltalk技术应用在C++里而引起的对继承的错误使用,来源于C语言的弱类型技术的过度使用,都是极难根除的。

在另一方面,我也希望能慢慢地摆脱掉设计数组的许多不安全的实践。...人们经常批评C和C++不检查数组的边界。这种批评中的大多数是一种误导,因为人们忘记了,这只是说你能够使C数组出现越界错误,而不是说你必须这样做。...而模板正在加速这种趋势,特别是库里定义的模板。

模板的第三个重要方面在于它们为库的设计打开了许多全新的可能性,可以使派生和组合相结合。长远看这可能成为最重要的一个方面。
对于一些人来说,问题是如何保证对于需要保持秘密的模板,用户不能(无论直接还是间接地)通过实例化产生出新的版本。这可以通过不提供源代码的方式去保证。只要提供商能够预先通过实例化(显式具现化,书生注)产生所有需要的版本,这个方法就可行。这些版本(也只有这些版本)被作为目标代码库发布。
一个模板参数可以是一个内部类型,也可以是一个用户定义类型。这就产生了一种持续的压力,要求用户定义类型无论是在外观还是在行为上都要尽可能与内部类型相仿。...这个努力一直继续到今天。...为了允许对内部类型采用这种初始化形式,我为内部类型也引进了建构函数和析构函数的概念。例如:inta(1);我考虑过进一步扩充这种概念,允许从内部类型出发做派生,允许为内部类型写内部运算符的显式声明。但是我撤回来了。

[第16章 异常处理]
理想的情况是,异常刻画应该在编译时进行检查,但是这就要求每个函数都必须与这个模式合作,而这又是不可行的。进一步说,这种静态检查很容易变成许多重新编译的根源。更糟糕的是,这种重新编译只有在用户掌握着所有需要重新编译的源代码时才可能进行。...这样,我们就决定只支持实时检查,将静态检查的问题留给另外的工具去做。

[第17章 名字空间]
按照个人观点,我认为使用指示(比如using namespacestd;,书生注)基本上是一种转变的工具。如果需要引用来自其它名字空间的名字,通过用显式量化(比如std::cout<< "Hello";,书生注)和使用声明(比如usingstd::cout;,书生注)可以把大部分程序表达得更清晰。
使用声明是向局部作用域里添加东西。而使用指示并不添加任何东西,它只是使一些名字能够被访问。
引进名字空间之后,全局作用域就变成了另一个名字空间。全局名字空间的独特之处仅在于你不必在显式量化形式中写出它的名字。::f的意思是“在全局作用域里声明的那个f”,而X::f意味着“在名字空间X里声明的那个f”。也就是说,用::量化意味着“全局”,而不是“外面包裹的最近名字空间”。
我期望能够看到全局名字的使用急剧减少。有关名字空间的规则都特别做了加工,以保证,与那些细心地防止污染全局空间的人们相比,随意使用全局名字的“懒散”用户将不可能得到更多的利益。
这样,名字空间的概念就使我们可以抑制static在控制全局名字的可见性方面的使用。这就会使static在C++里只剩下一个意义:静态地分配,且不重复。...在一个作用域里的各个匿名名字空间都共享同样的唯一名字。特别是在一个编译单元中所有全局的匿名名字空间都是同一个名字空间的一部分,而它们又与其它编译单元的匿名名字空间不同。

[第18章 C语言预处理器]
这个预处理程序具有字符和文件的本性,这些从根本上说与一个围绕着作用于、类型和界面(interface,书生注)等概念设计出来的程序设计语言是格格不入的。
C++为#define的主要应用提供了如下的替代方式:

——const用于常量。

——inline用于开子程序。

——template用于以类型为参数的函数。

——template用于参数化类型。

——namespace用于更一般的命名问题。

C++没有为#include提供替代形式。

没有#progma我也能活,因为我还没有见过一个自己喜欢的#progma。看起来#progma被过分经常地用于将语言语义的变形隐藏到编译系统里,或者被用于提供带有特殊语义和笨拙语法的语言扩充。

我们至今还没有#indef的很好的替代品。特别是用if语句和常量表达式还不足以替代它。
我很愿意看到Cpp(C语言预处理器,书生注)被废除。但无论如何,要想做到这件事,唯一现实的和负责任的方式就是首先使它成为多余的,而后鼓励人们去使用那些更好的替代品。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 读书笔记