logo资料库

Thinking In C++中文版pdf.pdf

第1页 / 共421页
第2页 / 共421页
第3页 / 共421页
第4页 / 共421页
第5页 / 共421页
第6页 / 共421页
第7页 / 共421页
第8页 / 共421页
资料共421页,剩余部分请下载后查看
第1 章对象的演化
1.1 基本概念
1.1.1 对象:特性+行为 [1]
1.1.2 继承:类型关系
1.1.3 多态性
1.1.4 操作概念:OOP 程序像什么
1.2 为什么C++会成功
1.2.1 较好的C
1.2.2 采用渐进的学习方式
1.2.3 运行效率
1.2.4 系统更容易表达和理解
1.2.5 “库”使你事半功倍
1.2.6 错误处理
1.2.7 大程序设计
1.3 方法学介绍
1.3.1 复杂性
1.3.2 内部原则
1.3.3 外部原则
1.3.4 对象设计的五个阶段
1.3.5 方法承诺什么
1.3.6 方法应当提供什么
1.4 起草:最小的方法
1.4.1 前提
1.4.2 高概念
1.4.3 论述(treatment)
1.4.4 结构化
1.4.5 开发
1.4.6 重写
1.4.7 逻辑
1.5 其他方法
1.5.1 Booch
1.5.2 责任驱动的设计(RDD )
1.5.3 对象建模技术(OMT )
1.6 为向OOP 转变而采取的策略
1.6.1 逐步进入OOP
1.6.2 管理障碍
1.7 小结
第2 章数据抽象
2.1 声明与定义
2.2 一个袖珍C 库
2.3 放在一起:项目创建工具
2.4 什么是非正常
2.5 基本对象
2.6 什么是对象
2.7 抽象数据类型
2.8 对象细节
2.9 头文件形式
2.10 嵌套结构
2.11 小结
2.12 练习
第3 章隐藏实现
3.1 设置限制
3.2 C++的存取控制
3.3 友元
3.3.1 嵌套友元
3.3.2 它是纯的吗
3.4 对象布局
3.5 类
3.5.1 用存取控制来修改stash
3.5.2 用存取控制来修改stack
3.6 句柄类(handle classes )
3.6.1 可见的实现部分
3.6.2 减少重复编译
3.7 小结
3.8 练习
第4 章初始化与清除
4.1 用构造函数确保初始化
4.2 用析构函数确保清除
4.3 清除定义块
4.3.1 for 循环
4.3.2 空间分配
4.4 含有构造函数和析构函数的stash
4.5 含有构造函数和析构函数的stack
4.6 集合初始化
4.7 缺省构造函数
4.8 小结
4.9 练习
第5 章函数重载与缺省参数
5.1 范围分解
5.1.1 用返回值重载
5.1.2 安全类型连接
5.2 重载的例子
5.3 缺省参数
5.4 小结
5.5 练习
第6 章输入输出流介绍
6.1 为什么要用输入输出流
6.2 解决输入输出流问题
6.2.1 预先了解操作符重载
6.2.2 插入符与提取符
6.2.3 通常用法
6.2.4 面向行的输入
6.3 文件输入输出流
6.4 输入输出流缓冲
6.5 在输入输出流中查找
6.6 strstreams
6.6.1 为用户分配的存储
6.6.2 自动存储分配
6.7 输出流格式化
6.7.1 内部格式化数据
6.7.2 例子
6.8 格式化操纵算子
6.9 建立操纵算子
6.10 输入输出流实例
6.10.1 代码生成
6.10.2 一个简单的数据记录
6.11 小结
6.12 练习
第7 章常量
7.1 值替代
7.1.1 头文件里的const
7.1.2 const 的安全性
7.1.3 集合
7.1.4 与C 语言的区别
7.2 指针
7.2.1 指向const 的指针
7.2.2 const 指针
7.2.3 赋值和类型检查
7.3 函数参数和返回值
7.3.1 传递const 值
7.3.2 返回const 值
7.3.3 传递和返回地址
7.4 类
7.4.1 类里的const 和enum
7.4.2 编译期间类里的常量
7.4.3 const 对象和成员函数
7.4.4 只读存储能力
7.5 可变的(volatile )
7.6 小结
7.7 练习
第8 章内联函数
8.1 预处理器的缺陷
8.2 内联函数
8.2.1 类内部的内联函数
8.2.2 存取函数
8.3 内联函数和编译器
8.3.1 局限性
8.3.2 赋值顺序
8.3.3 在构造函数和析构函数里隐藏行为
8.4 减少混乱
8.5 预处理器的特点
8.6 改进的错误检查
8.7 小结
8.8 练习
第9 章命名控制
9.1 来自C 语言中的静态成员
9.1.1 函数内部的静态变量
9.1.2 控制连接
9.1.3 其他的存储类型指定符
9.2 名字空间
9.2.1 产生一个名字空间
9.2.2 使用名字空间
9.3 C++中的静态成员
9.3.1 定义静态数据成员的存储
9.3.2 嵌套类和局部类
9.3.3 静态成员函数
9.4 静态初始化的依赖因素
9.5 转换连接指定
9.6 小结
9.7 练习
第1 0 章引用和拷贝构造函数
10.1 C++中的指针
10.2 C++中的引用
10.2.1 函数中的引用
10.2.2 参数传递准则
10.3 拷贝构造函数
10.3.1 传值方式传递和返回
10.3.2 拷贝构造函数
10.3.3 缺省拷贝构造函数
10.3.4 拷贝构造函数方法的选择
10.4 指向成员的指针(简称成员指针)
10.5 小结
10.6 练习
第11 章运算符重载
11.1 警告和确信
11.2 语法
11.3 可重载的运算符
11.3.1 一元运算符
11.3.2 二元运算符
11.3.3 参数和返回值
11.3.4 与众不同的运算符
11.3.5 不能重载的运算符
11.4 非成员运算符
11.5 重载赋值符
11.6 自动类型转换
11.6.1 构造函数转换
11.6.2 运算符转换
11.6.3 一个理想的例子:strings
11.6.4 自动类型转换的缺陷
11.7 小结
11.8 练习
第1 2 章动态对象创建
12.1 对象创建
12.1.1 C 从堆中获取存储单元的方法
12.1.2 运算符new
12.1.3 运算符delete
12.1.4 一个简单的例子
12.1.5 内存管理的开销
12.2 重新设计前面的例子
12.2.1 仅从堆中创建string 类
12.2.2 stash 指针
12.2.3 stack 例子
12.3 用于数组的new 和delete
12.4 用完内存
12.5 重载new 和delete
12.5.1 重载全局new 和delete
12.5.2 为一个类重载new 和delete
12.5.3 为数组重载new 和delete
12.5.4 构造函数调用
12.5.5 对象放置
12.6 小结
12.7 练习
第1 3 章继承和组合
13.1 组合语法
13.2 继承语法
13.3 构造函数的初始化表达式表
13.3.1 成员对象初始化
13.3.2 在初始化表达式表中的内置类型
13.4 组合和继承的联合
13.4.1 构造函数和析构函数的次序
13.4.2 名字隐藏
13.4.3 非自动继承的函数
13.5 组合与继承的选择
13.5.1 子类型设置
13.5.2 专门化
13.5.3 私有继承
13.6 保护
13.7 多重继承
13.8 渐增式开发
13.9 向上映射
13.9.1 为什么“向上映射”
13.9.2 组合与继承
13.9.3 指针和引用的向上映射
13.9.4 危机
13.10 小结
13.11 练习
第1 4 章多态和虚函数
14.1 向上映射
14.2 问题
14.3 虚函数
14.4 C++如何实现晚捆绑
14.4.1 存放类型信息
14.4.2 对虚函数作图
14.4.3 撩开面纱
14.4.4 安装vpointer
14.4.5 对象是不同的
14.5 为什么需要虚函数
14.6 抽象基类和纯虚函数
14.7 继承和VTABLE
14.8 虚函数和构造函数
14.8.1 构造函数调用次序
14.8.2 虚函数在构造函数中的行为
14.9 析构函数和虚拟析构函数
14.10 小结
14.11 练习
第1 5 章模板和包容器类
15.1 包容器和循环子
15.2 模板综述
15.2.1 C 方法
15.2.2 Smalltalk 方法
15.2.3 模板方法
15.3 模板的语法
15.3.1 非内联函数定义
15.3.2 栈模板(the stack as a template)
15.3.3 模板中的常量
15.4 stash & stack 模板
15.4.1 所有权问题
15.4.2 stash 模板
15.4.3 stack 模板
15.5 字符串和整型
15.5.1 栈上的字符串
15.5.2 整型
15.6 向量
15.6.1 “无穷”向量
15.6.2 集合
15.6.3 关联数组
15.7 模板和继承
15.7.1 设计和效率
15.7.2 防止模板膨胀
15.8 多态性和包容器
15.9 包容器类型
15.10 函数模板
15.10.1 存储分配系统
15.10.2 为tstack 提供函数
15.10.3 成员函数模板
15.11 控制实例
15.12 小结
15.13 练习
第1 6 章多重继承
16.1 概述
16.2 子对象重叠
16.3 向上映射的二义性
16.4 虚基类
16.4.1 “最晚辈派生”类和虚基初始化
16.4.2 使用缺省构造函数向虚基“警告”
16.5 开销
16.6 向上映射
16.7 避免MI
16.8 修复接口
16.9 小结
16.10 练习
第1 7 章异常处理
17.1 C 语言的出错处理
17.2 抛出异常
17.3 异常捕获
17.3.1 try 块
17.3.2 异常处理器
17.3.3 异常规格说明
17.3.4 更好的异常规格说明
17.3.5 捕获所有异常
17.3.6 异常的重新抛出
17.3.7 未被捕获的异常
17.4 清除
17.5 构造函数
17.6 异常匹配
17.7 标准异常
17.8 含有异常的程序设计
17.8.1 何时避免异常
17.8.2 异常的典型使用
17.9 开销
17.10 小结
17.11 练习
第1 8 章运行时类型识别
18.1 例子—shape
18.2 什么是RTTI
18.3 语法细节
18.3.1 对于内部类型的typeid()
18.3.2 产生合适的类型名字
18.3.3 非多态类型
18.3.4 映射到中间级
18.3.5 void 指针
18.3.6 用模板来使用RTTI
18.4 引用
18.5 多重继承
18.6 合理使用RTTI
18.7 RTTI 的机制及花费
18.8 创建我们自己的RTTI
18.9 新的映射语法
18.9.1 static_cast
18.9.2 const_cast
18.9.3 reinterpret_cast
18.10 小结
18.11 练习
附录A 其他性能
A.1 bool 、true 、false
A.2 新的包含格式
A.3 标准C++库
A.4 标准模板库(STL )
A.5 asm 关键字
A.6 明确的运算符
附录B 编程准则
附录C 模拟虚构造函数
C.1 全功能的虚构造函数
C.2 更简单的选择
下载 第1章 对象的演化 计算机革命起源于一台机器,程序设计语言也源于一台机器。 然而计算机并不仅仅是一台机器,它是心智放大器和另一种有表述能力的媒体。这一点 使它不很像机器,而更像我们大脑的一部分,更像其他有表述能力的手段,例如写作、绘画、 雕刻、动画制作或电影制作。面向对象的程序设计是计算机向有表述能力的媒体发展中的一 部分。 本章将介绍面向对象程序设计( O O P)的基本概念,然后讨论 O O P开发方法,最后介绍使 程序员、项目和公司使用面向对象程序设计方法而采用的策略。 本章是一些背景材料,如果读者急于学习这门语言的具体内容,可以跳到第 2章,然后再 回过头来学习本章。 1.1 基本概念 C + +包含了比面向对象程序设计基本概念更多的内容,读者应当在学习设计和开发程序之 前先理解该语言所包含的基本概念。 1.1.1 对象:特性+行为[1] 第一个面向对象的程序设计语言是 6 0年代开发的 S i m u l a - 6 7。其目的是为了解决模拟问题。 典型的模拟问题是银行出纳业务,包括出纳部门、顾客、业务、货币的单位等大量的“对象”。 把那些在程序执行期间除了状态之外其他方面都一样的对象归在一起,构成对象的“类”,这 就是“类”一词的来源。 类描述了一组有相同特性(数据元素)和相同行为(函数)的对象。类实际上就是数据类 型,例如,浮点数也有一组特性和行为。区别在于程序员定义类是为了与具体问题相适应,而 不是被迫使用已存在的数据类型。这些已存在的数据类型的设计动机仅仅是为了描述机器的存 储单元。程序员可以通过增添他所需要的新数据类型来扩展这个程序设计语言。该程序设计系 统欢迎创建、关注新的类,对它们进行与内部类型一样的类型检查。 这种方法并不限于去模拟具体问题。尽管不是所有的人都同意,但大部分人相信,任何程 序都模拟所设计系统。 O O P技术能很容易地将大量问题归纳成为一个简单的解,这一发现产生 了大量的O O P语言,其中最著名的是S m a l l t a l k—C++ 之前最成功的O O P语言。 抽象数据类型的创建是面向对象程序设计中的一个基本概念。抽象数据类型几乎能像内部类 型一样准确工作。程序员可以创建类型的变量(在面向对象程序设计中称为“对象”或“实例”) 并操纵这些变量(称为发送“消息”或“请求”,对象根据发来的消息知道需要做什么事情)。 1.1.2 继承:类型关系 类型不仅仅说明一组对象上的约束,还说明与其他类型之间的关系。两个类型可以有共同 的特性和行为,但是,一个类型可能包括比另一个类型更多的特性,也可以处理更多的消息 [1] 这一描述部分引自我对《The Tao of Objects》(Gary Entsminger著)一书的介绍。
2 C + +编程思想 下载 (或对消息进行不同的处理)。继承表示了基本类型和派生类型之间的相似性。一个基本类型具 有所有由它派生出来的类型所共有的特性和行为。程序员创建一个基本类型以描述系统中一些 对象的思想核心。由这个基本类型派生出其他类型,表达了认识该核心的不同途径。 例如,垃圾再生机要对垃圾进行分类。这里基本类型是“垃圾”, 每件垃圾有重量、价值 等等,并且可以被破碎、融化或分解。这样,可以派生出更特殊的垃圾类型,它们可以有另外 的特性(瓶子有颜色)或行为(铝可以被压碎,钢可以被磁化)。另外,有些行为可以不同 (纸的价值取决于它的种类和状态)。程序员可以用继承建立类的层次结构,在该层次结构中用 类型术语来表述他需要解决的问题。 第二个例子是经典的形体问题,可以用于计算机辅助设计系统或游戏模拟中。这里基本类 型是“形体”,每个形体有大小、颜色、位置等。每个形体能被绘制、擦除、移动、着色等。 由此,可以派生出特殊类型的形体:圆、正方形、三角形等,它们中的每一个都有另外的特性 和行为,例如,某些形体可以翻转。有些行为可以不同(计算形体的面积)。类型层次结构既 体现了形体间的类似,又体现了它们之间的区别。 用与问题相同的术语描述问题的解是非常有益的,这样,从问题描述到解的描述之间就不 需要很多中间模型(程序语言解决大型问题,就需要中间模型)。面向对象之前的语言,描述 问题的解不可避免地要用计算机术语。使用对象术语,类型层次结构是主要模型,所以可以从 现实世界中的系统描述直接进入代码中的系统描述。实际上,使用面向对象设计,人们的困难 之一是从开始到结束过于简单。一个已经习惯于寻找复杂解的、训练有素的头脑,往往会被问 题的简单性难住。 1.1.3 多态性 当处理类型层次结构时,程序员常常希望不把对象看作是某一特殊类型的成员,而把它看 作基本类型成员,这样就可以编写不依赖于特殊类型的代码。在形体例子中,函数可以对一般 形体进行操作,而不关心它们是圆、正方形还是三角形。所有的形体都能被绘制、擦除和移动, 所以这些函数能简单地发送消息给一个形体对象,而不考虑这个对象如何处理这个消息。 这样,新添类型不影响原来的代码,这是扩展面向对象程序以处理新情况的最普通的方法。 例如,可以派生出形体的一个新的子类,称为五边形,而不必修改那些处理一般形体的函数。 通过派生新子类,很容易扩展程序,这个能力很重要,因为它极大地减少了软件维护的花费。 (所谓“软件危机”正是由软件的实际花费远远超出人们的想象而产生的。) 如果试图把派生类型的对象看作它们的基本类型(圆看作形体,自行车看作车辆,鸬鹚看作 鸟),就有一个问题:如果一个函数告诉一个一般形体去绘制它自己,或者告诉一个一般的车辆去 行驶,或者告诉一只一般的鸟去飞,则编译器在编译时就不能确切地知道应当执行哪段代码。同 样的问题是,消息发送时,程序员并不想知道将执行哪段代码。绘图函数能等同地应用于圆、正 方形或三角形,对象根据它的特殊类型来执行合适的代码。如果增加一个新的子类,不用修改函 数调用,就可以执行不同的代码。编译器不能确切地知道执行哪段代码,那么它应该怎么办呢? 在面向对象的程序设计中,答案是巧妙的。编译器并不做传统意义上的函数调用。由非 O O P编译器产生的函数调用会引起与被调用代码的“早捆绑”,对于这一术语,读者可能还没 有听说过,因为从来没有想到过它。早捆绑意味着编译器对特定的函数名产生调用,而连接器 确定调用执行代码的绝对地址。对于 O O P,在程序运行之前,编译器不确定执行代码的地址, 所以,当消息发送给一般对象时,需要采用其他的方案。 为了解决这一问题,面向对象语言采用“晚捆绑”的思想。当给对象发送消息时,在程序
下载 第1章 对象的演化 3 运行之前不去确定被调用的代码。编译器保证这个被调用的函数存在,并完成参数和返回值的 类型检查,但是它不知道将执行的准确代码。 为了实现晚捆绑,编译器在真正调用的地方插入一段特殊的二进制代码。通过使用存放在 对象自身中的信息,这段代码在运行时计算被调用函数的地址(这一问题将在第 1 4章中详细介 绍)。这样,每个对象就能根据一个指针的内容有不同的行为。当一个对象接收到消息时,它 根据这个消息判断应当做什么。 程序员可以用关键字 v i r t u a l表明他希望某个函数有晚捆绑的灵活性,而并不需要懂得 v i r t u a l的使用机制。没有它,就不能用 C + +做面向对象的程序设计。 Vi r t u a l函数(虚函数)表 示允许在相同家族中的类有不同的行为。这些不同是引起多态行为的原因。 1.1.4 操作概念:OOP程序像什么 我们已经知道,用C 语言编写的过程程序就是一些数据定义和函数调用。要理解这种程序 的含义,程序员必须掌握函数调用和函数实现的本身。这就是过程程序需要中间表示的原因。 中间表示容易引起混淆,因为中间表示的表述是原始的,更偏向于计算机,而不偏向于所解决 的问题。 因为 C++ 向 C 语言增加了许多新概念,所以程序员很自然地认为, C + +程序中的m a i n ( )会 比功能相同的 C 程序更复杂。但令人吃惊的是,一个写得很好的 C + +程序一般要比功能相同的 C程序更简单和容易理解。程序员只会看到一些描述问题空间对象的定义(而不是计算机的描 述),发送给这些对象的消息。这些消息表示了在这个空间的活动。面向对象程序设计的优点 之一是通过阅读,很容易理解代码。通常,面向对象程序需要较少的代码,因为问题中的许多 部分都可以用已存在的库代码。 1.2 为什么C++会成功 C + +能够如此成功,部分原因是它的目标不只是为了将 C语言转变成 O O P语言(虽然这是 最初的目的),而且还为了解决当今程序员,特别是那些在 C语言中已经大量投资的程序员所 面临的许多问题。人们已经对 O O P语言有了这样传统的看法:程序员应当抛弃所知道的每件事 情并且从一组新概念和新文法重新开始,他应当相信,最好丢掉所有来自过程语言的老行装。 从长远角度看,这是对的。但从短期角度看,这些行装还是有价值的。最有价值的可能不是那 些已存在的代码库(给出合适的工具,可以转变它),而是已存在的头脑库。作为一个职业 C 程序员,如果让他丢掉他知道的关于 C的每一件事,以适应新的语言,那么,几个月内,他将 毫无成果,直到他的头脑适应了这一新范例为止。如果他能调整已有的 C知识,并在这个基础 上扩展,那么他就可以继续保持高效率,带着已有的知识,进入面向对象程序设计的世界。因 为每个人有他自己的程序设计模型,所以这个转变是很混乱的。因此, C + +成功的原因是经济 上的:转变到O O P需要代价,而转变到 C + +所花的代价较小。 C + +的目的是提高效率。效率取决于很多东西,而语言是为了尽可能地帮助使用者,尽可 能不用武断的规则或特殊的性能妨碍使用者。 C + +成功是因为它立足于实际:尽可能地为程序 员提供最大便利。 1.2.1 较好的C 即便程序员在 C + +环境下继续写 C代码,也能直接得到好处,因为 C + +堵塞了C语言中的一
4 C + +编程思想 下载 些漏洞,并提供更好的类型检查和编译时的分析。程序员必须先说明函数,使编译器能检查它 们的使用情况。预处理器虚拟删除值替换和宏,这就减少了查找疵点的困难。 C + +有一个性能, 称为r e f e r e n c e s (引用),它允许对函数参数和返回值的地址进行更方便的处理。函数重载改进了 对名字的处理,使程序员能对不同的函数使用相同的名字。另外,名字空间也加强了名字的控 制。许多性能使 C的更安全。 1.2.2 采用渐进的学习方式 与学习新语言有关的问题是效率的问题。所有公司都不可避免地因软件工程师学习新语言 而突然降低了效率。 C + +是对C的扩充,而不是新的文法和新的程序设计模型。程序员学习和 理解这些性能,逐渐应用并继续创建有用的代码。这是 C + +成功的最重要的原因之一。 另外,已有的 C代码在 C + +中仍然是有用的,但因为 C + +编译器更严格,所以,重新编译 这些代码时,常常会发现隐藏的错误。 1.2.3 运行效率 有时,以程序执行速度换取程序员的效率是值得的。假如一个金融模型仅在短期内有用, 那么快速创建这个模型比所写程序能更快速执行重要。很多应用程序都要求有一定的运行效率, 所以C + +在更高运行效率时总是犯错。 C程序员非常重视运行效率,这让他们认为这个语言不 太庞大,也不太慢。产生的代码运行效率不够时,程序员可以用 C + +的一些性能做一些调整。 C + +不仅有与C相同的基本控制能力(和 C + +程序中直接写汇编语言的能力),非正式的证 据指出,面向对象的C + +程序的速度与用C写的程序速度相差在± 1 0 %之内,而且常常更接近。 用O O P方法设计的程序可能比C的对应版本更有效。 1.2.4 系统更容易表达和理解 为适合于某问题而设计的类当然能更好地表达这个问题。这意味着写代码时,程序员是在 用问题空间的术语描述问题的解(例如“把锁链放在箱子里”),而不是用计算机的术语,也就 是解空间的术语,描述问题的解(例如“设置芯片的一位即合上继电器”)。程序员所涉及的是 较高层的概念,一行代码能做更多的事情。 易于表达所带来的另一个好处是易于维护。据报道,在程序的整个生命周期中,维护占了 花费的很大部分。如果程序容易理解,那么它就更容易维护,还能减少创建和维护文档的花 费。 1.2.5 “库”使你事半功倍 创建程序的最快方法是使用已经写好的代码:库。 C + +的主要目标是让程序员能更容易地 使用库,这是通过将库转换为新数据类型(类)来完成的。引入一个库,就是向该语言增加一 个新类型。编译器负责这个库如何使用,保证适当的初始化和清除,保证函数被正确地调用, 因此程序员的精力可以集中在他想要这个库做什么,而不是如何做上。 因为程序的各部分之间名字是隔离的,所以程序员想用多少库就用多少库,不会有像 C语 言那样的名字冲突。 • 模板的源代码重用 一些重要的类型要求修改源代码以便有效地重用。模板可以自动完成对代码的修改,因而
下载 第1章 对象的演化 5 是重用库代码特别有用的工具。用模板设计的类型很容易与其他类型一起工作。因为模板对程 序员隐藏了这类代码重用的复杂性,所以特别好用。 1.2.6 错误处理 在C语言中,错误处理声名狼藉。程序员常常忽视它们,对它们束手无策。如果正在建大 而复杂的程序,没有什么比让错误隐藏在某处,且不能指出它来自何处更糟的了。 C + +的异常 处理(见第1 7章的内容)保证能检查到错误并进行处理。 1.2.7 大程序设计 许多传统语言对程序的规模和复杂性有自身的限制。例如, B A S I C对于某些类型的问题能 很快解决,但是如果这个程序有几页纸长,或者超出该语言的正常解题范围,那么它可能永远 算不出结果。 C语言同样有这样的限制,例如当程序超过 50 000行时,名字冲突就开始成为问 题。简言之,程序员用光了函数和变量名。另一个特别糟糕的问题是如果 C语言中存在一些小 漏洞—错误藏在大程序中,要找出它们是极其困难的。 没有清楚的文字告诉程序员,什么时候他的语言会失效,即便有,他也会忽视它们。他不 说“我的 B A S I C程序太大,我必须用 C重写”,而是试图硬塞进另外几行,增加额外的性能。 所以额外的花费就悄悄增加了。 设计C + +的目的是为了辅助大程序设计,也就是说,去掉小程序和大程序之间复杂性的分 界。当程序员写h e l l o - w o r l d类实用程序时,他确实不需要用O O P、模板、名字空间和异常处理, 但当他需要的时候,这些性能就有用了。而且,编译器在排除错误方面,对于小程序和大程序 一样有效。 1.3 方法学介绍 所谓方法学是指一组过程和启发式,用以减少程序设计问题的复杂性。在 O O P中,方法学 是一个有许多实践的领域。因此,在程序员考虑采用某一方法之前,了解该方法将要解决的问 题是很重要的。对于 C + +,有一点是确实的:它本身就是希望减少程序表达的复杂性。从而不 必用更复杂方法学。对于用过程语言的简单方法所不能处理的大型问题,在 C + +中用一些简单 的方法就足够了。 认识到“方法学”一词含义太广是很重要的。实际上,设计和编写程序时,无论做什么都 在使用一种方法。只不过因为它是程序员自己的方法而没有意识到。但是,它是程序员编程中 的一个过程。如果过程是有效的,只需要用 C + +做很小的调整。如果程序员对他的效率和调整 程序的方法不满意,他可以考虑采用一种更正式的方法。 1.3.1 复杂性 为了分析复杂性,先假设:程序设计制定原则来对付复杂性。 原则以两种方式出现,每种方式都被单独检查。 1) 内部原则体现在程序自身的结构中,机灵而有见解的程序员可以通过程序设计语言的表 达方式了解这种内部原则。 2) 外部原则体现在程序的源信息中,一般被描述为“设计文档”(不要与产品文档混淆)。 我认为,这两种形式的原则互相不一致:一个是程序的本质,是为了让程序能工作而产生
6 C + +编程思想 下载 的,另一个是程序的分析,为了将来理解和维护程序而产生的。创建和维护都是程序生命期的 基本组成部分。有用的程序设计方法把两者综合为最合适的方式,而不偏向任何一方。 1.3.2 内部原则 程序设计的演化( C + +只是其中的一步)从程序设计模型强加于内部开始,也就是允许程 序员为内存位置和机器指令取别名。这是数字机器程序设计的一次飞跃,带动了其他方面的发 展,包括从初级机器中抽象出来,向更方便地解决手边问题的模型发展。不是所有这些发展都 能流行,起源于学术界并延伸进计算机世界的思想常常依赖于所适应的问题。 命名子程序的创建和支持子程序库的连接技术在 5 0年代向前飞跃发展,并且孕育出了两个 语言,它们在当时产生了巨大冲击,这就是为科学工作者使用的 F O RT R A N (F O R m u l a - T R A N s l a t i o n)和为商业者使用的 C O B O L(COmmon Business-Oriented Language)。纯计算机 科学中很成功的语言是L i s p(L i s t - P r o c e s s i n g),而面向数学的语言应当是A P L(A Programming L a n g u a g e)。 这些语言的共同特点是对过程的使用。 L i s p和A P L的创造专注于语言的高雅—语言的 “m i s s i o n语句”嵌入在处理所有任务情况的引擎中。 F O RT R A N和C O B O L的创造是为了解决专 门的问题,当这些问题变得更复杂,有新的问题出现时,它们又得到了发展。甚至它们进入衰 退期后,仍在发展:F O RT R A N和C O B O L的版本都面向对象进行了扩充(后时髦哲学的基本原 则是:任何具有自己独特生活方式的组织,其主要目标就是使这种生活方式永存)。 命名子程序在程序设计中起了重要作用,语言的设计围绕着这一原则,特别是 A l g o l和 P a s c a l。同时另外一些语言也出现了,它们成功地解决了程序设计的一些子集问题,并将它们 有序排列。最有趣的两个语言是 P r o l o g和F O RT H。前者是围绕着推理机而建立的(在其他语言 中常常称作库)。后者是一个可扩充语言,允许程序员重新形成这个语言,以适应所解决的问 题,观念上类似于面向对象程序设计。 F O RT H还可以改变语言,因而很难维护,并且是内部 原则概念最纯正的表达,它强调的是问题一时的解,而不是对这个解的维护。 人们还创造了其他许多语言,以解决某一部分的程序设计问题。通常,这些语言以特定的 目标开始。例如,B A S I C(Beginners All-purpose Symbolic Instruction Code)是在6 0年代设计 的,目的是使程序设计对初学者更简单。 A P L的设计是为了数学处理。两种语言都能够解决其 他问题,而关键在于它们是否是这些问题集合最理想的解。有一句笑话是,“带着锤子三年, 看什么都是钉子”。这反映了根本的经济学真理:如果我们只有 B A S I C或A P L语言,特别是, 当最终期限很短且这个解的生命期有限时,它就是我们问题最好的解。 然而,最终考虑两个因素:复杂性的管理和维护(将在下一部分讨论)。即这种语言首先 是为某一领域开发的,而程序员又不愿花很长时间来熟悉这门语言,其结果只能使程序越来越 长,使手头的问题屈服于语言。界限是模糊的:谁能说什么时候您的语言会使您失望呢?这不 是马上就出现的。 问题的解开始变长,并且对于程序员更具挑战性。为了知道语言大概的限制,你得更聪明, 这种聪明变成了一种标准,也就是“为了使该语言工作而努力”。这似乎是人类的操作方式, 而不是遇到缺陷就抱怨,并且不再称它为缺陷。 最终,程序设计问题对于求解和维护变得太困难了,即求得的解太昂贵了。人们最终明白 了,程序的复杂性超出了我们能够处理的程度。尽管一大类程序设计要求开发期间去做大部分 工作并创建要求最小维护的解(或者简单地丢掉这个解,或者用不同的解替换它),但这只是 问题的一部分。一般情况是,我们把软件看作是为人们提供服务的工具。如果用户的需要变化
下载 第1章 对象的演化 7 了,服务就必须随着变化。这样,当第一版本开始运行时,项目并没有结束。项目是一个不断 进化的生命体。程序的更新变成了一般程序设计问题的一个部分。 1.3.3 外部原则 为了更新和改善程序,需要更新思考问题的方法。它不只是“我们如何让程序工作”,而 是“我们如何让程序工作并且使它容易改变”。这里就有一个新问题:当我们只是试图让程序 工作时,我们可以假设开发组是稳定的(总之,我们可以希望这样),但是,如果我们正在考 虑程序的整个生命期,就必须假设开发组成员会改变。这意味着,新组员必须以某种方式学习 原程序的要点,并与老组员互相通讯(也许通过对话)。这样,该程序就需要某种形式的设计 文档。 因为只想让程序工作,文档并不是必需的,所以还没有像由程序设计语言强加于程序那样 的、强加于创建文档的规则。这样,如果要求文档满足特定的需要,就必须对文档强加外部原 则。文档是否“工作”,这很难确定(并且需要在程序一生中验证),因此,对外部原则“最好” 形式的争论.比对“最好”程序设计语言的争论更激烈。 决定外部原则时,头脑中的重要问题是“我准备解决什么问题”。问题的根本就是上面所 说的“我们如何让它工作和使它容易改变”。然而,这个问题常常有多种解释:它变成了“我 如何才能与 F o o b l e B l a h文档规范说明一致,以使政府会为此给我拨款”。这样,外部原则的目 的是为了建立文档,而不是为了设计好的、可维护的程序。文档竟然变得比程序本身更重要 了。 被问到未来一般和特殊的计算的方向时,我会从这样的问题开始:哪种解花费较少?假设 这个解满足需要,价格的不同足以使程序员放弃他当前做事情的习惯方式吗?如果他的方法包 括存储在项目分析和设计过程中所创建的每个文档,并且包括当项目进化时维护这些文档,那 么当项目更新时,他的系统将花费很大,但是它能使新组员容易理解(假设没有那么多的使人 害怕阅读的文档)。这样创建和维护方法的花费会和它打算替代方法的花费一样多。 外部结构系列的另一个极端是最小化方法。为完成设计而进行足够的分析,然后丢掉它们, 使得程序员不再花时间和钱去维护它;为开始编码而做足够的设计,然后丢掉这个设计,使得 程序员不再花时间和钱去维护这些文档;然后使得代码是一流的和清晰的,代码中只需要最少 的注释。为了使新组员快速参与项目,代码连同注释就足够了。因为在所有这些乏味的文档上, 新组员只需花费很少的时间(总之,没有人真地理解它们),所以他能较快地参与工作。 即便不维护文档,丢掉文档也不是最好的办法,因为这毕竟是程序员所做的有效工作。某 些形式的文档通常是必须的(参看本章后面的描述)。 1. 通讯 对于较大的项目,期望代码像文档一样充分是不合理的,尽管我们在实际中常常这样期望。 但是,代码包含了我们实际上希望外部原则所产生的事物的本质:通讯。我们只是希望能与改 进这个程序的新组员通讯就足够了。但是,我们还想使花费在外部原则上的钱最少,因为最终 人们只为这个程序所提供的服务付钱,而不是为它后面的设计文档付钱。为了真正有用,外部 原则应当做比只产生文档更多的事情—它应当是项目组成员在创建设计时为了讨论问题而采 用的通讯方法。理想的外部原则目标是使关于程序分析和设计的通讯更容易。这对于现在为这 个程序而工作的人们和将来为这个程序而工作的人们是有帮助的。中心问题不只是为了能通讯, 而为了产生好的设计。 人们(特别是程序员)被计算机吸引(由于机器为他们做工作),出于经济原因,要求开
8 C + +编程思想 下载 发者为机器做大量工作的外部原则似乎从一开始就注定要失败。成功的方法(也就是人们习惯 的方法)有两个重要的特征: 1) 它帮助人们进行分析和设计。这就是,用这种方法比用别的方法对分析和设计中的思考 和通讯要容易得多。目前的效率和采用这种方法后的效率应当明显不同。否则,人们可能还留 在原地。还有,它的使用必须足够简单,不需用手册。当程序员正在解决问题时,要考虑简单 性,而不管他适用于符号还是技术。 2) 没有短期回报,就不会加强投资。在通向目标的可见的进展中,没有短期回报,人们就 不会感到采用一种方法能使效率提高,就会回避它。不能把这个进展误认为是从一种中间形式 到另一种中间形式的变换。程序员可以看到他的类,连同类之间互相发送的消息一起出现。为 某人创造一种方法,就像武断的约束,因为它是简单的心理状态:人们希望感到他们正在做创 造性的工作,如果某种方法妨碍他们而不是帮助他们飞快地接近目标,他们将设法绕过这种方 法。 2. 量级 在方法学上反对我的观点之一是:“好了,您能够侥幸成功是因为您正在做的小项目很短。” 听众对“小”的理解因人而异。虽然这种看法并不全对,但它包含一个正确的核心:我们所需 要的原则与我们正在努力解决问题的量级有关。小项目完全不需要外部原则,这不同于个别程 序员正在解的生命期问题的模式。涉及很多人的大项目会使人们之间有一些通讯,所以必须使 通讯具有形式化方法,以使通讯有效和准确。 麻烦的是介于它们之间的项目。它们对外部原则的需要程度可能在很大程度上依赖于项目 的复杂性和开发者的经验。确实,所有中等规模的项目都不需要忠实于成熟的方法,即产生许 多报告和很多文档。一些项目也许这样做,但许多项目可以侥幸成功于“方法学简化”(代码 多而文档少)。我们面前的所有方法学的复杂性可以减少到 8 0 %~2 0 %的(或更少的)规则。 我们正在被方法学的细节淹没,在所解决的程序设计问题中,可能只有不足 2 0 %的问题需要这 些方法学。如果我们的设计是充分的,并且维护也不可怕,那么我们也许不需要方法学或不全 部需要它。 3. OOP是结构化的吗 现在提出一个更有意义的问题。为了使通讯方便,假设方法学是需要的。这种关于程序的 元通讯是必须的,因为程序设计语言是不充分的—它太原始,趋向于机器范例,对于谈论问 题不很有用。例如,过程程序设计方法要求用数据和变换数据的函数作为术语谈论程序。因为 这不是我们讨论实际问题的方法,所以必须在问题描述和解描述之间翻译来翻译去。一旦得到 了一个解描述并且实现了它,以后无论何时对这个解描述做改变就要对问题描述做改变。这意 味着必须从机器模式返回问题空间。为了得到真正可维护的程序,并且能够适应问题空间上的 改变,这种翻译是必须的。投资和组织的需要似乎要求某种外部原则。过程程序的最重要的方 法学是结构化技术。 现在考虑,是否解空间上的语言可以完全脱离机器模式?是否可以强迫解空间使用与问题 空间相同的术语? 例如,在气候控制大楼中的空气调节器就变成了气候调节程序的空气调节器,自动调温器 变成了自动调温程序,等等。(这是按直觉做的,与O O P不一致)。突然,从问题空间到解空间 的翻译变成了次要问题。可以想象,在程序分析、设计和实现的每一阶段,能使用相同的术语 学、相同的描述,这样,这个问题就变成了“如果文档(程序)能够充分地描述它自身,我们 仍然需要关于这个文档的文档吗?”如果 O O P做它所主张的事情,程序设计的形式就变成了这
分享到:
收藏