在第1堂中,我解释了C++吸引我的地方,以及为什么要在编程中使用它。本章将对这一点进行补充说明。过去的10年时间,我都用在了开发C+-编程工具,理解怎样使用它们,编写教授C++的资料,以及修改优化C++标准等工作上。C++有何魅力让我如此痴迷呢?本章中,我将做出解答。这些问题的跨度很大,就像开车上班和设计汽车之间的差距。
2.1小项目的成功
我们很容易就会注意到:很多最成功的、最有名的软件景初是由少数人开发出来的。这些软件后来可能逐渐成长,然而,令人吃惊的是许多真正的赢家都是从小系统做起的。UNIX操作系统就是最好的例子, C编程语言也是。其他的例子还包括:电子表格、Basic和FORTRAN编程语言、MS-DOS和IBM的VM/370操作系统。VM/370尤其有趣,因为它完全是在IBM正规生产线之外发展起来的。尽管IBM多年来一直不提倡客户使用VM/370,但该操作系统仍牢牢占据IBM大型机的主流市场。
同样令人吃惊的是,很多大项目的最终结果却表现平平。我实在不愿意在公共场合指手画脚,但是我想你自己也应该能举出大量的例子来。
到底是什么使得大项目难以成功呢?我认为原因在于软件行业和其他很多行业不一样,软件制造的规模和经济效益不成正比。绝大多数称职的程序员能在一两个小时内写完一个100行的程序,而在大项目中通常每个程序员每天平均只写10行代码。
2.1.1开销
有些负面的经济效益是由于项目组成员之间相互交流需要大量时间。一旦项目组的成页多到不能同时坐在一张餐桌旁,交流上的开销问题就相当严重了。基于这一点,就必须要有某种正规的机制,保证每个项目成员对于其他人在做什么都了解得足够清楚,这样才能确保所有的部分最终能拼在一起。随着项目的扩大,这种机制将占用每个人更多的时间,同时每个人要了解的东西也会更多。
我们只需要看一下项目组成员是如何利用时间的,就会发现这些开销是多么明显:管理错误报告数据库;阅读、编写和回顾需求报告;参加会议;处理规范以及做除编程外的任何事情。
2.1.2质疑软件工厂
由于这些开销是有目共暗的,所以很多人正在寻找减少它的途径。起码到目前为止,我还没有见过什么有效的方法。这是个难题,我们可能没有办法解决。当项目达到一定规模时,尽管作了百般努力,所有的一切好像还是老出错;塔科马海峡大桥和“挑战者号”航天飞机灾难至今仍然历历在目。
有些人认为大项目的开销是在所难免的。这种态度的结果就是产生了有着过多管理开销的复杂系统。然而,更常见的情况是,这些所谓的管理最终不过是另一种经过精心组织的开销。开销还在,只是被放进干净的盒子和图表中,因此也更易于理解。有些人沉迷于这种开销。他们心安理得地那么做,就好像它是件“好事”-就好像这种开销真地能促进而不是阻碍高效的软件开发。毕竟,如果一定的管理和组织是有效的,那么更多的管理和组织就应该更有效。我猜想,这个想法给程序项目引进的纪律和组织,与为工厂厂房引进生产流水线一样。
我希望这些人错了。实际上我所接触过的软件工厂给我的感觉很不愉快。每个单独的功能都是一个巨大机器的一部分, “系统”控制一切,人也要遵从它。正是这种强硬的控制导致生产线成为劳资双方众多矛盾的焦点。
所率的是,我并不认为软件只能朝这个方向发展。软件工厂忽视了编程和生产之间的本质区别。工厂是制造大量相同(或者基本相同)产品的地方。它讲求规模效益,在生产过程中充分利用了分工的优势。最近,它的目标已经变成了要完全消除人力劳动。相反,软件开发主要是要生产数目相对较少的、彼此完全不同的人造产品。这些产品可能在很多方面相似,但是如果太相似,开发工作就变成了机械的复制过程了,这可能用程序就能完成。因此,软件开发的理想环境应该不像工厂,而更像机械修理厂-在那里,熟练的技术工人可以利用手边所有可用的精密工具来尽可能地提高工作效率。
实际上,只要在能控制的范围内,程序员(当然指称职的)就总是争取让他们的机器代替自己做它们所能完成的机械工作。毕竟,机器擅长干这样的活儿,而人很容易产生厌倦情绪。
随着项目规模越来越大,越来越难以描达,这种把程序员看成是手工艺人的观点也渐渐变得难以支持了。因此,我曾尝试描述应该如何将一个庞大的编程问题当作一系列较小的、相互独立的编程问题看待。为了做到这一点,我们首先必须把大系统中各个小项目之间存在的关系理顺,使得相关人员不必反复互相核查。换言之,我们需要项目之间有接口,这样,每个项目的成员几乎不需要关心接口之外的东西。这些接口应该像那些常用的子程序和数据结构的抽象一样成为程序员开发工具中的重要组成部分。
2.2抽象
自从25年前开始编程以来,我一直痴迷于那些能扩展程序员能力的工具。这些工具可以是编程语言、操作系统,甚至可以是关于某个问题的独特思维方式。我知道有一天我将能够轻松解决问题,这些问题是我在刚开始编程时想都不敢想的——我也知道,我不是独自前行。
我最钟情的工具有一个共性,那就是抽象的概念。当我在处理大问题的时候,这样的工总是能帮助我将问题分解成独立的子问题,并能确保它们相互独立。也就是说,当我处理问题的某个部分的时候,完全不必担心其他部分。
例如,假设我正在用汇编语言写一个程序,我必须时常考虑机器的状态。我可以文配的工具是寄存器、内存,以及运行于这些寄存器、内存上的指令。要用汇编语言做成任何一件有用的事情,就必须把我的问题用这些特定概念表达出来。
即使是汇编语言也包含了一些有用的抽象。首先是编写的程序在机器执行之前先被解释了。这就是用汇编语言写程序和直接在机器上写程序的区别。更难以察觉的是,对于机器设计者来说, “内存"和“寄存器”的概念本身就是一种抽象。如果抛开抽象不用,则程序的运行就要表示成处理器内无数个门电路的状态变换。如果你的想象力够丰富的话,就可以看到除此之外还有更多层次的抽象。
高级语言提供了更复杂的抽象。甚至用表达式替代一连串单独的算术指令的想法,也是非常重大的。这种想法在20世纪50年代首次被提出时显得很不同凡响,以至于后来成了FORTRAN命名的基础: FormulaTranslation。抽象如此有用,因此程序员们不断发明新的抽象,并且运用到他们的程序中。结果几乎所有重要的程序都给用户提供了一套抽象。
2.2.1有些抽象不是语言的一部分
考虑一下文件的概念。事实上每种操作系统都以某种方式使文件能为用户所用。每个程序员都知道文件是什么。但是,在大多数情况下,文件根本不是物理存在的!文件只是组织长期存储的数据的一种方式,并由程序和数据结构的集合提供支持来实现这个抽象。
要使用文件做任何一件有意义的事情,程序员必须知道程序是通过什么访问文件的,以及需要什么样的请求队列。对于典型的操作系统来说,必须确保提出不合理请求的程序得到相应的错误提示,而不能造成系统本身崩渍或者文件系统破坏。实际上,现代的操作系统已经就一个目的达成了共识,就是要在文件之间构筑“防火墙” ,以便增加程序在无意中修改数据的难度。
2.2.2抽象和规范
操作系统提供了一定程度的保护措施,而编程语言通常没有。那些编写新的抽象给其他程序员用的程序员,往往不得不依靠用户自己去遵守编程语言技术上的限制。这些用户不仅要遵守语言的规则,还要遵守其他程序员制定的规范。
例如, 由mallo函数实现的动态内存的概念就是C库中经常使用的抽象。你可以用一个数字作参数来调用malloc,然后它在内存中分配空间,并给出地址。当你不再需要这块内存时,就用这个地址作参数来调用free函数,这块内存就返回给系统留作它用。
在很多情况下,这个简单的抽象都相当有用。不论规模大小,很难想象一个实际的C程序不使用malloc或者free,但是,要成功地使用抽象,必须遵循一些规范。要成功地使用动态内存,程序员必须:
知道要分配多大内存。
不使用超出分配的内存范围外的内存。
不再需要时释放内存。
只有不再需要时,才释放内存。
只释放分配的内存。
切记检查每个分配请求,以确保成功。
要记住的东西很多,而且一不留神就会出错。那么有多少可以做成自动实现的呢?用C的话,没有多少。如果你正在编写一个使用了动态内存的程序,就难免要允许你的用户释放掉任何由他们分配的内存,这些内存的分配是他们对程序调用请求的一部分。
2.2.3抽象和内存管理
有些语言通过垃圾收集(garbage collection)来解决这个问题,这是一种当内存空间不再需要时自动回收内存的技术。垃圾收集使得编写程序时能更方便地采用灵活的数据结构,但要求系统在运行速度、编译器和运行时系统复杂度方面付出代价。另外,垃圾收集只回收内存,不管理其他资源。C++采用了另外一种更不同寻常的方法:如果某种数据结构需要动态分配资源,则数据结构的设计者可以在构造函数和析构函数中精确定义如何释放该结构所对应的资源。
这种机制不是总像垃圾收集那样灵活,但是在实践中,它与许多应用更接近。另外,与垃圾收集比起来它有一个明显的优势,就是对环境要求低得多:内存一旦不用了就会被释放,而不是等待垃圾收集机制发现之后才释放。
仅仅这些还不够,要想名正言顺地放弃自动垃圾收集,还应该有一些好的理由。但是构造函数和析构函数的概念在其他方面也有很好的意义。用抽象的眼光看待数据结构,它们中的许多都有关于初始化和终止的概念,而不是单纯地只有内存分配。例如,一个代表缓冲输出文件的数据结构必须体现一个思想,就是缓冲区必须在文件关闭前释放。这种约定总是在一些让人意想不到的细节地方出现,而由此产生的bug也总是非常隐蔽、难觅其踪。我曾经写过一个程序,整整3年后才发现里面隐藏了一个bug! [2]在C++中,缓冲输出文件类的定义必须包括一个释放该缓冲区的析构函数。这样就不容易犯错了。垃圾收集对此无能为力。
同理, C++的很多地方也都用到了抽象和接口。其间的关键就是要能够把问题分解为完全独立的小块。这些小块不是通过规则相互联系的,而是通过类定义和对成员函数和友元函数的调用联系起来的。不遵守规则,就会马上收到由编译器而不是由异常征兆的出错程序发出的诊断消息。
2.3机器应该为人服务
为什么我要关注语言和抽象?因为我认为大项目是无法高效地、顺利地投入使用的,也不可能加以管理。我从没见过,也不能想象,会有一种方法使得一个庞大的项目能够对抗所有这些问题。但是,如果我能找到把大项目化解为众多小问题的方法,就能引入个体优于混乱的整体、人类优于机器的因素。我们必须做工具的主人,而不是其他任何角色。
本文节选自《C++沉思录》
《C++ 沉思录》集中反映了C++的关键思想和编程技术,不仅告诉你如何编程,还告诉你为什么要这样编程。本书曾出现在众多的C++专家推荐书目中。 这将是C++程序员的必读之作。因为: 它包含了丰富的C++思想和技术,从详细的代码实例总结出程序设计的原则和方法。 不仅教你如何遵循规则,还教你如何思考C++编程。 既包括面向对象编程也包括泛型编程。 探究STL这一近年来C++最重要的新成果的内在思想。 本书的作者在使用C++的时候,全世界的C++用户还寥寥无几。他们对C++语言的发展有着突出的贡献。
C语言编程教程:第一个程序
C语言编程教程:程序的编译链接