1、第 0 章 为什么要用 C+ (11-15)0.1 原因为什么选择 C+而不是 C?或者更抽象一点,为什么选择面向对象语言,而不是面向过程语言或汇编语言?这是一个很好的问题。有人可能心里知道一些,但说不清楚;有人可能会想到很多,并认为这是一个很泛泛的问题,说来话长。其实答案很简单:如果是一个技术人员在问这个问题,答案是“为了( 更好地)复用代码” ;如果是一个非技术人员在问 (比如你的老板或是什么资本家),回答只需两个字“省钱” ,或者让他眼睛发亮的四个字“省很多钱” 。话虽不同,其背后的道理却是一样的。软件开发己经有几十年的历史了,每个人都知道这个行业最费人力,因为从开发到测试,再到维护,基
2、本上以人的手工为主。我们还知道,软件开发人员从来都是高薪阶层。所以,软件的成本主要源于人的成本。那么如何降低成本?代码复用成了持续不断的主题。这是因为如果代码能够复用,则相应的开发时间、测试时间,以及分析修改时间都能节省下来,而这些时间都对应于软件人员的高薪。可见,代码复用率越高,成本削减的越多。C+语言,或者说所有面向对象语言,就是针对代码复用设计的。我们可以列举一下面向对象语言的有名的特点:封装:把具体实现封装在类内,而类内类外的代码只靠一些公共接口联系起来,类内实现接口的功能,类外使用接口的功能。目的是什么?类内实现变化了,可以不影响类外代码(复用) ;类外使用代码变化了,可以不影响类内
3、代码(也是复用) 。继承:子类可以继承父类的东西(复用) ,也可以扩展自己新的特性,这些新特性不会影响父类,也不会影响使用父类的代码(复用) ,甚至子类可以直接以父类的身份,使用所有父类可使用的代码( 还是复用)。多态:父类和子类可以为同一个接口(复用) 提供不同的实现,外部代码不需任何改动 (复用)就可以拥有不同的特性。0.2 语言的发展和代码复用一个主流语言的出现,或者说语言发展的一次质的飞跃,其背后都有代码复用的影子。C语言取代汇编而流行,源于 UNIX 操作系统的开发。在这之前的操作系统,基本上是用汇编写成的,而 UNIX 的 90%是 C,只有 10%左右是汇编。带来的好处是,UNI
4、X 比其他操作系统更容易移植到不同的机器上,因为不必重写所有代码,90%只需重新编译( 当然需要改动,现在看来改动应该还是一件艰巨的工作,但比起用汇编语言重头写要省事多了)。C+较之 C 在代码复用上的能力更强。一方面,C+试图不加修改地整块 类复用代码,而不像 C 那样需要逐行扫描修改 (如同 UMX 移植时);另一方面,C+的复用接口( 主要是类接口)更丰富、灵活、安全,而 C 主要依靠函数接口,包容性太窄,而函数间的联系也太弱。我们再看看 Java。它的流行不单是因为它也是一种面向对象的语言,还因为它在代码复用上有独到之处。我们基本上可以把一个网页看成一个程序,而浏览网页是将该程序从 I
5、nternet 上下载到本地机器上运行。但这个程序比较特殊,它要求能运行在所有平台上(尽可能) 。我们很难用C+来写这样的网页,因为 C 中编译器只能编译出适应一种平台(CPU)的执行代码。另一种方式是用诸如 HTML 之类的语言,它们的特点是将源码下载到客户端,再由客户端解释执行。我们确实希望网页一次开发,能(复) 用到所有平台,但有时我们不想公开源码 (这实际上是不同组织间的代码复用问题,和一个组织内部的复用还不同,因为有商业利益或版权问题)。此时,Java 可以帮助我们。它实际上是设计了一个虚拟平台(CPU),所有 Java 语言源码都会编译成可运行在这个虚拟平台上的二进制执行程序,而客
6、户端的 Java 解释器负责将这个虚拟平台的程序指令解释成真正平台的指令。可见,我们共享了目标级代码。0.3 代码复用的特点从以上的分析和我们自己的开发经验可以得出代码复用的两个特点。一是代码复用是一个由简到繁、从局部到整体的不断发展的过程。由于软件本身的复杂性,我们不可能一下子把代码做到复用率极高的程度。复用的经验和手段是一个逐渐积累的过程。具体到我们自己的程序,不要指望它们一下子成为代码复用的经典,从复用一个类、一个函数,甚至一两个好的编程风格或想法入手,日积月累,你手边会逐渐形成一个复用代码库,它将是你经验和财富的宝库。如果你所在的部门已经有几个人拥有复用代码库,那么恭喜你的部门,它可以
7、成立一个技术委员会,负责收集和整合不同的复用代码库,这标志着你的部门己具备较高的专业水准和开发较大规模程序的能力。代码复用的另一个特点是:为了复用,牺牲性能、 “浪费”系统资源都不在话下。我们从汇编程序、C 程序、C+程序,再到 Java 程序可以看出,复用性越强,性能越差 (成倍下降),程序尺寸越大( 成倍增加),但我们还是乐此不疲。究其原因,无非是性能、系统资源可以靠突飞猛进的硬件能力弥补,而硬件的成本比起人力资源根本不值一提。由此再顺便提醒大家一句,与其沉醉于程序性能的提高,不如关注代码的可复用性。0.4 代码复用对我们的影响回到本书的话题。本书列举了几百条编码原则,其实试图说明两个问题
8、:一是如何防范错误;一是如何更好地发挥 C+语言的特点。我自认为代码复用的境界更高,超出本书的范畴,但我还希望大家能思考一下,这几百条原则对代码复用有何帮助。另一方面,大家不妨回忆一下自己开发的 C+代码,看看它们有多大程度的可复用性。如果不高,我们能不能问一下自己:“我有充分的理由用 C+吗?效率的损失、资源的浪费都值得吗?”我们还可以考虑一下今后的项目。第 1 章 命名原则好的命名原则在软件开发中是很重要的,尤其在可复用代码中更是如此。好的命名应该是直观而容易理解的,在移入新环境或上下文后仍能保持这种清晰的特点,并且不易与其他组件产生名字冲突。原则 1.1 关于类型名1.1.1 说明注意:
9、缩写字当作普通字处理,即只有首字母大写。1.1.2 例子/ 类名class TnppCoverageArea_T/;/ 枚举类型名enum pageCode_T/.;/ 自定义类型名typedef short Int16_T;1.1.3 原因防止与变量名冲突(变量名定义请见 “原则 1.2 关于变量和函数名”) :这种类型名定义方法和变量名会有两处不同,一是名字的第一个字母大小写不同,二是类型名以_T 结尾。使得类型名更加清晰,尤其_T 可以突出表示这是一个类名(T 代表 TYPE 的意思):_T 来源于 POSIX 的类型命名:POSIX 统一采用_t 命名其类名。为沿用其思想而又防止和PO
10、SIX 软件包冲突( 请参阅“原则 1.9 命名时避免使用国际组织占用的格式”) 而采用_T。区分名字中各单词也可用下划线,但用大写字母会使得名字短些。缩写字当作普通字处理:一是为了防止破坏该原则而造成混淆,请比较 GPSReceiver_T 和 GpsReceiver_T 谁更清楚;二是为了防止和全大写的常量名等混淆(全大写的名字请见 “原则 1.4 关于宏、常量和模板名”) 。因为 namespace 是表示一个逻辑组,与 class 或 enum 的某些用法类似,所以采用同样的命名原则。原则 1.2 关于变量和函数名变量和函数名中首字母小写,其后每个英文单词的第一个字母大写,其他小写。(
11、同时参阅:“原则 1.7 关于匿名命名空间级标识符的前缀” 、 “原则 1.9 命名时避免使用国际组织占用的格式”以及函数名的例外“原则 1.3 关于全大写的函数名(建议)” 。)注意:缩写字当作普通字处理,即只有首字母大写。12.2 例子/ 变量名int flexPageCount;/ 函数名(1ength)class String_Tpubl ic :int length(void)coust;/;/ 比较类型名和变量名GpsCommand_T gpsCommand;原则 1.3 关于全大写的函数名(建议)1.3.1 说明有一类函数,它们调用普通函数,只是对普通函数的错误返回做一般化处理。
12、这些函数的名字要和所包含的函数名相同,只是全用大写字母(必要时用下划线分隔名字中的英文单词) 。1.3.2 例子/ 哆嗦的用法FILE*pFile=fopen (“abc.txt“,“rw+“);if(pFile=NULL)/ 错误处理:打印错误信息等abort();ret=fseek (pFile,0,SEEK_END);if (ret !=0 )/ 错误处理:打印错误信息等abort();ret=fwrite(buffer,100,1,pFile);if ( ret” 。原则 1.5 关于指针编标识符名(供参考)1.5.1 说明建议以 p 开头或以 Ptr 结尾。1.5.2 例子/ 指针变
13、量名char* pName;/ 函数指针类型的名字typedef int (*CallbackFunctionPtr _T) (int parameter);1.5.3 原因使阅读者不用查定义就能意识到这是一个指针:对指针的操作与其他类型变量有很大不同,比如对其成员的访问要用“-”而不是“.” ,特别是可能需要考虑内存回收的问题等,所以应给阅读者和使用者一个较明显的提醒。原则 1.6 关于变量名前缀(供参考)1.6.1 说明用下面不同的前缀来修饰变量名以区分不同的作用域:i_类内数据成员(对象级成员);c_类内静态数据成员( 类级成员) ;g_全局变量;f_文件作用域变量(静态变量)。函数内部
14、等局部变量前不用前缀。1.6.2 例子class Message_T/ 类内静态数据成员static int c_ id;/ 类内首通数据成员(对象级)int i_id;public :void someFunction (void )/ 函数内的局部变量int id;/.;/ 全局变量int g_id;/ 静态变量(文件作用域)static int f_id;1.6.3 原因减少作用域重叠时(不同作用域中的 )变量名冲突问题。比如在上例 someFunction()中,变量id、i_id、c_id、 f_id、g_id 都可见?若无前缀,名字就都相同( 冲突)了。这样也减少了为避免名字冲突而
15、无规律可循地改变变量名。例如,将 someFunction()中的变量 id 改名为 myId,阅读者可能无法一目了然地知道为何要在 Id 前加 my,会猜测有无特殊含义等。使得阅读者在读某个作用域代码时更清晰,知道碰到的变量是在哪个作用域中定义的。前缀的含义:i_表示 instance scope;c_表示 class scope;f_表示 file scope;g_表示 global scope。下划线前缀的用法:有些人喜欢用下划线前缀修饰类内私有成员,而另一些人则用它表示全局变量。但遗憾的是它们都与 ISO 组织关于下划线前缀用法相冲突(见“原则 1.9 命名时避免使用国际组织占用的格式
16、”) 。常量前缀的处理方法:常量和变量一样,也有作用域和名字冲突问题,因此也适用于此原则。不过由于常量名是全大写,前面加上小写的前缀稍嫌混淆。另一变通方法是将常量前缀改为大写,但造成此条规则复杂化(5 项前缀变 9 项),可能给阅读者造成更大的麻烦,建议还是不要这样做。类型前缀:有些组织对于变量前缀定义得更细致,比如用 n 代表整数类型(int,short,long 等)、用 c 代表字符型等,就如同本书用 p 作为指针类型的前缀一样。这些都是可以的。但需要提醒的是,变量命名( 以及所有的命名)有两个极端,一个是什么前后缀都不加 (精炼但有用信息过少),另一个是加上所有相关的信息(Packag
17、e 名、作用域提示、类型提示等,信息全面但显得哆嗦,且重要信息不突出) ,合理的命名方式肯定在这两个极端中间的某处,但具体在哪里有赖于大家自己的判断。原则 1.7 关于匿名命名空间级标识符的前缀1.7.1 说明给匿名命名空间级标识符一个公共前缀(如所属 Package 名或 Library 名,加下划线) ,用来区别其他提供类似功能的 Packet 或 Library 等。匿名命名空间中的标识符指的是全局或文件级变量名、常量名、宏、类型名、函数名等。前缀格式:全大写字母,(最好 )少于 3 个字母。1.7.2 例子/ HA 出项目中的代码class HA_ CheckPointTable_T.
18、class HA_HashMap_T./ UML 函数库 (第三方提供的标准函数库)中的代码class UML_HashMap_T. 1.7.3 原因如果希望代码复用,则匿名命名空间级标识符就需要防止命名冲突。用 Packet 名或 Library名是一个不错的选择。选择全大写是为了醒目,并尽量与其后的真正的名字区别开来(因为真正名字才包含该标识符的意义、途等)。限制其长度是为了防止它干扰对后面真正的名字的理解(要知道该前缀只是为了防止命名冲突,不能喧宾夺主)。原则 1.8 减少匿名命名空间级标识符1.8.1 说明尽量减少匿名命名空间级变量、常量、宏及函数等标识符。可以归类放在某个命名间、类或
19、函数中。1.8.2 例子class CommonDefinition_T.public :const float PIE;/.;1.8.3 原因明显减少命名冲突。归类后使用更清晰。缩短生命周期,使之只存在于它应该发挥作用的有限时间内,从而减少麻烦:更好记、更好维护、不会被误用或滥用等。原则 1.9 命名时避免使用国际组织占用的格式1.9.1 己知的被占用的格式双下划线开头 ISO C+、 ANSI C;包含双下划线 ISO C+;单下划线开头 ISO C+、 ANSI C;E0_9A_Z开头 ANSI C;isa_z开头 ANSI C;toa_z开头 ANSI C;LC_开头 ANSI C;S
20、IGLA_Z开头 ANSI C;stra_z开头 ANSI C;mema_z开头 ANSI C;wcsa_z开头 ANSI C;_t 结尾 POSIX;其他国际组织占用的格式。1.9.2 原因减少潜在的命名冲突。防止阅读者误以为是国际组织提供的代码。原则 1.10 名字要本着清楚、简单的原则1.10.1 说明名字本身首先要做到清楚,从而帮助(而不是混淆) 对代码的理解;其次在清楚的前提下尽量简单,简单本身也是要使阅读者更容易理解。理解之后才能谈到使用,使用之后才有修改和提高。1.10.2 例子/ 不好理解的名字int shldwncnt;int rs;int num;/ 比较一下int she
21、11DownloadCount;int returnstatus;int alarmNumber;1.10.3 定量分析的参考可以将名字长度限制在 3 到 25 个字符之间,并据此编写或利用现成的工具自动扫描代码,以检查名字是否做到“简单、清楚” 。少于 3 个字符通常不够清楚(选择 3 是因为 1hs、rhs 之类的名字应该足够清晰,见“原则 1.26 关于函数的左值参数和右值参数名”);大于 25 个字符则(感觉上)嫌不够简单。下限的 3 个字符应该不包括公共前缀如 package 名 HA_、变量作用域 i_、指针前缀 p 、公共后缀 (如类型名中的的 _T)以及下划线等,为的是确保名字
22、中真正核心的部分足够清晰;而上限的 25 个字符应包含名字中所有字符。原则 1.11 尽量用可发音的名字1.11.1 例子/ 不可发音的名字class Ymdhms;/ 可发音的名字class Timestamp_T;1.11.2 原因可发音的名字更好读、更好懂,也更便于交流。原则 1.12 尽量用英文命名原因英语是最通用的语言,特别是在程序语言中,其他语言(比如选择汉语拼音) 可能造成阅读者理解上的困难。原则 1.13 尽量选择通用词汇并贯穿始终1.13.1 说明许多单词都能表达一个含义,要选择一个最通用的(大家在编写类似代码时约定俗成的) ,并且始终保持这一用法。1.13.2 例子比如 g
23、et、read、fetch、retrieve 都能表达“取出”的意思,在定义一个有“取出”功能的函数时函数名用 get 或 read 较为常见。一旦定下使用哪一个(比如 read)就坚持用到底。如果一会儿用 read,一会儿又用 get,读者可能会误以为这两个函数有区别。原则 1.14 避免用模棱两可、晦涩或不标准的缩写1.14.1 原因好的缩写会明显简化名字,还能清楚地表达出原意。不好的缩写则相反,不如不要,就算因此导致名字长度倍增也在所不惜。此时要记住:(任何情况下都是)录入易,维护难。所谓好与不好,最简单的标准就是看该缩写是否通用,越通用越好。1.14.2 例子class Pgh_T;
24、/ 是 paragraph 的缩写吗原则 1.15 避免使用会引起误解的词汇例子比如用 portList 来描述一组 port,如果其数据结构不是链表就不合适,因为约定俗成 List 就是指链表,不要小看这一点,它会减少很多头疼的事。原则 1.16 减少名字中的冗余信息1.16.1 例子比如类的成员名不需要包含类名;class Alarm_T.Severity_T alarmseverity( void ) const; / 用 severity()就足够了;1.16.2 原因不要让阅读者花额外精力来区分哪些是有用信息,那些是冗余信息。原则 1.17 建议起名尽量通俗,太专一会限制以后的扩展例子比如定义了一个手机短语消息类,其中有一个域表示消息来源,用 sourcesubscriberID 做名字就不如用 source 通用。要预知将来的扩展有时很难,但在初次定义时想一下今后可能的扩展不是坏事。原则 1.18 名字最好尽可能精确地表达其内容例子没人知道 data、infotuff 的内容到底是什么。原则 1.19 避免名字中出现形状况混淆的字母或数字例子/* 字母 O 和数字 0 形状类似,避免混用;* 实在无法避免,最好总是用小写,这样和数字 0 区别* 还大一点