精华内容
下载资源
问答
  • 质量效率
    千次阅读
    2020-09-07 13:59:16

    什么样是质量对企业有价值呢?

    企业要给一个什么样质量效率原则对企业有价值呢?

    企业的需要的“质量与效率价值”和企业改员工传递的“质量与效率价值”该有多大的差别?

    有些质量影响企业的质量,有些不影响,有些影响会发现,有些影响不会被发现,有些影响发现的晚些,有些相对早些。

    那么如果定位企业的质量效率价值呢?

    质量和效率是相对成反比的,质量意味着成本,效率伴随着效益。

    那么有如下几个问题。

    质量高了,那么效率一定会降下来。

    1.有些质量的提高与不提高对短期没有影响

    2.有些质量的提高长时间不会有什么效果

    3.有些质量的提高很长时间都不会有什么效果,知道产品的生命周期结束,产品的质量代码也没发挥作用

    如果提高效率呢?那么有如下几中情况。

    那么质量的短板就会有如下几种情况

    1.短时间出问题

    2.长时间出问题

    3.很长时间都不出问题,也许过了这个软件的生命周期了都不出问题。

     

    更多相关内容
  • 研发人员如何提高工作效率

    万次阅读 多人点赞 2018-08-30 19:58:37
    大家都是程序员,接到同样的工作任务,有人每天忙碌加班弄到身心俱疲却还无法完成,而有人不仅能按时保质保量完成工作,还能有自己的时间健身、学习和充电。是什么原因导致这样的差别呢?小米的创始人雷军有一句名言...

    大家都是程序员,接到同样的工作任务,有人每天忙碌加班弄到身心俱疲却还无法完成,而有人不仅能按时保质保量完成工作,还能有自己的时间健身、学习和充电。是什么原因导致这样的差别呢?小米的创始人雷军有一句名言:不要用战术上的勤奋掩盖战略上的懒惰。天道不一定酬勤,比勤奋更重要的是通过深度思考,找到正确的道路,然后借用巧方法、好工具帮助自己更快到达目的地。

    本文将通过解答以下 8 个问题,和大家一起探讨如何提高工作效率。

    1. 写代码是应该写一行,编译一行吗?
    2. 拿到一个需求却不知从那里开始,面对一堆需求,先做哪些功能呢?
    3. 研发中踩过的坑,如果有效避免第二次掉坑?
    4. 常常为了一个小小的错误用了很长的时间,不知从那里查,也查不出结果,对于偶现或是小错误,是否值得投入、评判标准是什么?
    5. 常常做出来的程序,涉及到需求变动就挠头,怎么在程序设计阶段,考虑好扩展性?
    6. 熬夜赶工,是否可取?
    7. 一个功能,要不要自测,自测要多久,为什么 RD 排斥自测?
    8. 团队协同开发,如何向上或向下管理,提高开发效率?

     

    一、写代码是应该写一行,编译一行吗?

    不瞒大家,刚入行时,我写代码的习惯就是写好一行,编译一行,运行一行,自测一行,这样写代码效率当然很低下,一个简单逻辑,就需要写大半天,这里的写一行编译一行,描述的有点夸张,记得我初学一门语言时大多数情况都是一行一行的写,因为我还不确定每行代码,程序会怎么执行,会产生怎样的输出?

    如果你还处于我刚说的这个阶段,那么,你正在蹒跚学步。在这个阶段,你一定要用心,多写多练,明白你写的每一行代码会产生怎样的输出,尽量做到熟能生巧。

    如果你熟知每行代码的输出,仍然写一行,编译一行,进行着低效的劳作,这时候你就需要给自己一点自信啦,就像很多人早上出门去上班,要确认门有没有锁好,每天要确认 180 次自己带手机没?带家门钥匙没?身份证还在钱包里不?不光不相信自己,团队小伙伴提交一行代码,这些人都需要 72 次确认,这行代码是不是可运行,别笑,生活中这种人很多,在我的职业生涯中,也遇到了很多类似的程序员,他们写代码本身就很严谨,遵从谷歌公司代码规则,遵从团队编码约定,反复训练,可以做到和上公交就掏出公交卡一样熟练,但他们仍然需要反复确认,自己的代码成员变量命名是否为驼峰,指针变量生命周期管理是否合理无漏洞。

    大家都写过高考作文,写代码其实可以像写作文一样,初学者写的是句子,想一句写一句,厉害了想一段,写一段,作文写的好的,大部分都会打个提纲,总体想一下,一气呵成,最多检查几个错别字。

    程序开发,切忌上来就写,想到哪里写到哪里,一段好的代码并不是它们越复杂越好,简单实现复杂功能才是我们最需要的,linux 内核代码虽大,但是那些精典的算法实现的代码精炼的不能再精炼了。要提高编码速度更重要的是简化梳理程序流程,以最小的代码量完成功能。所以编程最重要的事情是思考。

    当思考好,几乎所有步骤后再写,基本上就是码代码,然后调试改 bug 的过程。

    如果你熟练各种语法,还在写一行,编译一行,基本上就是没经过系统思考,就开始一通乱写,这样效率真的会很低。

    建议写代码前,思考,然后把框架写出来,写框架的时候,可以只写方法,不写实现,仅在方法内,多写几个 TODO,写明输入哪些参数,输出哪些参数,把框架整理几遍后,再一个个的把 TODO 函数实现写好,一次编译!

    如果系统很复杂,那就一个一个模块的实现,然后一次编译。

    二、拿到一个需求却不知从那里开始,面对一堆需求,先做哪些功能呢?

    面对一堆需求,需要先对需求进行优先级排序。

    判断一个需求的优先级,主要是从三个方面:产品战略定位、用户影响、技术实现。通过重要性和紧急度两个指标进行排序,综合对比衡量确定某需求优先级,成本也需要纳入指标进行考量。

    当然作为一名 RD,产品功能需求优先级是PM根据以上或者其他考虑,预定好的。作为一名 RD,也需要有产品思维,根据自己对技术的了解,提出自己的技术方案,可行性分析,估时等等数据,供 PM 确定需求优先级.

    然而我想说的是,不必把每个需求模块都做完,憋个大招,三四天程序仍然是编写中,不可运行状态。

    尤其是在需求不明确的情况下,宜采用快速原型模型开发的开发方法,快速设计开发出软件系统的原型,该原型向用户展示待开发软件的全部或部分功能和性能;用户对该原型进行测试评定,给出具体改进意见以丰富细化软件需求;开发人员据此对软件进行修改完善,直至用户满意认可之后,进行软件的完整实现及测试、维护。

    把握主线需求,不断细化是快速出活的一大妙招。

    重要事情再说一次吧 … 切忌拿到一个模块直接想着出细活,除非需求很明确,时间要求又不太紧张,否则你会发现,你用好长时间做的精细活,PM 想了想,各种原因,砍掉了。。。

    举个例子,一个车载音乐播放器,PM 要求绘制音频频谱,这个绝对不是主线需求,音乐播放器,能稳定播放音乐,绝对是压倒一切其它需求的主线需求,如果一上来,你就忙着绘制频谱,而不是实现音频播放模块,那一定是本末倒置了。需要调整一下任务计划了。

    三、研发中踩过的坑,如果有效避免第二次掉坑?

    代码不糊弄人,人也不要糊弄代码,啥?你又掉坑里了?不长记性!

    我们要做到知其然,知其所以然,对每一个踩过的坑,我们要知道这个坑产生了怎样的影响,为啥我们会掉坑。只有对每个坑有个充分的认识,并深刻领悟,才能避免我们不断的掉坑。

    刚入行时,一个程序输出异常,我常用的方式就是网上胡乱搜索一通用 n 种大牛神贴提出的解决思路,来解决我遇到的 bug,然而不注意积累,不注意举一反三,当时把问题解决了,过了几个月,又掉坑里了!

    每次踩坑,一定要写一下自己的踩坑日记,咋掉进去的,通过啥方式爬出来的,以后怎么避免,等等,这样即使以后又掉进去了,查查日记,还是可以快速爬出来的… 有道云笔记,推荐大家使用!

    还有,别人刚从坑里爬出来,你又掉进去了,这种情况,我们也要避免,别人当年踩过的坑,也是和我们相关的。你踩过的坑,团队的其它小伙伴也需要学习。有些弯路不一定非得自己走,有些坑也不一定非得自己跳,别人的经历或许就会成为我们的经验,知道多一点,也会避开很多坑。

    可以通过 bug case study 的方式,把自己踩过的坑和大家描述一下,一起学习,也更加加深自己对踩坑经历的认识。

    四、常常为了一个小小的错误用了很长的时间,不知从那里查,也查不出结果,对于偶现或是小错误,是否值得投入、评判标准是什么?

    如果有优先级更高的任务,那么先做其他任务,我们每个人,都有通过潜意识思考的能力。

    高考时,老师会让大家在答题之前,去看一遍作文题,再从前往后作答,这里就用到了我们潜意识思考的能力,面对 bug,我们可以先看一眼,如果很容易,立马解决,不占用大脑太多思考时间,如果有难度,把问题记下来,可以先做其他任务,你放心,你的潜意识,会在你吃饭时,坐车时,甚至睡觉时去帮助你思考。

    如果一个 bug,解了好久,还是没有进展,不妨看一看 TodoList 任务清单,有没有更紧急的任务。

    另外如果 bug 仅仅是偶现,可以评估影响面,和 PM 商量是否解决,之前我做系统时,一个内部系统,每天上万次录音操作,只有一个电脑录音出 bug,我用了一整天时间,不知问题出在哪里,作为一个强资源,耗在一个 bug 上,肯定是不合适的,pm 决定给内部员工换台电脑,最低成本解决了…

    五、常常做出来的程序,涉及到需求变动就挠头,怎么在程序设计阶段,考虑好扩展性?

    如果你开发的系统,需求不是很明确,建议采用快速原型的开发方式进行开发,对功能不停的细化。

    程序的扩展性建立在对业务需求变化的预见性之上。作为一名 RD,需要了解业务,有产品思维,可以琢磨出产品未来发展方向。

    在入组之前,需要向 PM 和业务人员请教一下未来业务发展方向,竞品都有哪些功能。

    重视程序架构设计,“ 正交设计、类要有专职、善于用委托 ”,“ 解耦、解耦 ",“ 组件化 ”,“ 数据层、业务层分层设计 ” 等等,一定要烂熟于心,Gof23 种常用设计模式,可以信手拈来,但切忌东拼西凑,一定要适合。

    一般来讲,没有听说过、或者设计模式掌握不好的 RD,开发的程序,扩展性都会比较差,内功修习也很重要,一定要拿一本介绍设计模式的书,翻烂它。

     六、熬夜赶工,是否可取?

    熬夜赶工,是否可取?答案当然是不可取。

    没有熬过夜的程序员不是好程序员,晚上熬夜加班都已经成为程序员的名片了。不是有个段子吗,大晚上还在工作的除了盗贼,就是程序员。

    工作这些年,加过很多班,可是问自己,真的加班越多,效率越高么?

    答案其实是看状态的,状态好的时候,加班的效率,确实是很高的,白天的时间过于碎片化,一会儿开排期会、一会儿开沟通会、一会儿来一个面试,很难集中精力。而下班后,周围变得很安静,静下心来写代码,整个过程感觉还是不错的,经常感觉,如果可以,希望夜晚再长些,从晚上 7 点开始,再多来几个小时,让我安静的把这些需求实现完。

    但是一定不要鼓励加班,尤其是很多领导,鼓励下属加班,我认为是不可取的,状态好就多干点儿、状态不好,需要及时休息。
    切忌在状态不好时玩命加班,亲身体会,状态不好时,效率是很低的,作为一名 RD,要时刻感知自己的状态,状态好时,做最有挑战的事情,状态一般时,处理最有把握的事情,状态很差时,要休息一会儿。

    曾经为了快速出活,每天晚上干活到三四点、早上 7 点多起来,萎靡不振,一直感到很疲倦,后来调节了一下作息,感到疲惫时,及时入睡,第二天早两小时起床,感觉状态是很不一样的。
    如果长期加班,仅仅是因为每天订的目标完不成,那么我还有以下几个建议供参考:

    1.每日工作前做好时间规划,写一个 TODOLIST,将任务按照优先级排序,充分利用白天的工作时间。

    2.如果需要集中注意力,为避免其它事务打扰,可以选择戴上耳机、关掉 QQ、微信等,集中一两个小时的精力。

    3.人们常说今日事今日毕,但是我有不同的观点,竟可能遗留 TODOLIST 上一两个任务留给明天,这样下班后,你的潜意识还是会帮你思考今天未完成的任务的,等第二天上班,可以很快的衔接前一天的工作,不至于前一天工作都做完了,第二天一上班,不知道今天要从哪个任务继续。

     七 一个功能,要不要自测,自测要多久,为什么 RD 排斥自测?

    一个功能,必须经过自测,代码也最好 code review。

    QA 发现一个 bug,你猜了猜原因,然后直接去修改代码,很有信心的打包,然后就丢给 QA 同学继续测试了,结果第一次 QA 说,bug 没有解决,第二次 QA 说,bug 还是没有解决,第三次 QA 怒了,你们 RD 能不能测试一下自己的东西再给我们。结果测试很不开心,你也很不开心,悄悄的嘀咕,你们 RD 不就是负责测试的吗,提BUG是你们的工作呀。

    说到自测,RD 很多时候测一下自己的函数返回是不是正确,直接就给 QA 同学了。稍有不慎,修改一个函数会影响其它流程、甚至是主流程,所以提交给 QA 同学之前,一定要跑一下测试用例。提测时,也需要和 QA 说明,修改了哪些地方,会对哪些地方造成影响,便于 QA 同学制定回归计划。

    什么?不知道会影响哪些地方?那么这个 BUG,修复过程一定没有走心,不清楚改目前的bug对别的功能有什么影响,就改代码,绝对是应该禁止的。以防拆东墙补西墙,改一个 BUG,冒出一堆 BUG。

    修改 BUG,应该把自己当作是一个外科医生,没有充分的把握,是不能给患者开颅的,哪怕你知道患者的脑子有问题。一定要提前预估影响,平衡利弊。

    切不可认为 RD 就是编码实现的,QA 才是测 BUG 的,自测是 RD 工作的一部分,最牛的 RD,是拿到 QA 哪里挑不出 BUG,而不是开发的程序,到 RD 那里一堆堆 BUG。

    且 RD 也要懂得发现和使用一些自动化测试的工具,越早发现 BUG,bug 的修复成本越小,RD 应该自测,应该充分自测,最好自己的交付零 bug。

    八 团队协同开发,如何向上或向下管理,提高开发效率?

    向上或是向下管理,我认为核心就是两个词,目标、方案沟通。

     对于目标,从上到下,对目标的认识一定要一致,无论是你的下属还是领导,一定要统一对目标的认识。我们要在什么时间交付什么样的产品,哪些是必须交付的,哪些是可以接受 delay 或者不做的。

    目标很重要,首先你需要了解领导想要达成的目标,领导需要你帮忙开发一个文本编辑器、你给领导开发了一个 Word;领导只是想让你做一个可以播放声音的工具,你给领导做了一个音频编辑器;领导只是想出一个报表,你做了一个 Excel 给他,显然你耗费了比领导预期更多的资源,开发了一个功能过载的产品。

    方案沟通,也很重要,对于一个需求,领导认为开发一个小程序就可以,你认为需要开发一个 APP,甚至你认为最好还可以有个 Html5 网站,这个时候就需要和领导沟通清楚你的方案了。

    RD 天天敲代码,表达能力差,不善于沟通,这是大家对 RD 的普遍认识。

    我想说,不是这样,一名好的 RD,一定是逻辑性很强,表达能力最优秀的同学。写代码不是在写散文,其它人看半天,不懂是什么意思。写代码我感觉应该像是写证明题一样,别人可以按图索骥,一步步的知道,每行代码的意思,整个代码框架是什么样的。

    所以,我出技术方案的时候,一般都要考虑很长时间,不会看到需求就开始做,先把技术方案想清楚,会用到哪些技术,技术难点有哪些,哪些是需要验证的,验证不通过怎么办等,然后再拿去和领导、团队小伙伴沟通。

    沟通会很有必要,会议前把自己的技术方案想清楚,写成 PPT 或简单的写个提纲,提前想想在会议上大家可能的疑问有哪些,大家不支持的话,怎么说明自己的考虑 。找个大家状态都不错的时候,召集所有干系人,开会沟通一下。

    对于向下、向上管理,要远离几个雷区:

    1. 不是领导说啥就是啥,领导说我们今年要造个火箭,那我们要先耐心的听一下领导说的方案,提出疑问,看看是不是领导某些方面没有考虑到。如果领导不能自圆其说,相信他很乐意听你的意见。

    2. 不是你说啥,下属就要答应啥。很多 RD,刚刚做管理的时候,很讨厌听反对意见、感觉自己安排什么,下属就必须做什么,甚至先做啥,后做啥,都不能给下属自由度。这样很危险,长期以往,下属会习惯唯命是从,缺乏思考。

    3. 内心有想法,但是出于对领导的敬畏,不表示出来,怕自己说出反对想法,领导不高兴。这个,首先,你是帮领导出谋划策的,而不仅仅是一个执行人员。其次,任务的结果,是大家一起共担的,早一点给领导建议,早一点发现问题呢。

    只有大家对目标、对方案的认识保持高度一致,做事情的效率才可以提高上来,试想一架马车,领导想往南边拉,有人往北边使劲、有人往东边使劲、有人往西边使劲,很费力的,很多会是无用功。

    最后和大家分享知乎 ershou 大神的一段话,是很多新程序员不知道的小技巧,毕竟代码做到零 bug,那么效率才是最高的。

    1. 东西交付之前偷偷测试一遍;
    2. 问别人之前偷偷谷歌一下;
    3. 版本发布之前反复检查七八遍,用 check-list;
    4. 用谷歌,用英文搜索;
    5. 做十件事不如做好一件事,夺取话语权只有一条路径,就是超出别人的预期;
    6. 心要皮实,但话语和脸皮要柔软,记住有句老话叫,伸手不打笑脸人。
    7. 先假装你就是专家,慢慢为了装得像,不得不去学,假的就成真了。

    本文完,感谢你花时间把本文看完,如果你有收获或者有不同的见解,欢迎和我交流。


     

    展开全文
  • 高质量C++/C编程指南(林锐)

    千次阅读 多人点赞 2016-08-17 11:46:04
    就我的经历与阅历来看,国内大学的计算机教育压根就没有灌输高质量程序设计的观念,教师们和学生们也很少自觉关心软件的质量。勤奋好学的程序员长期在低质量的程序堆中滚爬,吃尽苦头之后才有一些心得体会,长进极慢...

    版本/状态

    作者

    参与者

    起止日期

    备注

    V 0.9

    草稿文件

    林锐

     

     

    2001-7-1

    2001-7-18

    林锐起草

    V 1.0

    正式文件

    林锐

     

     

    2001-7-18

    2001-7-24

    朱洪海审查V 0.9

    林锐修正草稿中的错误

     

     

     

     

     

     

     

     

     

     

     

     

     

       

     ... 6

    1 文件结构... 11

    1.1 版权和版本的声明... 11

    1.2 头文件的结构... 12

    1.3 定义文件的结构... 13

    1.4 头文件的作用... 13

    1.5 目录结构... 14

    2 程序的版式... 15

    2.1 空行... 15

    2.2 代码... 16

    2.3 代码行内的空格... 17

    2.4 对齐... 18

    2.5 长行拆分... 19

    2.6 修饰符的位置... 19

    2.7 注释... 20

    2.8 类的版式... 21

    3 命名规则... 22

    3.1 共性规则... 22

    3.2 简单的Windows应用程序命名规则... 23

    3.3 简单的Unix应用程序命名规则... 25

    4 表达式和基本语句... 26

    4.1 运算符的优先级... 26

    4.2 复合表达式... 27

    4.3 if 语句... 27

    4.4 循环语句的效率... 29

    4.5 for 语句的循环控制变量... 30

    4.6 switch语句... 30

    4.7 goto语句... 31

    5 常量... 33

    5.1 为什么需要常量... 33

    5.2 const  #define的比较... 33

    5.3 常量定义规则... 33

    5.4 类中的常量... 34

    6 函数设计... 36

    6.1 参数的规则... 36

    6.2 返回值的规则... 37

    6.3 函数内部实现的规则... 39

    6.4 其它建议... 40

    6.5 使用断言... 41

    6.6 引用与指针的比较... 42

    7 内存管理... 44

    7.1内存分配方式... 44

    7.2常见的内存错误及其对策... 44

    7.3指针与数组的对比... 45

    7.4指针参数是如何传递内存的?... 47

    7.5 freedelete把指针怎么啦?... 50

    7.6 动态内存会被自动释放吗?... 50

    7.7 杜绝“野指针”... 51

    7.8 有了malloc/free为什么还要new/delete ... 52

    7.9 内存耗尽怎么办?... 53

    7.10 malloc/free 的使用要点... 54

    7.11 new/delete 的使用要点... 55

    7.12 一些心得体会... 56

    8 C++函数的高级特性... 57

    8.1 函数重载的概念... 57

    8.2 成员函数的重载、覆盖与隐藏... 60

    8.3 参数的缺省值... 63

    8.4 运算符重载... 64

    8.5 函数内联... 65

    8.6 一些心得体会... 68

    9 类的构造函数、析构函数与赋值函数... 69

    9.1 构造函数与析构函数的起源... 69

    9.2 构造函数的初始化表... 70

    9.3 构造和析构的次序... 72

    9.4 示例:类String的构造函数与析构函数... 72

    9.5 不要轻视拷贝构造函数与赋值函数... 73

    9.6 示例:类String的拷贝构造函数与赋值函数... 73

    9.7 偷懒的办法处理拷贝构造函数与赋值函数... 75

    9.8 如何在派生类中实现类的基本函数... 75

    9.9 一些心得体会... 77

    10 类的继承与组合... 78

    10.1 继承... 78

    10.2 组合... 80

    11 其它编程经验... 82

    11.1 使用const提高函数的健壮性... 82

    11.2 提高程序的效率... 84

    11.3 一些有益的建议... 85

    参考文献... 87

    附录C++/C代码审查表... 88

    附录C++/C试题... 93

    附录C++/C试题的答案与评分标准... 97

     

          

    软件质量是被大多数程序员挂在嘴上而不是放在心上的东西!

        除了完全外行和真正的编程高手外,初读本书,你最先的感受将是惊慌:“哇!我以前捏造的C++/C程序怎么会有那么多的毛病?”

        别难过,作者只不过比你早几年、多几次惊慌而已。

        请花一两个小时认真阅读这本百页经书,你将会获益匪浅,这是前面N-1个读者的建议。

     

    一、编程老手与高手的误区

     

    自从计算机问世以来,程序设计就成了令人羡慕的职业,程序员在受人宠爱之后容易发展成为毛病特多却常能自我臭美的群体。

    如今在Internet上流传的“真正”的程序员据说是这样的:

    (1)    真正的程序员没有进度表,只有讨好领导的马屁精才有进度表,真正的程序员会让领导提心吊胆。

    (2)    真正的程序员不写使用说明书,用户应当自己去猜想程序的功能。

    (3)    真正的程序员几乎不写代码的注释,如果注释很难写,它理所当然也很难读。

    (4)    真正的程序员不画流程图,原始人和文盲才会干这事。

    (5)    真正的程序员不看参考手册,新手和胆小鬼才会看。

    (6)    真正的程序员不写文档也不需要文档,只有看不懂程序的笨蛋才用文档。

    (7)    真正的程序员认为自己比用户更明白用户需要什么。

    (8)    真正的程序员不接受团队开发的理念,除非他自己是头头。

    (9)    真正的程序员的程序不会在第一次就正确运行,但是他们愿意守着机器进行若干个30小时的调试改错。

    (10)真正的程序员不会在上午9:00到下午5:00之间工作,如果你看到他在上午9:00工作,这表明他从昨晚一直干到现在。

    ……

    具备上述特征越多,越显得水平高,资格老。所以别奇怪,程序员的很多缺点竟然可以被当作优点来欣赏。就象在武侠小说中,那些独来独往、不受约束且带点邪气的高手最令人崇拜。我曾经也这样信奉,并且希望自己成为那样的“真正”的程序员,结果没有得到好下场。

     

    我从读大学到博士毕业十年来一直勤奋好学,累计编写了数十万行C++/C代码。有这样的苦劳和疲劳,我应该称得上是编程老手了吧?

    我开发的软件都与科研相关(集成电路CAD3D图形学领域),动辄数万行程序,技术复杂,难度颇高。这些软件频频获奖,有一个软件获得首届中国大学生电脑大赛软件展示一等奖。在1995年开发的一套图形软件库到2000年还有人买。罗列出这些“业绩”,可以说明我算得上是编程高手了吧?

    可惜这种个人感觉不等于事实。

     

    读博期间我曾用一年时间开发了一个近10万行C++代码的3D图形软件产品,我内心得意表面谦虚地向一位真正的软件高手请教。他虽然从未涉足过3D图形领域,却在几十分钟内指出该软件多处重大设计错误。让人感觉那套软件是用纸糊的华丽衣服,扯一下掉一块,戳一下破个洞。我目瞪口呆地意识到这套软件毫无实用价值,一年的心血白化了,并且害死了自己的软件公司。

     

    人的顿悟通常发生在最心痛的时刻,在沮丧和心痛之后,我作了深刻反省,“面壁”半年,重新温习软件设计的基础知识。补修“内功”之后,又觉得腰板硬了起来。博士毕业前半年,我曾到微软中国研究院找工作,接受微软公司一位资深软件工程师的面试。他让我写函数strcpy的代码。

    太容易了吧?

    错!

    这么一个小不点的函数,他从三个方面考查:

    1)编程风格;

    2)出错处理;

    3)算法复杂度分析(用于提高性能)。

    在大学里从来没有人如此严格地考查过我的程序。我化了半个小时,修改了数次,他还不尽满意,让我回家好好琢磨。我精神抖擞地进“考场”,大汗淋漓地出“考场”。这“高手”当得也太窝囊了。我又好好地反省了一次。

     

    我把反省后的心得体会写成文章放在网上传阅,引起了不少软件开发人员的共鸣。我因此有幸和国产大型IT企业如华为、上海贝尔、中兴等公司的同志们广泛交流。大家认为提高质量与生产率是软件工程要解决的核心问题。高质量程序设计是非常重要的环节,毕竟软件是靠编程来实现的。

    我们心目中的老手们和高手们能否编写出高质量的程序来?

    不见得都能!

     

    就我的经历与阅历来看,国内大学的计算机教育压根就没有灌输高质量程序设计的观念,教师们和学生们也很少自觉关心软件的质量。勤奋好学的程序员长期在低质量的程序堆中滚爬,吃尽苦头之后才有一些心得体会,长进极慢,我就是一例。

    现在国内IT企业拥有学士、硕士、博士文凭的软件开发人员比比皆是,但他们在接受大学教育时就“先天不足”,岂能一到企业就突然实现质的飞跃。试问有多少软件开发人员对正确性、健壮性、可靠性、效率、易用性、可读性(可理解性)、可扩展性、可复用性、兼容性、可移植性等质量属性了如指掌?并且能在实践中运用自如?。“高质量”可不是干活小心点就能实现的!

     

    我们有充分的理由疑虑:

    1)编程老手可能会长期用隐含错误的方式编程(习惯成自然),发现毛病后都不愿相信那是真的!

    2)编程高手可以在某一领域写出极有水平的代码,但未必能从全局把握软件质量的方方面面。

     

           事实证明如此。我到上海贝尔工作一年来,陆续面试或测试过近百名“新”“老”程序员的编程技能,质量合格率大约是10%。很少有人能够写出完全符合质量要求的if语句,很多程序员对指针、内存管理一知半解,……

    领导们不敢相信这是真的。我做过现场试验:有一次部门新进14名硕士生,在开欢迎会之前对他们进行“C++/C编程技能”摸底考试。我问大家试题难不难?所有的人都回答不难。结果没有一个人及格,有半数人得零分。竞争对手公司的朋友们也做过试验,同样一败涂地。

     

    真的不是我“心狠手辣”或者要求过高,而是很多软件开发人员对自己的要求不够高。

    要知道华为、上海贝尔、中兴等公司的员工素质在国内IT企业中是比较前列的,倘若他们的编程质量都如此差的话,我们怎么敢期望中小公司拿出高质量的软件呢?连程序都编不好,还谈什么振兴民族软件产业,岂不胡扯。

     

    我打算定义编程老手和编程高手,请您别见笑。

    定义1:能长期稳定地编写出高质量程序的程序员称为编程老手。

    定义2:能长期稳定地编写出高难度、高质量程序的程序员称为编程高手。

    根据上述定义,马上得到第一推论:我既不是高手也算不上是老手。

     

    在写此书前,我阅读了不少程序设计方面的英文著作,越看越羞惭。因为发现自己连编程基本技能都未能全面掌握,顶多算是二流水平,还好意思谈什么老手和高手。希望和我一样在国内土生土长的程序员朋友们能够做到:

    1)知错就改;

    2)经常温故而知新;

    3)坚持学习,天天向上。

     

    二、本书导读

     

        首先请做附录BC++/C试题(不要看答案),考查自己的编程质量究竟如何。然后参照答案严格打分。

    1)如果你只得了几十分,请不要声张,也不要太难过。编程质量差往往是由于不良习惯造成的,与人的智力、能力没有多大关系,还是有药可救的。成绩越差,可以进步的空间就越大,中国不就是在落后中赶超发达资本主义国家吗?只要你能下决心改掉不良的编程习惯,第二次考试就能及格了。

    2)如果你考及格了,表明你的技术基础不错,希望你能虚心学习、不断进步。如果你还没有找到合适的工作单位,不妨到上海贝尔试一试。

    3)如果你考出85分以上的好成绩,你有义务和资格为你所在的团队作“C++/C编程”培训。希望你能和我们多多交流、相互促进。半年前我曾经发现一颗好苗子,就把他挖到我们小组来。

    4)如果你在没有任何提示的情况下考了满分,希望你能收我做你的徒弟。

     

        编程考试结束后,请阅读本书的正文。

        本书第一章至第六章主要论述C++/C编程风格。难度不高,但是细节比较多。别小看了,提高质量就是要从这些点点滴滴做起。世上不存在最好的编程风格,一切因需求而定。团队开发讲究风格一致,如果制定了大家认可的编程风格,那么所有组员都要遵守。如果读者觉得本书的编程风格比较合你的工作,那么就采用它,不要只看不做。人在小时候说话发音不准,写字潦草,如果不改正,总有后悔的时候。编程也是同样道理。

        第七章至第十一章是专题论述,技术难度比较高,看书时要积极思考。特别是第七章“内存管理”,读了并不表示懂了,懂了并不表示就能正确使用。有一位同事看了第七章后觉得“野指针”写得不错,与我切磋了一把。可是过了两周,他告诉我,他忙了两天追查出一个Bug,想不到又是“野指针”出问题,只好重读第七章。

    光看本书对提高编程质量是有限的,建议大家阅读本书的参考文献,那些都是经典名著。

     

           如果你的编程质量已经过关了,不要就此满足。如果你想成为优秀的软件开发人员,建议你阅读并按照CMMI规范做事,让自己的综合水平上升一个台阶。上海贝尔的员工可以向网络应用事业部软件工程研究小组索取CMMI有关资料,最好能参加培训。

     

    三、版权声明

     

           本书的大部分内容取材于作者一年前的书籍手稿(尚未出版),现整理汇编成为上海贝尔网络应用事业部的一个规范化文件,同时作为培训教材。

           由于C++/C编程是众所周知的技术,没有秘密可言。编程的好经验应该大家共享,我们自己也是这么学来的。作者愿意公开本书的电子文档。

           版权声明如下:

    1)读者可以任意拷贝、修改本书的内容,但不可以篡改作者及所属单位。

    2)未经作者许可,不得出版或大量印发本书。

    3)如果竞争对手公司的员工得到本书,请勿公开使用,以免发生纠纷。

           预计到20027月,我们将建立切合中国国情的CMMI 3级解决方案。届时,包括本书在内的约1000页规范将严格受控。

     

           欢迎读者对本书提出批评建议

     

     

    林锐,20017

     

     

    1 文件结构

    每个C++/C程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。

    C++/C程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀,C++程序的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。

    1.1 版权和版本的声明

    版权和版本的声明位于头文件和定义文件的开头(参见示例1-1),主要内容有:

    1)版权信息。

    2)文件名称,标识符,摘要。

    3)当前版本号,作者/修改者,完成日期。

    4)版本历史信息。

     

    /*

    * Copyright (c) 2001,上海贝尔有限公司网络应用事业部

    * All rights reserved.

    *

    文件名称:filename.h

    文件标识:见配置管理计划书

        要:简要描述本文件的内容

    *

    当前版本:1.1

        者:输入作者(或修改者)名字

    完成日期:2001720

    *

    取代版本1.0

    原作者 输入原作者(或修改者)名字

    完成日期:2001510

    */

     

    示例1-1 版权和版本的声明

    1.2 头文件的结构

    头文件由三部分内容组成:

    1)头文件开头处的版权和版本声明(参见示例1-1)。

    2)预处理块。

    3)函数和类结构声明等。

    假设头文件名称为 graphics.h,头文件的结构参见示例1-2

     

    l         【规则1-2-1为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。

    l         【规则1-2-2 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。

    l         【规则1-2-3 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。

    ²        【建议1-2-1头文件中只存放“声明”而不存放“定义”

    C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。

    ²        【建议1-2-2不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。

     

    // 版权和版本声明见示例1-1,此处省略。

     

    #ifndef   GRAPHICS_H // 防止graphics.h被重复引用

    #define   GRAPHICS_H

     

    #include <math.h>     // 引用标准库的头文件

    #include “myheader.h”  // 引用非标准库的头文件

    void Function1(…);   // 全局函数声明

    class Box             // 类结构声明

    {

    };

    #endif

    示例1-2 C++/C头文件的结构

     

    1.3 定义文件的结构

    定义文件有三部分内容:

    (1)       定义文件开头处的版权和版本声明(参见示例1-1)。

    (2)       对一些头文件的引用。

    (3)       程序的实现体(包括数据和代码)。

    假设定义文件的名称为 graphics.cpp定义文件的结构参见示例1-3

     

    // 版权和版本声明见示例1-1,此处省略。

     

    #include “graphics.h”     // 引用头文件

     

    // 全局函数的实现体

    void Function1(…)

    {

    }

     

    // 类成员函数的实现体

    void Box::Draw(…)

    {

    }

    示例1-3 C++/C定义文件的结构

    1.4 头文件的作用

    早期的编程语言如BasicFortran没有头文件的概念,C++/C语言的初学者虽然会用使用头文件,但常常不明其理。这里对头文件的作用略作解释:

    1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

    2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

    1.5 目录结构

    如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护。

    例如可将头文件保存于include目录,将定义文件保存于source目录(可以是多级目录)。

    如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。

     

    2 程序的版式

           版式虽然不会影响程序的功能,但会影响可读性。程序的版式追求清晰、美观,是程序风格的重要构成因素。

    可以把程序的版式比喻为“书法”。好的“书法”可让人对程序一目了然,看得兴致勃勃。差的程序“书法”如螃蟹爬行,让人看得索然无味,更令维护者烦恼有加。请程序员们学习程序的“书法”,弥补大学计算机教育的漏洞,实在很有必要。

    2.1 空行

    空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。

     

    l         【规则2-1-1在每个类声明之后、每个函数定义结束之后都要加空行。参见示例2-1a

    l         【规则2-1-2在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。参见示例2-1

     

    // 空行

    void Function1(…)

    {

     …

    }

    // 空行

    void Function2(…)

    {

     …

    }

    // 空行

    void Function3(…)

    {

     …

    }

     

    // 空行

    while (condition)

    {

     statement1;

     // 空行

     if (condition)

     {

         statement2;

     }

     else

     {

         statement3;

     }

    // 空行

     statement4;

    示例2-1(a) 函数之间的空行                  示例2-1(b) 函数内部的空行

     

    2.2 代码行

    l         【规则2-2-1一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。

    l         【规则2-2-2ifforwhiledo等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。

     

    示例2-2a)为风格良好的代码行,示例2-2b)为风格不良的代码行。

     

     

    int width;    // 宽度

    int height;   // 高度

    int depth;    // 深度

     

    int width, height, depth; // 宽度高度深度

     

    x = a + b;

    y = c + d;

    z = e + f;

    = a + b;   y = c + d; z = e + f;

     

    if (width < height)

    {

    dosomething();

    }

    if (width < height) dosomething();

    for (initialization; condition; update)

    {

    dosomething();

    }

    // 空行

    other();

     

    for (initialization; condition; update)

         dosomething();

    other();

     

     

    示例2-2(a) 风格良好的代码行                示例2-2(b) 风格不良的代码行

     

    ²        【建议2-2-1尽可能在定义变量的同时初始化该变量(就近原则)

    如果变量的引用处和其定义处相隔比较远,变量的初始化很容易被忘记。如果引用了未被初始化的变量,可能会导致程序错误。本建议可以减少隐患。例如

    int width = 10;     // 定义并初绐化width

    int height = 10;    // 定义并初绐化height

    int depth = 10;     // 定义并初绐化depth

     

    2.3 代码行内的空格

    l         【规则2-3-1关键字之后要留空格。象constvirtualinlinecase 等关键字之后至少要留一个空格,否则无法辨析关键字。象ifforwhile等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。

    l         【规则2-3-2函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。

    l         【规则2-3-3‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。

    l         【规则2-3-4‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如for (initialization; condition; update)

    l         【规则2-3-5赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+= >=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<,^”等二元操作符的前后应当加空格。

    l         【规则2-3-6一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后不加空格。

    l         【规则2-3-7象“[]”、“.”、“->这类操作符前后不加空格。

    ²        【建议2-3-1对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)if ((a<=b) && (c<=d))

     

    void Func1(int x, int y, int z);          // 良好的风格

    void Func1 (int x,int y,int z);           // 不良的风格

    if (year >= 2000)                         // 良好的风格

    if(year>=2000)                            // 不良的风格

    if ((a>=b) && (c<=d))                     // 良好的风格

    if(a>=b&&c<=d)                            // 不良的风格

    for (i=0; i<10; i++)                      // 良好的风格

    for(i=0;i<10;i++)                         // 不良的风格

    for (i = 0; I < 10; i ++)                 // 过多的空格

    x = a < b ? a : b;                        // 良好的风格

    x=a<b?a:b;                                // 不好的风格

    int *x = &y;                              // 良好的风格 

    int * x = & y;                            // 不良的风格 

    array[5] = 0;                             // 不要写成 array [ 5 ] = 0;

    a.Function();                             // 不要写成 a . Function();

    b->Function();                            // 不要写成 b -> Function();

     

    示例2-3 代码行内的空格

     

    2.4 对齐

    l         【规则2-4-1程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。

    l         【规则2-4-2{ }之内的代码块在‘{’右边数格处左对齐。

     

    示例2-4a)为风格良好的对齐,示例2-4b)为风格不良的对齐。

     

     

    void Function(int x)

    {

    … // program code

    }

     

    void Function(int x){

    … // program code

    }

     

    if (condition)

    {

    … // program code

    }

    else

    {

    … // program code

    }

    if (condition){

    … // program code

    }

    else {

    … // program code

    }

    for (initialization; condition; update)

    {

    … // program code

    }

    for (initialization; condition; update){

    … // program code

    }

    While (condition)

    {

    … // program code

    }

    while (condition){

    … // program code

    }

    如果出现嵌套的{},则使用缩进对齐,如:

         {

           …

              {

               …

              }

           …

    }

     

    示例2-4(a) 风格良好的对齐                      示例2-4(b) 风格不良的对齐

     

    2.5 长行拆分

    l         【规则2-5-1代码行最大长度宜控制在7080个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。

    l         【规则2-5-2长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。

     

    if ((very_longer_variable1 >= very_longer_variable12)

    && (very_longer_variable3 <= very_longer_variable14)

    && (very_longer_variable5 <= very_longer_variable16))

    {

        dosomething();

    }

    virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,

                                     CMatrix rightMatrix);

     

    for (very_longer_initialization;

         very_longer_condition;

         very_longer_update)

    {

        dosomething();

    }

    示例2-5 长行的拆分

    2.6 修饰符的位置

    修饰符   应该靠近数据类型还是该靠近变量名,是个有争议的活题。

    若将修饰符 * 靠近数据类型,例如:int* x; 从语义上讲此写法比较直观,即xint 类型的指针。

    上述写法的弊端是容易引起误解,例如:int* x, y; 此处y容易被误解为指针变量。虽然将xy分行定义可以避免误解,但并不是人人都愿意这样做。

     

    l         【规则2-6-1应当将修饰符 *   紧靠变量名

    例如:

    char *name;

        int   *x, y;    // 此处y不会被误解为指针

    2.7 注释

    C语言的注释符为“/*…*/”。C++语言中,程序块的注释常采用“/*…*/”,行注释一般采用“//…”。注释通常用于:

    1)版本、版权声明;

    2)函数接口说明;

    3)重要的代码行或段落提示。

    虽然注释有助于理解代码,但注意不可过多地使用注释。参见示例2-6

     

    l         【规则2-7-1注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱。注释的花样要少。

    l         【规则2-7-2如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。例如

    i++;    // i  1,多余的注释

    l         【规则2-7-3边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。

    l         【规则2-7-4注释应当准确、易懂,防止注释有二义性错误的注释不但无益反而有害。

    l         【规则2-7-5尽量避免在注释中使用缩写,特别是不常用缩写。

    l         【规则2-7-6注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。

    l         【规则2-7-8当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。

     

     

    /*

    函数介绍:

    输入参数:

    输出参数:

    返回值 

    */

    void Function(float x, float y, float z)

    {

     …

    }

     

    if (…)

    {

     …

     while (…)

     {

    } // end of while

    } // end of if

    示例2-6 程序的注释

     

    2.8 类的版式

    类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关键字publicprotectedprivate,分别用于声明哪些数据和函数是公有的、受保护的或者是私有的。这样可以达到信息隐藏的目的,即让类仅仅公开必须要让外界知道的内容,而隐藏其它一切内容。我们不可以滥用类的封装功能,不要把它当成火锅,什么东西都往里扔。

    类的版式主要有两种方式:

    1)将private类型的数据写在前面,而将public类型的函数写在后面,如示例8-3a)。采用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。

    2)将public类型的函数写在前面,而将private类型的数据写在后面,如示例8.3b)采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)。

    很多C++教课书受到Biarne Stroustrup第一本著作的影响,不知不觉地采用了“以数据为中心”的书写方式,并不见得有多少道理。

    我建议读者采用“以行为为中心”的书写方式,即首先考虑类应该提供什么样的函数。这是很多人的经验——“这样做不仅让自己在设计类时思路清晰,而且方便别人阅读。因为用户最关心的是接口,谁愿意先看到一堆私有数据成员!”

     

    class A

    {

     private:

    int    i, j;

    float x, y;

        

     public:

    void Func1(void);

    void Func2(void);

    }

    class A

    {

     public:

    void Func1(void);

    void Func2(void);

     private:

    int    i, j;

    float x, y;

        

    }

    示例8.3(a) 以数据为中心版式                 示例8.3(b) 以行为为中心的版式

     

     

     

    3 命名规则

    比较著名的命名规则当推Microsoft公司的“匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以ch为前缀,若是指针变量则追加前缀p。如果一个变量由ppch开头,则表明它是指向字符指针的指针。

    “匈牙利”法最大的缺点是烦琐,例如

    int    i, j, k; 

    float x, y, z;

    倘若采用匈牙利”命名规则,则应当写成

    int    iI, iJ, ik; // 前缀 i表示int类型

    float fX, fY, fZ; // 前缀 f表示float类型

    如此烦琐的程序会让绝大多数程序员无法忍受。

    据考察,没有一种命名规则可以让所有的程序员赞同,程序设计教科书一般都不指定命名规则。命名规则对软件产品而言并不是“成败悠关”的事,我们不要化太多精力试图发明世界上最好的命名规则,而应当制定一种令大多数项目成员满意的命名规则,并在项目中贯彻实施。

    3.1 共性规则

           本节论述的共性规则是被大多数程序员采纳的,我们应当在遵循这些共性规则的前提下,再扩充特定的规则,如3.2节。

     

    l         【规则3-1-1标识符应当直观且可以拼读,可望文知意,不必进行“解码”。

    标识符最好采用英文单词或其组合,便于记忆和阅读。切忌使用汉语拼音来命名。程序中的英文单词一般不会太复杂,用词应当准确。例如不要把CurrentValue写成NowValue

     

    l         【规则3-1-2标识符的长度应当符合“min-length && max-information”原则。

    几十年前老ANSI C规定名字不准超过6字符,现今的C++/C不再有此限制。一般来说,长名字能更好地表达含义,所以函数名、变量名、类名长达十几个字符不足为怪。那么名字是否越长约好?不见得例如变量名maxval就比maxValueUntilOverflow好用。单字符的名字也是有用的,常见的如i,j,k,m,n,x,y,z等,它们通常可用作函数内的局部变量。

     

    l         【规则3-1-3命名规则尽量与所采用的操作系统或开发工具的风格保持一致。

    例如Windows应用程序的标识符通常采用“大小写”混排的方式,如AddChild。而Unix应用程序的标识符通常采用“小写加下划线”的方式,如add_child。别把这两类风格混在一起用。

     

    l         【规则3-1-4程序中不要出现仅靠大小写区分的相似的标识符。

    例如:

    int x, X;     // 变量与 容易混淆

    void foo(int x);    // 函数foo FOO容易混淆

    void FOO(float x);

     

    l         【规则3-1-5程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。

     

    l         【规则3-1-6变量的名字应当使用“名词”或者“形容词+名词”。

    例如:

    float value;

    float oldValue;

    float newValue;

     

    l         【规则3-1-7全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。

    例如:

    DrawBox();           // 全局函数

                  box->Draw();        // 类的成员函数

     

    l         【规则3-1-8用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。

    例如:

    int   minValue;

    int   maxValue;

     

    int   SetValue(…);

    int   GetValue(…);

     

    ²        【建议3-1-1尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。

    3.2 简单的Windows应用程序命名规则

           作者对“匈牙利”命名规则做了合理的简化,下述的命名规则简单易用,比较适合于Windows应用软件的开发。

     

    l         【规则3-2-1类名和函数名用大写字母开头的单词组合而成。

    例如:

     class Node;                 // 类名

     class LeafNode;             // 类名

     void Draw(void);           // 函数名

     void SetValue(int value); // 函数名

     

    l         【规则3-2-2变量和参数用小写字母开头的单词组合而成。

    例如:

        BOOL flag;

        int drawMode;

     

    l         【规则3-2-3常量全用大写的字母,用下划线分割单词。

    例如:

        const int MAX = 100;

        const int MAX_LENGTH = 100;

     

    l         【规则3-2-4静态变量加前缀s_(表示static)。

    例如:

    void Init(…)

    {

           static int s_initValue;      // 静态变量

           

    }

     

    l         【规则3-2-5如果不得已需要全局变量,则使全局变量加前缀g_(表示global)。

    例如:

    int g_howManyPeople;   // 全局变量

    int g_howMuchMoney; // 全局变量

     

    l         【规则3-2-6类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。

    例如:

        void Object::SetValue(int width, int height)

        {

            m_width = width;

    m_height = height;

    }

     

    l         【规则3-2-7为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀。例如三维图形标准OpenGL的所有库函数均以gl开头,所有常量(或宏定义)均以GL开头。

    3.3 简单的Unix应用程序命名规则

     

    4 表达式和基本语句

    读者可能怀疑:连ifforwhilegotoswitch这样简单的东西也要探讨编程风格,是不是小题大做?

    我真的发觉很多程序员用隐含错误的方式写表达式和基本语句,我自己也犯过类似的错误。

    表达式和语句都属于C++/C的短语结构语法。它们看似简单,但使用时隐患比较多。本章归纳了正确使用表达式和语句的一些规则与建议。

    4.1 运算符的优先级

           C++/C语言的运算符有数十个,运算符的优先级与结合律如表4-1所示。注意一元运算符 + - * 的优先级高于对应的二元运算符。

     

    优先级

    运算符

    结合律

     

     

     

     

     

     

     

    ( ) [ ] -> .

    从左至右

    ! ~ ++ -- (类型) sizeof

    + - * &

    从右至左

     

    * / %

    从左至右

    + -

    从左至右

    << >>

    从左至右

    <   <=   > >=

    从左至右

    == !=

    从左至右

    &

    从左至右

    ^

    从左至右

    |

    从左至右

    &&

    从左至右

    ||

    从右至左

    ?:

    从右至左

    = += -= *= /= %= &= ^=

    |= <<= >>=

    从左至右

    4-1 运算符的优先级与结合律

     

    l         【规则4-1-1】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。

    由于将表4-1熟记是比较困难的,为了防止产生歧义并提高可读性,应当用括号确定表达式的操作顺序。例如:

    word = (high << 8) | low

    if ((a | b) && (a & c))  

    4.2 复合表达式

     a = b = c = 0这样的表达式称为复合表达式。允许复合表达式存在的理由是:(1)书写简洁;(2)可以提高编译效率。但要防止滥用复合表达式。

     

    l         【规则4-2-1不要编写太复杂的复合表达式。

    例如:

          i = a >= b && c < d && c + f <= g + h ; // 复合表达式过于复杂

     

    l         【规则4-2-2不要有多用途的复合表达式。

    例如:

    d = (a = b + c) + r ;

    该表达式既求a值又求d值。应该拆分为两个独立的语句:

    a = b + c;

    d = a + r;

     

    l         【规则4-2-3不要把程序中的复合表达式与“真正的数学表达式”混淆。

    例如: 

    if (a < b < c)          // a < b < c数学表达式而不是程序表达式

    并不表示      

    if ((a<b) && (b<c))

    而是成了令人费解的

    if ( (a<b)<c )

    4.3 if 语句

        if语句是C++/C语言中最简单、最常用的语句,然而很多程序员用隐含错误的方式写if语句。本节以“与零值比较”为例,展开讨论。

     

    4.3.1 布尔变量与零值比较

    l         【规则4-3-1不可将布尔变量直接与TRUEFALSE或者10进行比较。

    根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如Visual C++ TRUE定义为1,而Visual Basic则将TRUE定义为-1

    假设布尔变量名字为flag,它与零值比较的标准if语句如下:

    if (flag)   // 表示flag为真

    if (!flag) // 表示flag为假

    其它的用法都属于不良风格,例如:

        if (flag == TRUE)  

        if (flag == 1 )    

        if (flag == FALSE)  

        if (flag == 0)    

     

    4.3.2 整型变量与零值比较

    l         【规则4-3-2应当将整型变量用“==”或“!=”直接与0比较

        假设整型变量的名字为value,它与零值比较的标准if语句如下:

    if (value == 0)  

    if (value != 0)

    不可模仿布尔变量的风格而写成

    if (value)     // 会让人误解 value是布尔变量

    if (!value)

     

    4.3.3 浮点变量与零值比较

    l         【规则4-3-3不可将浮点变量用“==”或“!=”与任何数字比较

        千万要留意,无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。

        假设浮点变量的名字为x,应当将  

    if (x == 0.0)  // 隐含错误的比较

    转化为

    if ((x>=-EPSINON) && (x<=EPSINON))

    其中EPSINON是允许的误差(即精度)。

     

    4.3.4 指针变量与零值比较

    l         【规则4-3-4应当将指针变量用“==”或“!=”与NULL比较

        指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。假设指针变量的名字为p,它与零值比较的标准if语句如下:

            if (p == NULL) // pNULL显式比较,强调p是指针变量

            if (p != NULL) 

    不要写成

            if (p == 0)     // 容易让人误解p是整型变量

            if (p != 0)    

        或者

    if (p)          // 容易让人误解p是布尔变量

        if (!p)        

     

    4.3.5 if语句的补充说明

    有时候我们可能会看到 if (NULL == p) 这样古怪的格式。不是程序写错了,是程序员为了防止将 if (p == NULL) 误写成 if (p = NULL),而有意把pNULL颠倒。编译器认为 if (p = NULL) 是合法的,但是会指出 if (NULL = p)是错误的,因为NULL不能被赋值。

    程序中有时会遇到if/else/return的组合,应该将如下不良风格的程序

        if (condition) 

            return x;

        return y;

    改写为

        if (condition)

        {

            return x;

        }

        else

        {

    return y;

    }

    或者改写成更加简练的

    return (condition ? x : y);

    4.4 循环语句的效率

        C++/C循环语句中,for语句使用频率最高,while语句其次,do语句很少用。本节重点论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。

     

    l         【建议4-4-1在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。例如示例4-4(b)的效率比示例4-4(a)的高。

     

    for (row=0; row<100; row++)

    {

    for ( col=0; col<5; col++ )

    {

    sum = sum + a[row][col];

    }

    }

    for (col=0; col<5; col++ )

    {

    for (row=0; row<100; row++)

    {

        sum = sum + a[row][col];

    }

    }

    示例4-4(a) 低效率:长循环在最外层           示例4-4(b) 高效率:长循环在最内层

     

    l         【建议4-4-2如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例4-4(c)的程序比示例4-4(d)多执行了N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N非常大,最好采用示例4-4(d)的写法,可以提高效率。如果N非常小,两者效率差别并不明显,采用示例4-4(c)的写法比较好,因为程序更加简洁。

     

    for (i=0; i<N; i++)

    {

    if (condition)

        DoSomething();

    else

        DoOtherthing();

    }

    if (condition)

    {

    for (i=0; i<N; i++)

        DoSomething();

    }

    else

    {

        for (i=0; i<N; i++)

        DoOtherthing();

    }

    4-4(c) 效率低但程序简洁                4-4(d) 效率高但程序不简洁

    4.5 for 语句的循环控制变量

    l         【规则4-5-1不可在for 循环体内修改循环变量,防止for 循环失去控制。

     

    l         【建议4-5-1建议for语句的循环控制变量的取值采用“半开半闭区间”写法。

    示例4-5(a)中的x值属于半开半闭区间“0 =< x < N”,起点到终点的间隔为N,循环次数为N

    示例4-5(b)中的x值属于闭区间“0 =< x <= N-1”,起点到终点的间隔为N-1,循环次数为N

    相比之下,示例4-5(a)的写法更加直观,尽管两者的功能是相同的。

     

    for (int x=0; x<N; x++)

    {

    }

    for (int x=0; x<=N-1; x++)

    {

    }

    示例4-5(a) 循环变量属于半开半闭区间           示例4-5(b) 循环变量属于闭区间

    4.6 switch语句

        有了if语句为什么还要switch语句?

    switch是多分支选择语句,而if语句只有两个分支可供选择。虽然可以用嵌套的if语句来实现多分支选择,但那样的程序冗长难读。这是switch语句存在的理由。

        switch语句的基本格式是:

    switch (variable)

    {

    case value1 :  …

    break;

    case value2 :  …

    break;

        

        default : …

    break;

    }

     

    l         【规则4-6-1每个case语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。

    l         【规则4-6-2不要忘记最后那个default分支。即使程序真的不需要default处理,也应该保留语句   default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了default处理。

    4.7 goto语句

        自从提倡结构化设计以来,goto就成了有争议的语句。首先,由于goto语句可以灵活跳转,如果不加限制,它的确会破坏结构化设计风格。其次,goto语句经常带来错误或隐患。它可能跳过了某些对象的构造、变量的初始化、重要的计算等语句,例如:

    goto state;

    String s1, s2; // goto跳过

    int sum = 0;    // goto跳过

    state:

    如果编译器不能发觉此类错误,每用一次goto语句都可能留下隐患。

        很多人建议废除C++/Cgoto语句,以绝后患。但实事求是地说,错误是程序员自己造成的,不是goto的过错。goto 语句至少有一处可显神通,它能从多重循环体中咻地一下子跳到外面,用不着写很多次的break语句例如

     { …

          { …

              { …

                  goto error;

              }

          }

     }

     error:

     …

    就象楼房着火了,来不及从楼梯一级一级往下走,可从窗口跳出火坑。所以我们主张少用、慎用goto语句,而不是禁用。

     

    5 常量

        常量是一种标识符,它的值在运行期间恒定不变。C语言用 #define来定义常量(称为宏常量)。C++ 语言除了 #define外还可以用const来定义常量(称为const常量)。

    5.1 为什么需要常量

    如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?

    (1)       程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么。

    (2)       在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。

    (3)       如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。

     

    l         【规则5-1-1 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。

    例如:

        #define         MAX   100       /*  C语言的宏常量  */

    const int      MAX = 100;      //  C++ 语言的const常量

    const float     PI = 3.14159;   //  C++ 语言的const常量

    5.2 const  #define的比较

        C++ 语言可以用const来定义常量,也可以用 #define来定义常量。但是前者比后者有更多的优点:

    (1)       const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。

    (2)       有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。

     

    l         【规则5-2-1C++ 程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

    5.3 常量定义规则

    l         【规则5-3-1需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。

    l         【规则5-3-2如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。

    例如:

    const float   RADIUS = 100;

    const float   DIAMETER = RADIUS * 2;

    5.4 类中的常量

    有时我们希望某些常量只在类中有效。由于#define定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const修饰数据成员来实现。const数据成员的确是存在的,但其含义却不是我们所期望的。const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。

        不能在类声明中初始化const数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道SIZE的值是什么。

        class A

        {…

            const int SIZE = 100;  // 错误,企图在类声明中初始化const数据成员

            int array[SIZE];        // 错误,未知的SIZE

        };

     

    const数据成员的初始化只能在类构造函数的初始化表中进行,例如

        class A

        {…

            A(int size);        // 构造函数

            const int SIZE ;   

        };

        A::A(int size) : SIZE(size) // 构造函数的初始化表

        {

         …

        }

        A a(100); // 对象 a SIZE值为100

        A b(200); // 对象 b SIZE值为200

     

        怎样才能建立在整个类中都恒定的常量呢?别指望const数据成员了,应该用类中的枚举常量来实现。例如

        class A

        {…

            enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量

            int array1[SIZE1]; 

            int array2[SIZE2];

        };

        枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159)。

     

     

     

    6 函数设计

    函数是C++/C程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很容易导致该函数被错用,所以光使函数的功能正确是不够的。本章重点论述函数的接口设计和内部实现的一些规则。

    函数接口的两个要素是参数和返回值。C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。由于引用传递的性质象指针传递,而使用方式却象值传递,初学者常常迷惑不解,容易引起混乱,请先阅读6.6节“引用与指针的比较”。

    6.1 参数的规则

    l         【规则6-1-1参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。

    例如:

    void SetValue(int width, int height); // 良好的风格

    void SetValue(int, int);               // 不良的风格

    float GetValue(void);      // 良好的风格

    float GetValue();          // 不良的风格

     

    l         【规则6-1-2参数命名要恰当,顺序要合理。

    例如编写字符串拷贝函数StringCopy,它有两个参数。如果把参数名字起为str1str2

    void StringCopy(char *str1, char *str2);

    那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。

    可以把参数名字起得更有意义,如叫strSourcestrDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination

    还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。

    如果将函数声明为:

    void StringCopy(char *strSource, char *strDestination);

    别人在使用时可能会不假思索地写成如下形式:

    char str[20];

    StringCopy(str, “Hello World”); // 参数顺序颠倒

     

    l         【规则6-1-3如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。

    例如:

    void StringCopy(char *strDestinationconst char *strSource);

     

    l         【规则6-1-4如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

     

    ²        【建议6-1-1避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。

     

    ²        【建议6-1-2尽量不要使用类型和数目不确定的参数。

    C标准库函数printf是采用不确定参数的典型代表,其原型为:

    int printf(const chat *format[, argument]…);

    这种风格的函数在编译时丧失了严格的类型安全检查。

    6.2 返回值的规则

    l         【规则6-2-1不要省略返回值的类型。

    C语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void类型。

    C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C函数,为了避免混乱,规定任何C++/ C函数都必须有类型。如果函数没有返回值,那么应声明为void类型。

     

    l         【规则6-2-2函数名字与返回值类型在语义上不可冲突。

    违反这条规则的典型代表是C标准库函数getchar

    例如:

    char c;

    c = getchar();

    if (c == EOF)

    按照getchar名字的意思,将变量c声明为char类型是很自然的事情。但不幸的是getchar的确不是char类型,而是int类型,其原型如下:

            int getchar(void);

    由于cchar类型,取值范围是[-128127],如果宏EOF的值在char的取值范围之外,那么if语句将总是失败,这种“危险”人们一般哪里料得到!导致本例错误的责任并不在用户,是函数getchar误导了使用者。

     

    l         【规则6-2-3不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。

    回顾上例,C标准库函数的设计者为什么要将getchar声明为令人迷糊的int类型呢?他会那么傻吗?

    在正常情况下,getchar的确返回单个字符。但如果getchar碰到文件结束标志或发生读错误,它必须返回一个标志EOF。为了区别于正常的字符,只好将EOF定义为负数(通常为负1)。因此函数getchar就成了int类型。

    我们在实际工作中,经常会碰到上述令人为难的问题。为了避免出现误解,我们应该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用return语句返回。

    函数getchar可以改写成 BOOL GetChar(char *c);

    虽然gecharGetChar灵活,例如 putchar(getchar()); 但是如果getchar用错了,它的灵活性又有什么用呢?

     

    ²        【建议6-2-1有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。

    例如字符串拷贝函数strcpy的原型:

    char *strcpy(char *strDestconst char *strSrc);

    strcpy函数将strSrc拷贝至输出参数strDest中,同时函数的返回值又是strDest。这样做并非多此一举,可以获得如下灵活性:

        char str[20];

        int length = strlen( strcpy(str, “Hello World”) );

     

    ²        【建议6-2-2如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。

    例如:

    class String

    {…

        // 赋值函数

        String & operate=(const String &other);

    // 相加函数,如果没有friend修饰则只许有一个右侧参数

    friend String   operate+( const String &s1, const String &s2);

    private:

        char *m_data;

    }

     

           String的赋值函数operate = 的实现如下:

    String & String::operate=(const String &other)

    {

        if (this == &other)

            return *this;

        delete m_data;

        m_data = new char[strlen(other.data)+1];

        strcpy(m_data, other.data);

        return *this;   // 返回的是 *this的引用,无需拷贝过程

    }

     

    对于赋值函数,应当用“引用传递”的方式返回String对象。如果用“值传递”的方式,虽然功能仍然正确,但由于return语句要 *this拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。例如:

     String a,b,c;

     …

     a = b;      // 如果用“值传递”,将产生一次 *this 拷贝

     a = b = c; // 如果用“值传递”,将产生两次 *this 拷贝

     

           String的相加函数operate + 的实现如下:

    String operate+(const String &s1, const String &s2)  

    {

        String temp;

        delete temp.data;   // temp.data是仅含‘\0’的字符串

            temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];

            strcpy(temp.data, s1.data);

            strcat(temp.data, s2.data);

            return temp;

        }

     

    对于相加函数,应当用“值传递”的方式返回String对象。如果改用“引用传递”,那么函数返回值是一个指向局部对象temp的“引用”。由于temp在函数结束时被自动销毁,将导致返回的“引用”无效。例如:

        c = a + b;

    此时 a + b 并不返回期望值,c什么也得不到,流下了隐患。

    6.3 函数内部实现的规则

    不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。

     

    l         【规则6-3-1在函数体的“入口处”,对参数的有效性进行检查。

    很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。详见6.5节“使用断言”。

     

    l         【规则6-3-2在函数体的“出口处”,对return语句的正确性和效率进行检查。

        如果函数有返回值,那么函数的“出口处”是return语句。我们不要轻视return语句。如果return语句写得不好,函数要么出错,要么效率低下。

    注意事项如下:

    1return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。例如

        char * Func(void)

        {

            char str[] = hello world; // str内存位于栈上

            

            return str;     // 将导致错误

        }

    2)要搞清楚返回的究竟是“值”、“指针”还是“引用”。

    3)如果函数返回值是一个对象,要考虑return语句的效率。例如   

                  return String(s1 + s2);

    这是临时对象的语法,表示“创建一个临时对象并返回它”。不要以为它与“先创建一个局部对象temp并返回它的结果”是等价的,如

    String temp(s1 + s2);

    return temp;

    实质不然,上述代码将发生三件事。首先,temp对象被创建,同时完成初始化;然后拷贝构造函数把temp拷贝到保存返回值的外部存储单元中;最后,temp在函数结束时被销毁(调用析构函数)。然而“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。

    类似地,我们不要将 

    return int(x + y); // 创建一个临时变量并返回它

    写成

    int temp = x + y;

    return temp;

    由于内部数据类型如int,float,double的变量不存在构造函数与析构函数,虽然该“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。

    6.4 其它建议

    ²        【建议6-4-1函数的功能要单一,不要设计多用途的函数。

    ²        【建议6-4-2函数体的规模要小,尽量控制在50行代码之内。

    ²        【建议6-4-3尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。

    带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必需。

    ²        【建议6-4-4不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。

    ²        【建议6-4-5用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。

    6.5 使用断言

    程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。

    断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。示例6-5是一个内存复制函数。在运行过程中,如果assert的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了assert)。

     

             void *memcpy(void *pvTo, const void *pvFrom, size_t size)

    {

            assert((pvTo != NULL) && (pvFrom != NULL));     // 使用断言

            byte *pbTo = (byte *) pvTo;     // 防止改变pvTo的地址

            byte *pbFrom = (byte *) pvFrom; // 防止改变pvFrom的地址

            while(size -- > 0 )

                *pbTo ++ = *pbFrom ++ ;

            return pvTo;

    }

    示例6-5 复制不重叠的内存块

     

    assert不是一个仓促拼凑起来的宏。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到发生错误的原因。

    很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。你化了很多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。有的时候,程序员偶尔还会设计出有错误的断言。所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。这好比一个人在森林里,看到树上钉着一块“危险”的大牌子。但危险到底是什么?树要倒?有废井?有野兽?除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的断言常常被程序员忽略,甚至被删除。[Maguire, p8-p30]

     

    l         【规则6-5-1使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。

    l         【规则6-5-2】在函数的入口处,使用断言检查参数的有效性(合法性)。

    l         【建议6-5-1在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。

    l         【建议6-5-2一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

    6.6 引用与指针的比较

    引用是C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,nm的一个引用(reference),m是被引用物(referent)。

        int m;

        int &n = m;

    n相当于m的别名(绰号),对n的任何操作就是对m的操作。例如有人名叫王小毛,他的绰号是“三毛”。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以n既不是m的拷贝,也不是指向m的指针,其实n就是m它自己。

    引用的一些规则如下:

    1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

    2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。

    3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

        以下示例程序中,k被初始化为i的引用。语句k = j并不能将k修改成为j的引用,只是把k的值改变成为6。由于ki的引用,所以i的值也变成了6

        int i = 5;

        int j = 6;

        int &k = i;

        k = j; // ki的值都变成了6;

        上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。

        以下是“值传递”的示例程序。由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0

        void Func1(int x)

    {

        x = x + 10;

    }

    int n = 0;

        Func1(n);

        cout << “n = ” << n << endl; // n = 0

       

    以下是“指针传递”的示例程序。由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10

        void Func2(int *x)

    {

        (* x) = (* x) + 10;

    }

    int n = 0;

        Func2(&n);

        cout << “n = ” << n << endl;     // n = 10

     

        以下是“引用传递”的示例程序。由于Func3函数体内的x是外部变量n的引用,xn是同一个东西,改变x等于改变n,所以n的值成为10

        void Func3(int &x)

    {

        x = x + 10;

    }

    int n = 0;

        Func3(n);

        cout << “n = ” << n << endl;     // n = 10

     

        对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?

    答案是“用适当的工具做恰如其分的工作”。

        指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?

    如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。

     

    7 内存管理

        欢迎进入内存这片雷区。伟大的Bill Gates 曾经失言:

    640K ought to be enough for everybody

    — Bill Gates 1981

    程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本章的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。

    7.1内存分配方式

    内存分配方式有三种:

    (1)       从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

    (2)       在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

    (3)       从堆上分配,亦称动态内存分配。程序在运行的时候用mallocnew申请任意多少的内存,程序员自己负责在何时用freedelete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

    7.2常见的内存错误及其对策

           发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。

    常见的内存错误及其对策如下:

    u       内存分配未成功,却使用了它。

    编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用mallocnew来申请内存,应该用if(p==NULL) if(p!=NULL)进行防错处理。

     

    u       内存分配虽然成功,但是尚未初始化就引用它。

    犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。

    内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

     

    u       内存分配成功并且已经初始化,但操作越过了内存的边界。

    例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。

     

    u       忘记了释放内存,造成内存泄露。

    含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。

    动态内存的申请与释放必须配对,程序中mallocfree的使用次数一定要相同,否则肯定有错误(new/delete同理)。

     

    u       释放了内存却继续使用它。

    有三种情况:

    1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

    2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

    3)使用freedelete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

     

    l         【规则7-2-1mallocnew申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

    l         【规则7-2-2不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

    l         【规则7-2-3避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

    l         【规则7-2-4动态内存的申请与释放必须配对,防止内存泄漏。

    l         【规则7-2-5freedelete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

    7.3指针与数组的对比

           C++/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。

           数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

    指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

    下面以字符串为例比较指针与数组的特性。

     

    7.3.1 修改内容

           示例7-3-1中,字符数组a的容量是6个字符,其内容为hello\0a的内容可以改变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

     

    char a[] = hello;

    a[0] = X;

    cout << a << endl;

    char *p = world;     // 注意p指向常量字符串

    p[0] = X           // 编译器不能发现该错误

    cout << p << endl;

    示例7-3-1 修改数组和指针的内容

     

    7.3.2 内容复制与比较

        不能对数组名进行直接复制与比较。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较ba的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。

        语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数mallocp申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

     

        // 数组

        char a[] = "hello";

        char b[10];

        strcpy(b, a);           // 不能用   b = a;

        if(strcmp(b, a) == 0)   // 不能用 if (b == a)

        // 指针

        int len = strlen(a);

        char *p = (char *)malloc(sizeof(char)*(len+1));

        strcpy(p,a);            // 不要用 p = a;

        if(strcmp(p, a) == 0)   // 不要用 if (p == a)

    示例7-3-2 数组和指针的内容复制与比较

     

     

    7.3.3 计算内存容量

        用运算符sizeof可以计算出数组的容量(字节数)。示例7-3-3a)中,sizeof(a)的值是12(注意别忘了\0)。指针p指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

    注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。示例7-3-3b)中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)

     

        char a[] = "hello world";

        char *p = a;

        cout<< sizeof(a) << endl;   // 12字节

        cout<< sizeof(p) << endl;   // 4字节

    示例7-3-3a 计算数组和指针的内存容量

          

        void Func(char a[100])

        {

            cout<< sizeof(a) << endl;   // 4字节而不是100字节

    }

    示例7-3-3b 数组退化为指针

    7.4指针参数是如何传递内存的?

           如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例7-4-1中,Test数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?

     

    void GetMemory(char *p, int num)

    {

        p = (char *)malloc(sizeof(char) * num);

    }

    void Test(void)

    {

        char *str = NULL;

        GetMemory(str, 100);    // str 仍然为 NULL 

        strcpy(str, "hello");   // 运行错误

    }

    示例7-4-1 试图用指针参数申请动态内存

     

    毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

    如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例7-4-2

     

    void GetMemory2(char **p, int num)

    {

        *p = (char *)malloc(sizeof(char) * num);

    }

    void Test2(void)

    {

        char *str = NULL;

        GetMemory2(&str, 100); // 注意参数是 &str,而不是str

        strcpy(str, "hello");  

        cout<< str << endl;

        free(str); 

    }

    示例7-4-2用指向指针的指针申请动态内存

     

    由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例7-4-3

     

    char *GetMemory3(int num)

    {

        char *p = (char *)malloc(sizeof(char) * num);

        return p;

    }

    void Test3(void)

    {

        char *str = NULL;

        str = GetMemory3(100); 

        strcpy(str, "hello");

        cout<< str << endl;

        free(str); 

    }

    示例7-4-3 用函数返回值来传递动态内存

     

    用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例7-4-4

     

    char *GetString(void)

    {

        char p[] = "hello world";

        return p;   // 编译器将提出警告

    }

    void Test4(void)

    {

    char *str = NULL;

    str = GetString(); // str 的内容是垃圾

    cout<< str << endl;

    }

    示例7-4-4 return语句返回指向“栈内存”的指针

     

    用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是hello world而是垃圾。

    如果把示例7-4-4改写成示例7-4-5,会怎么样?

     

    char *GetString2(void)

    {

        char *p = "hello world";

        return p;

    }

    void Test5(void)

    {

        char *str = NULL;

        str = GetString2();

        cout<< str << endl;

    }

    示例7-4-5 return语句返回常量字符串

     

    函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。

     

    7.5 freedelete把指针怎么啦?

    别看freedelete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。

    用调试器跟踪示例7-5,发现指针pfree以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p成了“野指针”。如果此时不把p设置为NULL,会让人误以为p是个合法的指针。

    如果程序比较长,我们有时记不住p所指的内存是否已经被释放,在继续使用p之前,通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。

     

        char *p = (char *) malloc(100);

        strcpy(p, hello);

        free(p);        // p 所指的内存被释放,但是p所指的地址仍然不变

        

        if(p != NULL)   // 没有起到防错作用

        {

           strcpy(p, world); // 出错

    }

    示例7-5 p成为野指针

    7.6 动态内存会被自动释放吗?

           函数体内的局部变量在函数结束时自动消亡。很多人误以为示例7-6是正确的。理由是p是局部的指针变量,它消亡的时候会让它所指的动态内存一起完蛋。这是错觉!

     

        void Func(void)

    {

        char *p = (char *) malloc(100); // 动态内存会自动释放吗?

    }

    示例7-6 试图让动态内存自动释放

     

        我们发现指针有一些“似是而非”的特征:

    1)指针消亡了,并不表示它所指的内存会被自动释放。

    2)内存被释放了,并不表示指针会消亡或者成了NULL指针。

    这表明释放内存并不是一件可以草率对待的事。也许有人不服气,一定要找出可以草率行事的理由:

        如果程序终止了运行,一切指针都会消亡,动态内存会被操作系统回收。既然如此,在程序临终前,就可以不必释放内存、不必将指针设置为NULL了。终于可以偷懒而不会发生错误了吧?

        想得美。如果别人把那段程序取出来用到其它地方怎么办?

    7.7 杜绝“野指针”

    “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。

    “野指针”的成因主要有两种:

    1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

        char *p = NULL;

        char *str = (char *) malloc(100);

     

    2)指针pfree或者delete之后,没有置为NULL,让人误以为p是个合法的指针。参见7.5节。

     

    3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

        class A

    {  

    public:

        void Func(void){ cout << Func of class A << endl; }

    };

        void Test(void)

    {

        A *p;

            {

                A a;

                p = &a; // 注意 a 的生命期

    }

            p->Func();      // p是“野指针”

    }

     

    函数Test在执行语句p->Func(),对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。

     

    7.8 有了malloc/free为什么还要new/delete 

           mallocfreeC++/C语言的标准库函数,new/deleteC++的运算符。它们都可用于申请动态内存和释放内存。

    对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free

           因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

    我们先看一看malloc/freenew/delete如何实现对象的动态内存管理,见示例7-8

     

    class Obj

    {

    public :

            Obj(void){ cout << Initialization << endl; }

    ~Obj(void){ cout << Destroy << endl; }

    void    Initialize(void){ cout << Initialization << endl; }

    void    Destroy(void){ cout << Destroy << endl; }

    };

    void UseMallocFree(void)

    {

        Obj *a = (obj *)malloc(sizeof(obj));   // 申请动态内存

        a->Initialize();                        // 初始化

        //

        a->Destroy();   // 清除工作

        free(a);        // 释放内存

    }

    void UseNewDelete(void)

    {

        Obj *a = new Obj; // 申请动态内存并且初始化

        //

        delete a;           // 清除并且释放内存

    }

    示例7-8 malloc/freenew/delete如何实现对象的动态内存管理

     

    Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数InitializeDestroy来完成初始化与清除工作。函数UseNewDelete则简单得多。

    所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/freenew/delete是等价的。

        既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

    如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

    7.9 内存耗尽怎么办?

           如果在申请动态内存时找不到足够大的内存块,mallocnew将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。

    1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:

    void Func(void)

    {

    A *a = new A;

    if(a == NULL)

    {

        return;

        }

    }

     

    2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:

    void Func(void)

    {

    A *a = new A;

    if(a == NULL)

    {

        cout << Memory Exhausted << endl;

        exit(1);

    }

        

    }

     

    3)为newmalloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

     

           上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。

    很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”

           不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。

     

           有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用mallocnew,几乎不可能导致“内存耗尽”。我在Windows 98下用Visual C++编写了测试程序,见示例7-9。这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。

    我可以得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把UnixWindows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。

    我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

     

    void main(void)

    {

        float *p = NULL;

        while(TRUE)

        {

            p = new float[1000000];

            cout << eat memory << endl;

            if(p==NULL)

                exit(1);

        }

    }

    示例7-9试图耗尽操作系统的内存

    7.10 malloc/free 的使用要点

        函数malloc的原型如下:

            void * malloc(size_t size);

        malloc申请一块长度为length的整数类型的内存,程序如下:

            int *p = (int *) malloc(sizeof(int) * length);

    我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

    u       malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

    u       malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:

    cout << sizeof(char) << endl;

    cout << sizeof(int) << endl;

    cout << sizeof(unsigned int) << endl;

    cout << sizeof(long) << endl;

    cout << sizeof(unsigned long) << endl;

    cout << sizeof(float) << endl;

    cout << sizeof(double) << endl;

        cout << sizeof(void *) << endl;

       

        malloc的“()”中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))这样的程序来。

     

    u       函数free的原型如下:

    void free( void * memblock );

        为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果pNULL指针,那么freep无论操作多少次都不会出问题。如果p不是NULL指针,那么freep连续操作两次就会导致程序运行错误。

    7.11 new/delete 的使用要点

           运算符new使用起来要比函数malloc简单得多,例如:

    int *p1 = (int *)malloc(sizeof(int) * length);

    int *p2 = new int[length];

    这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如

    class Obj

    {

    public :

        Obj(void);      // 无参数的构造函数

        Obj(int x);     // 带一个参数的构造函数

    }

    void Test(void)

    {

        Obj *a = new Obj;

        Obj *b = new Obj(1);   // 初值为1

        

        delete a;

        delete b;

    }

    如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如

        Obj *objects = new Obj[100];   // 创建100个动态对象

    不能写成

        Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1

    在用delete释放对象数组时,留意不要丢了符号‘[]’。例如

        delete []objects;   // 正确的用法

    delete objects; // 错误的用法

    后者相当于delete objects[0],漏掉了另外99个对象。

    7.12 一些心得体会

    我认识不少技术不错的C++/C程序员,很少有人能拍拍胸脯说通晓指针与内存管理(包括我自己)。我最初学习C语言时特别怕指针,导致我开发第一个应用软件(约1万行C代码)时没有使用一个指针,全用数组来顶替指针,实在蠢笨得过分。躲避指针不是办法,后来我改写了这个软件,代码量缩小到原先的一半。

    我的经验教训是:

    1)越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。

    2)必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。

     

    8 C++函数的高级特性

    对比于C语言的函数,C++增加了重载(overloaded)、内联(inline)、constvirtual四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,constvirtual机制仅用于类的成员函数。

           重载和内联肯定有其好处才会被C++语言采纳,但是不可以当成免费的午餐而滥用。本章将探究重载和内联的优点与局限性,说明什么情况下应该采用、不该采用以及要警惕错用。

    8.1 函数重载的概念

    8.1.1 重载的起源

        自然语言中,一个词可以有许多不同的含义,即该词被重载了。人们可以通过上下文来判断该词到底是哪种含义。“词的重载”可以使语言更加简练。例如“吃饭”的含义十分广泛,人们没有必要每次非得说清楚具体吃什么不可。别迂腐得象孔已己,说茴香豆的茴字有四种写法。

        C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。这样便于记忆,提高了函数的易用性,这是C++语言采用重载机制的一个理由。例如示例8-1-1中的函数EatBeef,EatFish,EatChicken可以用同一个函数名Eat表示,用不同类型的参数加以区别。

     

     

    void EatBeef(…);       // 可以改为     void Eat(Beef …);

    void EatFish(…);       // 可以改为     void Eat(Fish …);

    void EatChicken(…);    // 可以改为     void Eat(Chicken …);

     

    示例8-1-1 重载函数Eat

     

        C++语言采用重载机制的另一个理由是:类的构造函数需要重载机制。因为C++规定构造函数与类同名(请参见第9章),构造函数只能有一个名字。如果想用几种不同的方法创建对象该怎么办?别无选择,只能用重载机制来实现。所以类可以有多个同名的构造函数。

     

    8.1.2 重载是如何实现的?

        几个同名的重载函数仍然是不同的函数,它们是如何区分的呢?我们自然想到函数接口的两个要素:参数与返回值。

    如果同名函数的参数不同(包括类型、顺序不同),那么容易区别出它们是不同的函数。

    如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能。例如:

    void Function(void);

    int Function (void);

    上述两个函数,第一个没有返回值,第二个的返回值是int类型。如果这样调用函数:

        int x = Function ();

    则可以判断出Function是第二个函数。问题是在C++/C程序中,我们可以忽略函数的返回值。在这种情况下,编译器和程序员都不知道哪个Function函数被调用。

        所以只能靠参数而不能靠返回值类型的不同来区分重载函数。编译器根据参数为每个重载函数产生不同的内部标识符。例如编译器为示例8-1-1中的三个Eat函数产生象_eat_beef_eat_fish_eat_chicken之类的内部标识符(不同的编译器可能产生不同风格的内部标识符)。

     

    如果C++程序要调用已经被编译后的C函数,该怎么办?

    假设某个C函数的声明如下:

    void foo(int x, int y);

    该函数被C编译器编译后在库中的名字为_fooC++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C函数。C++提供了一个C连接交换指定符号externC”来解决这个问题。例如:

    extern “C”

    {

       void foo(int x, int y);

       … // 其它函数

    }

    或者写成

    extern “C”

    {

       #include “myheader.h”

       … // 其它C头文件

    }

    这就告诉C++编译译器,函数foo是个C连接,应该到库中找名字_foo而不是找_foo_int_intC++编译器开发商已经对C标准库的头文件作了externC”处理,所以我们可以用#include 直接引用这些头文件。

     

        注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。例如:

        void Print(…);     // 全局函数

        class A

        {…

            void Print(…); // 成员函数

        }

        不论两个Print函数的参数是否不同,如果类的某个成员函数要调用全局函数Print,为了与成员函数Print区别,全局函数被调用时应加‘::’标志。如

        ::Print(…);    // 表示Print是全局函数而非成员函数

     

    8.1.3 当心隐式类型转换导致重载函数产生二义性

        示例8-1-3中,第一个output函数的参数是int类型,第二个output函数的参数是float类型。由于数字本身没有类型,将数字当作参数时将自动进行类型转换(称为隐式类型转换)。语句output(0.5)将产生编译错误,因为编译器不知道该将0.5转换成int还是float类型的参数。隐式类型转换在很多地方可以简化程序的书写,但是也可能留下隐患。

     

    # include <iostream.h>

    void output( int x);    // 函数声明

    void output( float x); // 函数声明

     

    void output( int x)

    {

        cout << " output int " << x << endl ;

    }

     

    void output( float x)

    {

        cout << " output float " << x << endl ;

    }

     

    void main(void)

    {

        int   x = 1;

        float y = 1.0;

        output(x);          // output int 1

        output(y);          // output float 1

        output(1);          // output int 1

    // output(0.5);        // error! ambiguous call, 因为自动类型转换

        output(int(0.5));   // output int 0

        output(float(0.5)); // output float 0.5

    }

    示例8-1-3 隐式类型转换导致重载函数产生二义性

     

    8.2 成员函数的重载、覆盖与隐藏

        成员函数的重载、覆盖(override)与隐藏很容易混淆,C++程序员必须要搞清楚概念,否则错误将防不胜防。

     

    8.2.1 重载与覆盖

        成员函数被重载的特征:

    1)相同的范围(在同一个类中);

    2)函数名字相同;

    3)参数不同;

    4virtual关键字可有可无。

        覆盖是指派生类函数覆盖基类函数,特征是:

    1)不同的范围(分别位于派生类与基类);

    2)函数名字相同;

    3)参数相同;

    4)基类函数必须有virtual关键字。

        示例8-2-1中,函数Base::f(int)Base::f(float)相互重载,而Base::g(void)Derived::g(void)覆盖。

     

    #include <iostream.h>

        class Base

    {

    public:

                 void f(int x){ cout << "Base::f(int) " << x << endl; }

    void f(float x){ cout << "Base::f(float) " << x << endl; }

         virtual void g(void){ cout << "Base::g(void)" << endl;}

    };

     

        class Derived : public Base

    {

    public:

         virtual void g(void){ cout << "Derived::g(void)" << endl;}

    };

     

        void main(void)

        {

         Derived d;

         Base *pb = &d;

         pb->f(42);        // Base::f(int) 42

         pb->f(3.14f);     // Base::f(float) 3.14

         pb->g();          // Derived::g(void)

    }

    示例8-2-1成员函数的重载和覆盖

       

    8.2.2 令人迷惑的隐藏规则

        本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

    1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

    2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

        示例程序8-2-2a)中:

    1)函数Derived::f(float)覆盖了Base::f(float)

    2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。

    3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。

     

    #include <iostream.h>

        class Base

    {

    public:

        virtual void f(float x){ cout << "Base::f(float) " << x << endl; }

    void g(float x){ cout << "Base::g(float) " << x << endl; }

                void h(float x){ cout << "Base::h(float) " << x << endl; }

    };

        class Derived : public Base

    {

    public:

        virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }

    void g(int x){ cout << "Derived::g(int) " << x << endl; }

                void h(float x){ cout << "Derived::h(float) " << x << endl; }

    };

    示例8-2-2a)成员函数的重载、覆盖和隐藏

     

        据作者考察,很多C++程序员没有意识到有“隐藏”这回事。由于认识不够深刻,“隐藏”的发生可谓神出鬼没,常常产生令人迷惑的结果。

    示例8-2-2b)中,bpdp指向同一地址,按理说运行结果应该是相同的,可事实并非这样。

     

    void main(void)

    {

    Derived d;

    Base *pb = &d;

    Derived *pd = &d;

    // Good : behavior depends solely on type of the object

    pb->f(3.14f); // Derived::f(float) 3.14

    pd->f(3.14f); // Derived::f(float) 3.14

     

    // Bad : behavior depends on type of the pointer

    pb->g(3.14f); // Base::g(float) 3.14

    pd->g(3.14f); // Derived::g(int) 3        (surprise!)

     

    // Bad : behavior depends on type of the pointer

    pb->h(3.14f); // Base::h(float) 3.14      (surprise!)

    pd->h(3.14f); // Derived::h(float) 3.14

    }

    示例8-2-2b) 重载、覆盖和隐藏的比较

    8.2.3 摆脱隐藏

        隐藏规则引起了不少麻烦。示例8-2-3程序中,语句pd->f(10)的本意是想调用函数Base::f(int),但是Base::f(int)不幸被Derived::f(char *)隐藏了。由于数字10不能被隐式地转化为字符串,所以在编译时出错。

     

    class Base

    {

    public:

    void f(int x);

    };

    class Derived : public Base

    {

    public:

    void f(char *str);

    };

    void Test(void)

    {

    Derived *pd = new Derived;

    pd->f(10);    // error

    }

    示例8-2-3 由于隐藏而导致错误

     

        从示例8-2-3看来,隐藏规则似乎很愚蠢。但是隐藏规则至少有两个存在的理由:

    u       写语句pd->f(10)的人可能真的想调用Derived::f(char *)函数,只是他误将参数写错了。有了隐藏规则,编译器就可以明确指出错误,这未必不是好事。否则,编译器会静悄悄地将错就错,程序员将很难发现这个错误,流下祸根。

    u       假如类Derived有多个基类(多重继承),有时搞不清楚哪些基类定义了函数f。如果没有隐藏规则,那么pd->f(10)可能会调用一个出乎意料的基类函数f。尽管隐藏规则看起来不怎么有道理,但它的确能消灭这些意外。

     

    示例8-2-3中,如果语句pd->f(10)一定要调用函数Base::f(int),那么将类Derived修改为如下即可。

    class Derived : public Base

    {

    public:

    void f(char *str);

    void f(int x) { Base::f(x); }

    };

    8.3 参数的缺省值

    有一些参数的值在每次函数调用时都相同,书写这样的语句会使人厌烦。C++语言采用参数的缺省值使书写变得简洁(在编译时,缺省值由编译器自动插入)。

        参数缺省值的使用规则:

    l         【规则8-3-1参数缺省值只能出现在函数的声明中,而不能出现在定义体中。

    例如:

        void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中

     

        void Foo(int x=0, int y=0)     // 错误,缺省值出现在函数的定义体中

        {

        }

    为什么会这样?我想是有两个原因:一是函数的实现(定义)本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体中。二是参数的缺省值可能会改动,显然修改函数的声明比修改函数的定义要方便。

     

    l         【规则8-3-2如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。

    正确的示例如下:

    void Foo(int x, int y=0, int z=0);

    错误的示例如下:

    void Foo(int x=0, int y, int z=0); 

     

    要注意,使用参数的缺省值并没有赋予函数新的功能,仅仅是使书写变得简洁一些。它可能会提高函数的易用性,但是也可能会降低函数的可理解性。所以我们只能适当地使用参数的缺省值,要防止使用不当产生负面效果。示例8-3-2中,不合理地使用参数的缺省值将导致重载函数output产生二义性。

     

    #include <iostream.h>

    void output( int x);

    void output( int x, float y=0.0);

     

    void output( int x)

    {

        cout << " output int " << x << endl ;

    }

     

    void output( int x, float y)

    {

        cout << " output int " << x << " and float " << y << endl ;

    }

     

    void main(void)

    {

        int x=1;

        float y=0.5;

    // output(x);          // error! ambiguous call

        output(x,y);        // output int 1 and float 0.5

    }

     

    示例8-3-2 参数的缺省值将导致重载函数产生二义性

    8.4 运算符重载

    8.4.1 概念

        C++语言中,可以用关键字operator加上运算符来表示函数,叫做运算符重载。例如两个复数相加函数:

        Complex Add(const Complex &a, const Complex &b);

    可以用运算符重载来表示:

        Complex operator +(const Complex &a, const Complex &b);

        运算符与普通函数在调用时的不同之处是:对于普通函数,参数出现在圆括号内;而对于运算符,参数出现在其左、右侧。例如

        Complex a, b, c;

        

        c = Add(a, b); // 用普通函数

        c = a + b;      // 用运算符 +

        如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。

        如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。

        从语法上讲,运算符既可以定义为全局函数,也可以定义为成员函数。文献[Murray , p44-p47]对此问题作了较多的阐述,并总结了表8-4-1的规则。

     

    运算符

    规则

    所有的一元运算符

    建议重载为成员函数

    = () [] ->

    只能重载为成员函数

    += -= /= *= &= |= ~= %= >>= <<=

    建议重载为成员函数

    所有其它运算符

    建议重载为全局函数

    8-4-1 运算符的重载规则

     

    由于C++语言支持函数重载,才能将运算符当成函数来用,C语言就不行。我们要以平常心来对待运算符重载:

    1)不要过分担心自己不会用,它的本质仍然是程序员们熟悉的函数。

    2)不要过分热心地使用,如果它不能使代码变得更加易读易写,那就别用,否则会自找麻烦。

     

    8.4.2 不能被重载的运算符

        C++运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。

    1)不能改变C++内部数据类型(如int,float等)的运算符。

    2)不能重载‘.’,因为‘.’在类中对任何成员都有意义,已经成为标准用法。

    3)不能重载目前C++运算符集合中没有的符号,如#,@,$等。原因有两点,一是难以理解,二是难以确定优先级。

    4)对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。

    8.5 函数内联

    8.5.1 用内联取代宏代码

        C++ 语言支持函数内联,其目的是为了提高函数的执行效率(速度)。

        C程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。例如

        #define MAX(a, b)       (a) > (b) ? (a) : (b)

    语句

    result = MAX(i, j) + 2 ;

    将被预处理器解释为

        result = (i) > (j) ? (i) : (j) + 2 ;

    由于运算符‘+’比运算符‘:’的优先级高,所以上述语句并不等价于期望的

        result = ( (i) > (j) ? (i) : (j) ) + 2 ;

    如果把宏代码改写为

        #define MAX(a, b)       ( (a) > (b) ? (a) : (b) )

    则可以解决由优先级引起的错误。但是即使使用修改后的宏代码也不是万无一失的,例如语句 

    result = MAX(i++, j);

    将被预处理器解释为

        result = (i++) > (j) ? (i++) : (j);

        对于C++ 而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。

     

    让我们看看C++ 的“函数内联”是如何工作的。对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。

    C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在C++ 程序中,应该用内联函数取代所有宏代码,“断言assert”恐怕是唯一的例外。assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。如果assert是函数,由于函数调用会引起内存、代码的变动,那么将导致Debug版本与Release版本存在差异。所以assert不是函数,而是宏。(参见6.5节“使用断言”)

     

    8.5.2 内联函数的编程风格

        关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。如下风格的函数Foo不能成为内联函数:

        inline void Foo(int x, int y); // inline仅与函数声明放在一起

        void Foo(int x, int y)

        {

            

        }

    而如下风格的函数Foo则成为内联函数:

        void Foo(int x, int y);    

        inline void Foo(int x, int y)   // inline与函数定义体放在一起

        {

            

        }

        所以说,inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline关键字,但我认为inline不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

        定义在类声明之中的成员函数将自动地成为内联函数,例如

        class A

        {

    public:

            void Foo(int x, int y) { … }  // 自动地成为内联函数

        }

    将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:

        // 头文件

    class A

        {

    public:

            void Foo(int x, int y)

        }

        // 定义文件

        inline void A::Foo(int x, int y)

    {

    }

     

    8.5.3 慎用内联

        内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?

        如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?

        内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:

    1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

    2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

        类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。

    一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。

    8.6 一些心得体会

        C++ 语言中的重载、内联、缺省参数、隐式转换等机制展现了很多优点,但是这些优点的背后都隐藏着一些隐患。正如人们的饮食,少食和暴食都不可取,应当恰到好处。我们要辨证地看待C++的新机制,应该恰如其分地使用它们。虽然这会使我们编程时多费一些心思,少了一些痛快,但这才是编程的艺术。

     

    9 类的构造函数、析构函数与赋值函数

    构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。

           每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如

        A(void);                    // 缺省的无参数构造函数

        A(const A &a);              // 缺省的拷贝构造函数

        ~A(void);                   // 缺省的析构函数

        A & operate =(const A &a); // 缺省的赋值函数

     

    这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?

    原因如下:

    1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup的好心好意白费了。

    2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。

          

    对于那些没有吃够苦头的C++程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。

    本章以类String的设计与实现为例,深入阐述被很多教科书忽视了的道理。String的结构如下:

        class String

        {

         public:

            String(const char *str = NULL); // 普通构造函数

            String(const String &other);    // 拷贝构造函数

            ~ String(void);                 // 析构函数

            String & operate =(const String &other);    // 赋值函数

         private:

            char  *m_data;                // 用于保存字符串

        };

    9.1 构造函数与析构函数的起源

           作为比C更先进的语言,C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。

           根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。Stroustrup在设计C++语言时充分考虑了这个问题并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。

           构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行。Stroustrup的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就加前缀~以示区别

    除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。构造函数与析构函数的使命非常明确,就象出生与死亡,光溜溜地来光溜溜地去。如果它们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(以上典故参考了文献[Eekel, p55-p56]

    9.2 构造函数的初始化表

           构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。

           构造函数初始化表的使用规则:

    u       如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。

    例如

        class A

        {

            A(int x);       // A的构造函数

    }; 

        class B : public A

        {

            B(int x, int y);// B的构造函数

        };

        B::B(int x, int y)

         : A(x)             // 在初始化表里调用A的构造函数

        {

         

    }  

    u       类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化(参见5.4节)。

    u       类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。

        非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如

        class A

    {

        A(void);                // 无参数构造函数

        A(const A &other);      // 拷贝构造函数

        A & operate =( const A &other); // 赋值函数

    }

     

        class B

        {

         public:

            B(const A &a); // B的构造函数

         private: 

            A m_a;         // 成员对象

    };

     

    示例9-2(a)中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。

    示例9-2 (b)中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a

     

    B::B(const A &a)

     : m_a(a)          

    {

       

    }

    B::B(const A &a)

    {

    m_a = a;

    }

     示例9-2(a) 成员对象在初始化表中被初始化      示例9-2(b) 成员对象在函数体内被初始化

     

    对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。若类F的声明如下:

    class F

    {

     public:

        F(int x, int y);        // 构造函数

     private:

        int m_x, m_y;

        int m_i, m_j;

    }

    示例9-2(c)F的构造函数采用了第一种初始化方式,示例9-2(d)F的构造函数采用了第二种初始化方式。

     

    F::F(int x, int y)

     : m_x(x), m_y(y)          

    {

       m_i = 0;

       m_j = 0;

    }

    F::F(int x, int y)

    {

       m_x = x;

       m_y = y;

       m_i = 0;

       m_j = 0;

    }

     示例9-2(c) 数据成员在初始化表中被初始化     示例9-2(d) 数据成员在函数体内被初始化

    9.3 构造和析构的次序

           构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。

    一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。[Eckel, p260-261]

    9.4 示例:类String的构造函数与析构函数

           // String的普通构造函数

           String::String(const char *str)

    {

        if(str==NULL)

        {

            m_data = new char[1];

            *m_data = \0;

        }  

        else

        {

            int length = strlen(str);

            m_data = new char[length+1];

            strcpy(m_data, str);

        }

    }  

     

    // String的析构函数

           String::~String(void)

    {

        delete [] m_data;  

    // 由于m_data是内部数据类型,也可以写成 delete m_data;

           }

    9.5 不要轻视拷贝构造函数与赋值函数

           由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:

    u       本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。

    现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄露;二是b.m_dataa.m_data指向同一块内存,ab任何一方变动都会影响另一方;三是在对象被析构时,m_data被释放了两次。

     

    u       拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?

    String a(hello);

    String b(world);

    String c = a; // 调用了拷贝构造函数,最好写成 c(a);

    c = b; // 调用了赋值函数

    本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。

    9.6 示例:类String的拷贝构造函数与赋值函数

        // 拷贝构造函数

        String::String(const String &other)

        {  

    // 允许操作other的私有成员m_data

        int length = strlen(other.m_data); 

        m_data = new char[length+1];

        strcpy(m_data, other.m_data);

    }

     

    // 赋值函数

        String & String::operate =(const String &other)

        {  

            // (1) 检查自赋值

            if(this == &other)

                return *this;

           

            // (2) 释放原有的内存资源

            delete [] m_data;

           

            // 3)分配新的内存资源,并复制内容

        int length = strlen(other.m_data); 

        m_data = new char[length+1];

            strcpy(m_data, other.m_data);

           

            // 4)返回本对象的引用

            return *this;

    }  

       

        String拷贝构造函数与普通构造函数(参见9.4节)的区别是:在函数入口处无需NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL

        String的赋值函数比构造函数复杂得多,分四步实现:

    1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如

       

    // 内容自赋值

    b = a;

    c = b;

    a = c; 

    // 地址自赋值

    b = &a;

    a = *b;

     

    也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”

    他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if语句

    if(this == &other)

    错写成为

        if( *this == other)

    2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。

    3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy则连‘\0’一起复制。

    4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?

    不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。

    9.7 偷懒的办法处理拷贝构造函数与赋值函数

           如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?

           偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。

    例如:

        class A

        

         private:

            A(const A &a);             // 私有的拷贝构造函数

            A & operate =(const A &a); // 私有的赋值函数

        };

     

    如果有人试图编写如下程序:

        A b(a);    // 调用了私有的拷贝构造函数

        b = a;      // 调用了私有的赋值函数

    编译器将指出错误,因为外界不可以操作A的私有函数。

    9.8 如何在派生类中实现类的基本函数

           基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:

    u       派生类的构造函数应在其初始化表里调用基类的构造函数。

    u       基类与派生类的析构函数应该为虚(即加virtual关键字)。例如

    #include <iostream.h>

    class Base

    {

     public:

        virtual ~Base() { cout<< "~Base" << endl ; }

    };

     

    class Derived : public Base

    {

     public:

        virtual ~Derived() { cout<< "~Derived" << endl ; }

    };

     

    void main(void)

    {

        Base * pB = new Derived; // upcast

        delete pB;

    }

     

    输出结果为:

           ~Derived

           ~Base

    如果析构函数不为虚,那么输出结果为

           ~Base

     

    u       在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:

    class Base

    {

     public:

        Base & operate =(const Base &other);    // Base的赋值函数

     private:

        int m_i, m_j, m_k;

    };

     

    class Derived : public Base

    {

     public:

        Derived & operate =(const Derived &other); // Derived的赋值函数

     private:

        int m_x, m_y, m_z;

    };

     

    Derived & Derived::operate =(const Derived &other)

    {

        //1)检查自赋值

        if(this == &other)

            return *this;

     

        //2)对基类的数据成员重新赋值

        Base::operate =(other); // 因为不能直接操作私有数据成员

     

        //3)对派生类的数据成员赋值

        m_x = other.m_x;

        m_y = other.m_y;

        m_z = other.m_z;

     

        //4)返回本对象的引用

        return *this;

    }

     

    9.9 一些心得体会

    有些C++程序设计书籍称构造函数、析构函数和赋值函数是类的“Big-Three”,它们的确是任何类最重要的函数,不容轻视。

    也许你认为本章的内容已经够多了,学会了就能平安无事,我不能作这个保证。如果你希望吃透“Big-Three”,请好好阅读参考文献[Cline] [Meyers] [Murry]

     

     

     

    10 类的继承与组合

     

    对象(Object)是类(Class)的一个实例(Instance)。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。

    对于C++程序而言,设计孤立的类是比较容易的,难的是正确设计基类及其派生类。本章仅仅论述“继承”(Inheritance)和“组合”(Composition)的概念。

    注意,当前面向对象技术的应用热点是COMCORBA,这些内容超出了C++教材的范畴,请阅读COMCORBA相关论著。

    10.1 继承

    如果A是基类,BA的派生类,那么B将继承A的数据和函数。例如:

           class A

    {

     public:

                  void Func1(void);

                  void Func2(void);

    };

     

    class B : public A

    {

     public:

                  void Func3(void);

                  void Func4(void);

    };

     

           main()

    {

                  B b;                   

                  b.Func1();             // BA继承了函数Func1

                  b.Func2();             // BA继承了函数Func2

                  b.Func3();

                  b.Func4();

    }

     

    这个简单的示例程序说明了一个事实:C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”。我们应当给“继承”立一些使用规则。

     

    l         【规则10-1-1如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“白吃白不吃”,让一个好端端的健壮青年无缘无故地吃人参补身体。

    l         【规则10-1-2若在逻辑上BA的“一种”(a kind of ),则允许B继承A的功能和属性。例如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类Man可以从类Human派生,类Boy可以从类Man派生。

             class Human

    {

                      

    };

             class Man : public Human

    {

                      

    };

             class Boy : public Man

    {

                      

    };

     

    u       注意事项

    【规则10-1-2看起来很简单,但是实际应用时可能会有意外,继承的概念在程序世界与现实世界并不完全相同。

    例如从生物学角度讲,鸵鸟(Ostrich)是鸟(Bird)的一种,按理说类Ostrich应该可以从类Bird派生。但是鸵鸟不能飞,那么Ostrich::Fly是什么东西?

    class Bird

    {

    public:   

           virtual void Fly(void);

    };

     

    class Ostrich : public Bird

    {

    };

     

    例如从数学角度讲,圆(Circle)是一种特殊的椭圆(Ellipse),按理说类Circle应该可以从类Ellipse派生。但是椭圆有长轴和短轴,如果圆继承了椭圆的长轴和短轴,岂非画蛇添足?

           所以更加严格的继承规则应当是:若在逻辑上BA的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。

    10.2 组合

    l         【规则10-2-1若在逻辑上AB的“一部分”(a part of),则不允许BA派生,而是要用A和其它东西组合出B

    例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head应该由类EyeNoseMouthEar组合而成,不是派生而成。如示例10-2-1所示。

     

    class Eye

    {
     public:

    void Look(void); 

    };

    class Nose

    {
     public:

    void Smell(void);

    };

    class Mouth

    {
     public:

    void Eat(void);    

    };

    class Ear

    {
     public:

    void Listen(void);

    };

    // 正确的设计,虽然代码冗长。

    class Head

    {

     public:

                  void       Look(void)     { m_eye.Look(); }

                  void       Smell(void)     { m_nose.Smell(); }

                  void       Eat(void) { m_mouth.Eat(); }

                  void       Listen(void)    { m_ear.Listen(); }

     private:

                  Eye      m_eye;

                  Nose    m_nose;

                  Mouth m_mouth;

                  Ear       m_ear;

    };

    示例10-2-1 HeadEyeNoseMouthEar组合而成

          

    如果允许HeadEyeNoseMouthEar派生而成,那么Head将自动具有Look SmellEatListen这些功能。示例10-2-2十分简短并且运行正确,但是这种设计方法却是不对的。

     

           // 功能正确并且代码简洁,但是设计方法不对。

    class Head : public Eye, public Nose, public Mouth, public Ear

    {

    };

    示例10-2-2 HeadEyeNoseMouthEar派生而成

     

    一只公鸡使劲地追打一只刚下了蛋的母鸡,你知道为什么吗?

    因为母鸡下了鸭蛋。

    很多程序员经不起“继承”的诱惑而犯下设计错误。“运行正确”的程序不见得是高质量的程序,此处就是一个例证。

     

     

    11 其它编程经验

    11.1 使用const提高函数的健壮性

    看到const关键字,C++程序员首先想到的可能是const常量。这可不是良好的条件反射。如果只知道用const定义常量,那么相当于把火药仅用于制作鞭炮。const更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。

    constconstant的缩写,“恒定不变”的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。

     

    11.1.1 const修饰函数的参数

    如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加const修饰,否则该参数将失去输出功能。

    const只能修饰输入参数:

    u       如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。

    例如StringCopy函数:

            void StringCopy(char *strDestination, const char *strSource);

    其中strSource是输入参数,strDestination是输出参数。给strSource加上const修饰后,如果函数体内的语句试图改动strSource的内容,编译器将指出错误。

     

    u       如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰。

    例如不要将函数void Func1(int x) 写成void Func1(const int x)。同理不要将函数void Func2(A a) 写成void Func2(const A a)。其中A为用户自定义的数据类型。

     

    u       对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。

    为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)

    以此类推,是否应将void Func(int x) 改写为void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。

        问题是如此的缠绵,我只好将“const &”修饰输入参数的用法总结一下,如表11-1-1所示。

     

    对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)

     

    对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)

     

    11-1-1 const &”修饰输入参数的规则

     

    11.1.2 const修饰函数的返回值

    u       如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。

    例如函数

            const char * GetString(void);

    如下语句将出现编译错误:

            char *str = GetString();

    正确的用法是

            const char *str = GetString();

     

    u       如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值。

        例如不要把函数int GetInt(void) 写成const int GetInt(void)

        同理不要把函数A GetA(void) 写成const A GetA(void),其中A为用户自定义的数据类型。

        如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的“拷贝”还是仅返回“别名”就可以了,否则程序会出错。见6.2节“返回值的规则”。

     

    u       函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。

    例如

        class A

        {…

            A & operate = (const A &other); // 赋值函数

        };

        A a, b, c;      // a, b, c A的对象

        

        a = b = c;          // 正常的链式赋值

        (a = b) = c;        // 不正常的链式赋值,但合法

    如果将赋值函数的返回值加const修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c仍然正确,但是语句 (a = b) = c 则是非法的。

     

    11.1.3 const成员函数

        任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。

    以下程序中,stack的成员函数GetCount仅用于计数,从逻辑上讲GetCount应当为const函数。编译器将指出GetCount函数中的错误。

        class Stack

    {

         public:

            void    Push(int elem);

            int     Pop(void);

            int     GetCount(void) const; // const成员函数

         private:

            int     m_num;

            int     m_data[100];

    };

     

        int Stack::GetCount(void) const

    {
            ++ m_num;   // 
    编译错误,企图修改数据成员m_num

        Pop();     // 编译错误,企图调用非const函数

        return m_num;

        }

        const成员函数的声明看起来怪怪的:const关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。

    11.2 提高程序的效率

    程序的时间效率是指运行速度,空间效率是指程序占用内存或者外存的状况。

    全局效率是指站在整个系统的角度上考虑的效率,局部效率是指站在模块或函数角度上考虑的效率。

     

    l         【规则11-2-1不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。

     

    l         【规则11-2-2以提高程序的全局效率为主,提高局部效率为辅。

     

    l         【规则11-2-3在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。

     

    l         【规则11-2-4先优化数据结构和算法,再优化执行代码。

     

    l         【规则11-2-5有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。

     

    l         【规则11-2-6不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。

     

    11.3 一些有益的建议

    ²        【建议11-3-1当心那些视觉上不易分辨的操作符发生书写错误。

    我们经常会把“==”误写成“=”,象“||”、“&&”、“<=”、“>=”这类符号也很容易发生“丢1”失误。然而编译器却不一定能自动指出这类错误。

     

    ²        【建议11-3-2变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。

     

    ²        【建议11-3-3当心变量的初值、缺省值错误,或者精度不够。

     

    ²        【建议11-3-4当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。

     

    ²        【建议11-3-5当心变量发生上溢或下溢,数组的下标越界。

     

    ²        【建议11-3-6当心忘记编写错误处理程序,当心错误处理程序本身有误。

     

    ²        【建议11-3-7当心文件I/O有错误。

     

    ²        【建议11-3-8避免编写技巧性很高代码。

     

    ²        【建议11-3-9不要设计面面俱到、非常灵活的数据结构。

     

    ²        【建议11-3-10如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。

     

    ²        【建议11-3-11尽量使用标准库函数,不要“发明”已经存在的库函数。

     

    ²        【建议11-3-12尽量不要使用与具体硬件或软件环境关系密切的变量。

     

    ²        【建议11-3-13把编译器的选择项设置为最严格状态

     

    ²        【建议11-3-14如果可能的话,使用PC-LintLogiScope等工具进行代码审查

     

     

     

    参考文献

    [Cline] Marshall P. Cline and Greg A. Lomow, C++ FAQs, Addison-Wesley, 1995

     

    [Eckel] Bruce Eckel, Thinking in C++C++ 编程思想,刘宗田 等译),机械工业出版社,2000

     

    [Maguire] Steve Maguire, Writing Clean Code(编程精粹,姜静波 等译),电子工业出版社,1993

     

    [Meyers] Scott Meyers, Effective C++, Addison-Wesley, 1992

     

    [Murry] Robert B. Murry, C++ Strategies and Tactics, Addison-Wesley, 1993

     

    [Summit] Steve Summit, C Programming FAQs, Addison-Wesley, 1996

     

     

    附录C++/C代码审查表

    文件结构

    重要性

    审查项

    结论

     

    头文件和定义文件的名称是否合理?

     

     

    头文件和定义文件的目录结构是否合理?

     

     

    版权和版本声明是否完整?

     

    重要

    头文件是否使用了 ifndef/define/endif 预处理块?

     

     

    头文件中是否只存放“声明”而不存放“定义”

     

     

    ……

     

    程序的版式

    重要性

    审查项

    结论

     

    空行是否得体?

     

     

    代码行内的空格是否得体?

     

     

    长行拆分是否得体?

     

     

    “{”  “}” 是否各占一行并且对齐于同一列?

     

    重要

    一行代码是否只做一件事?如只定义一个变量,只写一条语句。

     

    重要

    Ifforwhiledo等语句自占一行,不论执行语句多少都要加“{}”。

     

    重要

    在定义变量(或参数)时,是否将修饰符 *   紧靠变量名?

     

     

    注释是否清晰并且必要?

     

    重要

    注释是否有错误或者可能导致误解?

     

    重要

    类结构的public, protected, private顺序是否在所有的程序中保持一致?

     

     

    ……

     

    命名规则

    重要性

    审查项

    结论

    重要

    命名规则是否与所采用的操作系统或开发工具的风格保持一致?

     

     

    标识符是否直观且可以拼读?

     

     

    标识符的长度应当符合“min-length && max-information”原则?

     

    重要

    程序中是否出现相同的局部变量和全部变量?

     

     

    类名、函数名、变量和参数、常量的书写格式是否遵循一定的规则?

     

     

    静态变量、全局变量、类的成员变量是否加前缀?

     

     

    ……

     

    表达式与基本语句

    重要性

    审查项

    结论

    重要

    如果代码行中的运算符比较多,是否已经用括号清楚地确定表达式的操作顺序?

     

     

    是否编写太复杂或者多用途的复合表达式?

     

    重要

    是否将复合表达式与“真正的数学表达式”混淆?

     

    重要

    是否用隐含错误的方式写if语句例如

    1)将布尔变量直接与TRUEFALSE或者10进行比较。

    2)将浮点变量用“==”或“!=”与任何数字比较

    3)将指针变量用“==”或“!=”与NULL比较

     

     

    如果循环体内存在逻辑判断,并且循环次数很大,是否已经将逻辑判断移到循环体的外面?

     

    重要

    Case语句的结尾是否忘了加break

     

    重要

    是否忘记写switchdefault分支?

     

    重要

    使用goto 语句时是否留下隐患例如跳过了某些对象的构造、变量的初始化、重要的计算等。

     

     

    ……

     

    常量

    重要性

    审查项

    结论

     

    是否使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串?

     

     

    C++ 程序中,是否用const常量取代宏常量?

     

    重要

    如果某一常量与其它常量密切相关,是否在定义中包含了这种关系?

     

     

    是否误解了类中的const数据成员?因为const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。

     

     

    ……

     

    函数设计

    重要性

    审查项

    结论

     

    参数的书写是否完整?不要贪图省事只写参数的类型而省略参数名字。

     

     

    参数命名、顺序是否合理?

     

     

    参数的个数是否太多?

     

     

    是否使用类型和数目不确定的参数?

     

     

    是否省略了函数返回值的类型?

     

     

    函数名字与返回值类型在语义上是否冲突?

     

    重要

    是否将正常值和错误标志混在一起返回?正常值应当用输出参数获得,而错误标志用return语句返回。

     

    重要

    在函数体的“入口处”,是否用assert对参数的有效性进行检查?

     

    重要

    使用滥用了assert 例如混淆非法情况与错误情况,后者是必然存在的并且是一定要作出处理的。

     

    重要

    return语句是否返回指向“栈内存”的“指针”或者“引用”?

     

     

    是否使用const提高函数的健壮性?const可以强制保护函数的参数、返回值,甚至函数的定义体。“Use const whenever you need

     

     

    ……

     

    内存管理

    重要性

    审查项

    结论

    重要

    mallocnew申请内存之后,是否立即检查指针值是否为NULL?(防止使用指针值为NULL的内存)

     

    重要

    是否忘记为数组和动态内存赋初值?(防止将未被初始化的内存作为右值使用)

     

    重要

    数组或指针的下标是否越界?

     

    重要

    动态内存的申请与释放是否配对?(防止内存泄漏)

     

    重要

    是否有效地处理了“内存耗尽”问题?

     

    重要

    是否修改“指向常量的指针”的内容?

     

    重要

    是否出现野指针?例如

    1)指针变量没有被初始化。

    2)用freedelete释放了内存之后,忘记将指针设置为NULL

     

    重要

    是否将malloc/free  new/delete 混淆使用?

     

    重要

    malloc语句是否正确无误?例如字节数是否正确?类型转换是否正确?

     

    重要

    在创建与释放动态对象数组时,new/delete的语句是否正确无误?

     

     

    ……

     

    C++ 函数的高级特性

    重要性

    审查项

    结论

     

    重载函数是否有二义性?

     

    重要

    是否混淆了成员函数的重载、覆盖与隐藏?

     

     

    运算符的重载是否符合制定的编程规范?

     

     

    是否滥用内联函数?例如函数体内的代码比较长,函数体内出现循环。

     

    重要

    是否用内联函数取代了宏代码?

     

     

    ……

     

    类的构造函数、析构函数和赋值函数

    重要性

    审查项

    结论

    重要

    是否违背编程规范而让C++ 编译器自动为类产生四个缺省的函数:1缺省的无参数构造函数;(2)缺省的拷贝构造函数;(3)缺省的析构函数;(4)缺省的赋值函数。

     

    重要

    构造函数中是否遗漏了某些初始化工作?

     

    重要

    是否正确地使用构造函数的初始化表?

     

    重要

    析构函数中是否遗漏了某些清除工作?

     

     

    是否错写、错用了拷贝构造函数和赋值函数?

     

    重要

    赋值函数一般分四个步骤:(1)检查自赋值;(2)释放原有内存资源;(3)分配新的内存资源,并复制内容;(4)返回 *this。是否遗漏了重要步骤?

     

    重要

    是否正确地编写了派生类的构造函数、析构函数、赋值函数?注意事项:

    1)派生类不可能继承基类的构造函数、析构函数、赋值函数。

    2)派生类的构造函数应在其初始化表里调用基类的构造函数。

    3)基类与派生类的析构函数应该为虚(即加virtual关键字)。

    4)在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。

     

     

    ……

     

    类的高级特性

    重要性

    审查项

    结论

    重要

    是否违背了继承和组合的规则?

    1)若在逻辑上BA的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。

    2)若在逻辑上AB的“一部分”(a part of),则不允许BA派生,而是要用A和其它东西组合出B

     

     

    ……

     

    其它常见问题

    重要性

    审查项

    结论

    重要

    数据类型问题:

    (1)变量的数据类型有错误吗?

    (2)存在不同数据类型的赋值吗?

    (3)存在不同数据类型的比较吗?

     

    重要

    变量值问题:

    (1)变量的初始化或缺省值有错误吗?

    (2)变量发生上溢或下溢吗?

    (3)变量的精度够吗?

     

    重要

    逻辑判断问题:

    (1)由于精度原因导致比较无效吗?

    (2)表达式中的优先级有误吗?

    (3)逻辑判断结果颠倒吗?

     

    重要

    循环问题:

    (1)循环终止条件不正确吗?

    (2)无法正常终止(死循环)吗?

    (3)错误地修改循环变量吗?

    (4)存在误差累积吗?

     

    重要

    错误处理问题:

    (1)忘记进行错误处理吗?

    (2)错误处理程序块一直没有机会被运行?

    (3)错误处理程序块本身就有毛病吗?如报告的错误与实际错误不一致,处理方式不正确等等。

    (4)错误处理程序块是“马后炮”吗?如在被它被调用之前软件已经出错。

     

    重要

    文件I/O问题:

    (1)对不存在的或者错误的文件进行操作吗?

    (2)文件以不正确的方式打开吗?

    (3)文件结束判断不正确吗?

    (4)没有正确地关闭文件吗?

     

     

     

    附录C++/C试题

           本试题仅用于考查C++/C程序员的基本编程技能。内容限于C++/C常用语法,不涉及数据结构、算法以及深奥的语法。考试成绩能反映出考生的编程质量以及对C++/C的理解程度,但不能反映考生的智力和软件开发能力。

           笔试时间90分钟。请考生认真答题,切勿轻视。

     

    一、请填写BOOL , float, 指针变量 与“零值”比较的 if 语句。(10分)

    提示:这里“零值”可以是0, 0.0 , FALSE或者“空指针”。例如 int 变量 n 与“零值”比较的 if 语句为:

        if ( n == 0 )

        if ( n != 0 )

    以此类推。

     

    请写出 BOOL flag 与“零值”比较的 if 语句:

     

     

    请写出 float x 与“零值”比较的 if 语句:

     

     

     

    请写出 char  *p 与“零值”比较的 if 语句:

     

     

     

    二、以下为Windows NT下的32C++程序,请计算sizeof的值(10分)

     

           char str[] = “Hello” ;

           char   *p = str ;

    int     n = 10;

    请计算

    sizeof (str ) =       

             

    sizeof ( p ) =       

              

    sizeof ( n ) =

    void Func ( char str[100])

    {

    请计算

     sizeof( str ) =   

    }

     

    void *p = malloc( 100 );

    请计算

    sizeof ( p ) =

     

     

    三、简答题(25分)

     

    1、头文件中的 ifndef/define/endif 干什么用?

     

     

     

    2#include <filename.h>    #include “filename.h” 有什么区别?

     

     

     

    3const 有什么用途?(请至少说明两种)

     

     

     

    4、在C++ 程序中调用被 C编译器编译后的函数,为什么要加 extern “C”声明?

     

     

     

     

    5、请简述以下两个for循环的优缺点

     

    // 第一个

    for (i=0; i<N; i++)

    {

    if (condition)

        DoSomething();

    else

        DoOtherthing();

    }

    // 第二个

    if (condition)

    {

    for (i=0; i<N; i++)

        DoSomething();

    }

    else

    {

        for (i=0; i<N; i++)

        DoOtherthing();

    }

    优点:

     

     

    缺点:

     

     

     

    优点:

     

     

    缺点:

     

     

     

    四、有关内存的思考题(20分)

     

    void GetMemory(char *p)

    {

    p = (char *)malloc(100);

    }

    void Test(void)

    {

    char *str = NULL;

    GetMemory(str);  

    strcpy(str, "hello world");

    printf(str);

    }

     

    请问运行Test函数会有什么样的结果?

    答:

     

     

     

     

    char *GetMemory(void)

    {  

    char p[] = "hello world";

    return p;

    }

    void Test(void)

    {

    char *str = NULL;

    str = GetMemory();   

    printf(str);

    }

     

    请问运行Test函数会有什么样的结果?

    答:

    Void GetMemory2(char **p, int num)

    {

    *p = (char *)malloc(num);

    }

    void Test(void)

    {

    char *str = NULL;

    GetMemory(&str, 100);

    strcpy(str, "hello");  

    printf(str);   

    }

    请问运行Test函数会有什么样的结果?

    答:

     

     

     

     

     

    void Test(void)

    {

    char *str = (char *) malloc(100);

        strcpy(str, hello);

        free(str);    

        if(str != NULL)

        {

         strcpy(str, world);

    printf(str);

    }

    }

    请问运行Test函数会有什么样的结果?

    答:

     

     

     

     

     

     

    五、编写strcpy函数(10分)

    已知strcpy函数的原型是

           char *strcpy(char *strDest, const char *strSrc);

           其中strDest是目的字符串,strSrc是源字符串。

     

    1)不调用C++/C的字符串库函数,请编写函数 strcpy

     

     

     

     

     

     

     

    2strcpy能把strSrc的内容复制到strDest,为什么还要char * 类型的返回值?

     

     

     

     

    六、编写类String的构造函数、析构函数和赋值函数(25分)

    已知类String的原型为:

        class String

        {

         public:

            String(const char *str = NULL); // 普通构造函数

            String(const String &other);        // 拷贝构造函数

            ~ String(void);                     // 析构函数

            String & operate =(const String &other);    // 赋值函数

         private:

            char  *m_data;                // 用于保存字符串

        };

           请编写String的上述4个函数。

     

    附录C++/C试题的答案与评分标准

    一、请填写BOOL , float, 指针变量 与“零值”比较的 if 语句。(10分)

     

    请写出 BOOL flag 与“零值”比较的 if 语句。(3分)

    标准答案:

        if ( flag )

        if ( !flag )

    如下写法均属不良风格,不得分。

        if (flag == TRUE)  

        if (flag == 1 )    

        if (flag == FALSE)  

            if (flag == 0)    

    请写出 float x 与“零值”比较的 if 语句。(4分)

    标准答案示例:

    const float EPSINON = 0.00001;

    if ((x >= - EPSINON) && (x <= EPSINON)

    不可将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”此类形式。

      

     

    如下是错误的写法,不得分。

        if (x == 0.0)  

        if (x != 0.0)      

       

    请写出 char  *p 与“零值”比较的 if 语句。(3分)

    标准答案:

        if (p == NULL)

        if (p != NULL)

    如下写法均属不良风格,不得分。

        if (p == 0)

        if (p != 0)    

        if (p)  

            if (!)    

     

    二、以下为Windows NT下的32C++程序,请计算sizeof的值(10分)

     

           char str[] = “Hello” ;

           char   *p = str ;

    int     n = 10;

    请计算

    sizeof (str ) = 6   2分)

             

    sizeof ( p ) =   4   2分)

              

    sizeof ( n ) =   4   2分)

    void Func ( char str[100])

    {

    请计算

     sizeof( str ) =       2分)

    }

     

    void *p = malloc( 100 );

    请计算

    sizeof ( p ) = 4      2分)

     

     

    三、简答题(25分)

     

    1、头文件中的 ifndef/define/endif 干什么用?(5分)

    答:防止该头文件被重复引用。

     

    2#include <filename.h>    #include “filename.h” 有什么区别?(5分)

    答:对于#include <filename.h> ,编译器从标准库路径开始搜索 filename.h

        对于#include “filename.h” ,编译器从用户的工作路径开始搜索 filename.h

     

    3const 有什么用途?(请至少说明两种)(5分)

    答:(1)可以定义 const 常量

    2const可以修饰函数的参数、返回值,甚至函数的定义体。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

     

    4、在C++ 程序中调用被 C编译器编译后的函数,为什么要加 extern “C” 5分)

    答:C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为: void foo(int x, int y);

    该函数被C编译器编译后在库中的名字为_fooC++编译器则会产生像_foo_int_int之类的名字。

    C++提供了C连接交换指定符号externC”来解决名字匹配问题。

     

    5、请简述以下两个for循环的优缺点(5分)

     

    for (i=0; i<N; i++)

    {

    if (condition)

        DoSomething();

    else

        DoOtherthing();

    }

    if (condition)

    {

    for (i=0; i<N; i++)

        DoSomething();

    }

    else

    {

        for (i=0; i<N; i++)

        DoOtherthing();

    }

    优点:程序简洁

     

    缺点:多执行了N-1次逻辑判断,并且打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。

    优点:循环的效率高

     

    缺点:程序不简洁

     

     

     

    四、有关内存的思考题(每小题5分,共20分)

     

    void GetMemory(char *p)

    {

    p = (char *)malloc(100);

    }

    void Test(void)

    {

    char *str = NULL;

    GetMemory(str);  

    strcpy(str, "hello world");

    printf(str);

    }

     

    请问运行Test函数会有什么样的结果?

    答:程序崩溃。

    因为GetMemory并不能传递动态内存,

    Test函数中的 str一直都是 NULL

    strcpy(str, "hello world");将使程序崩溃。

     

    char *GetMemory(void)

    {  

    char p[] = "hello world";

    return p;

    }

    void Test(void)

    {

    char *str = NULL;

    str = GetMemory();   

    printf(str);

    }

     

    请问运行Test函数会有什么样的结果?

    答:可能是乱码。

    因为GetMemory返回的是指向“栈内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

    void GetMemory2(char **p, int num)

    {

    *p = (char *)malloc(num);

    }

    void Test(void)

    {

    char *str = NULL;

    GetMemory(&str, 100);

    strcpy(str, "hello");  

    printf(str);   

    }

    请问运行Test函数会有什么样的结果?

    答:

    1)能够输出hello

    2)内存泄漏

     

     

    void Test(void)

    {

    char *str = (char *) malloc(100);

        strcpy(str, hello);

        free(str);    

        if(str != NULL)

        {

         strcpy(str, world);

    printf(str);

    }

    }

    请问运行Test函数会有什么样的结果?

    答:篡改动态内存区的内容,后果难以预料,非常危险。

    因为free(str);之后,str成为野指针,

    if(str != NULL)语句不起作用。

     

     

     

    五、编写strcpy函数(10分)

    已知strcpy函数的原型是

           char *strcpy(char *strDest, const char *strSrc);

           其中strDest是目的字符串,strSrc是源字符串。

    1)不调用C++/C的字符串库函数,请编写函数 strcpy

    char *strcpy(char *strDest, const char *strSrc);

    {

        assert((strDest!=NULL) && (strSrc !=NULL)); // 2

        char *address = strDest;                   // 2

        while( (*strDest++ = * strSrc++) != \0 )    // 2

           NULL ;

        return address ;                          // 2

    }

     

    2strcpy能把strSrc的内容复制到strDest,为什么还要char * 类型的返回值?

    答:为了实现链式表达式。                                              // 2

    例如       int length = strlen( strcpy( strDest, “hello world”) );

     

    六、编写类String的构造函数、析构函数和赋值函数(25分)

    已知类String的原型为:

        class String

        {

         public:

            String(const char *str = NULL); // 普通构造函数

            String(const String &other);        // 拷贝构造函数

            ~ String(void);                     // 析构函数

            String & operate =(const String &other);    // 赋值函数

         private:

            char  *m_data;                // 用于保存字符串

        };

           请编写String的上述4个函数。

    标准答案:

     

    // String的析构函数

           String::~String(void)               // 3

    {

        delete [] m_data;                     

    // 由于m_data是内部数据类型,也可以写成 delete m_data;

           }

     

           // String的普通构造函数            

           String::String(const char *str)      // 6

    {

        if(str==NULL)                         

        {

           m_data = new char[1];    // 若能加 NULL 判断则更好

           *m_data = \0;                     

        }                                         

        else

        {

           int length = strlen(str);          

           m_data = new char[length+1]; // 若能加 NULL 判断则更好     

           strcpy(m_data, str);               

        }

    }  

    // 拷贝构造函数

        String::String(const String &other)   // 3

        {  

        int length = strlen(other.m_data);

        m_data = new char[length+1];      // 若能加 NULL 判断则更好   

        strcpy(m_data, other.m_data);        

    }

    // 赋值函数

        String & String::operate =(const String &other)    // 13

        {  

           // (1) 检查自赋值                     // 4

           if(this == &other)

               return *this;

       

    // (2) 释放原有的内存资源            // 3

           delete [] m_data;

          

           // 3)分配新的内存资源,并复制内容 // 3

        int length = strlen(other.m_data);

        m_data = new char[length+1];         // 若能加 NULL 判断则更好

            strcpy(m_data, other.m_data);

          

           // 4)返回本对象的引用            // 3

           return *this;

    }  

    展开全文
  • MySQL优化书写高质量sql语句

    万次阅读 多人点赞 2021-03-22 22:21:49
    -- 方案三:在业务允许的情况下限制页数: 理由: 当偏移量大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量 + 要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回...

    1. 查询SQL尽量不要使用全查 select *,而是 select + 具体字段。

    反例:

    select * from student;
    

    正例:

    select id,name, age from student;
    

    理由

    • 只取需要的字段,可以节省资源、减少CPU和IO以及网络开销。
    • select * 进行查询时,无法使用到覆盖索引,就会造成回表查询
    • 使用具体字段可以减少表结构变动带来的影响。

    2. 使用预编译语句进行数据库操作

    理由

    • 预编译语句可以重复使用计划,减少SQL编译所需要的时间
    • 可以解决动态SQL所带来的SQL注入的问题
    • 只传参数,比传递SQL语句更高效
    • 相同语句可以一次解析,多次使用,提高处理效率

    3. 禁止使用不含字段列表的 insert 语句

    反例:

    insert into values ('a', 'b', 'c');
    

    正例:

    insert into t(a, b, c) values ('a','b','c');
    

    理由

    • 不含字段名的 insert 语句,很难区分到底对应的是什么字段,而且只能全值插入,可读性差。
    • 一旦表结构发生改变,很难修改。

    4. 尽量避免在 where 子句中使用 or 来连接条件

    案例:新建一个user表,它有一个普通索引userId,表结构如下:

    create table 'student' (
        'id' int(11) not null,
        'name' varchar(255) default null,
        'age' int(11) default null,
        'date' datetime default null,
        'sex' int(1) default null,
                primary key ('id')
    ) engine=innodb default charset=utf8;
    

    查询userid为1 或者 年龄为 18 岁的用户
    反例:

    select id, user_id, age, name from user where userid=1 or age =18
    

    正例:

    -- 使用union
    select id, user_id, age, name
    from user
    where userid = 1
    union
    select *
    from user
    where age = 18
    

    理由:

    使用or可能会使索引失效,从而全表扫描
    对于 or + 没有索引 的字段,如上面的 age 这种情况,假设它走了userId 的索引,但是走到 age 查询条件时,它还得全表扫描,也就是需要三步过程:全表扫描+索引扫描+合并,如果它一开始就走全表扫描,直接一遍扫描就结束。
    mysql是有优化器的,处于效率与成本考虑,遇到or条件,索引可能失效。

    unionunion all 联合查询
    用于合并两个或多个 select 语句的结果集
    【注意】union 内部的 select 语句必须拥有相同数量的列列必须拥有相似的数据类型。同时,每条 select 语句中的列的顺序必须相同,才能联合查询。
    【不同点】union 操作符只会选取不同的结果值(行数据)。如果需要允许重复的值,需要使用 union all


    5. 使用 where 条件查询,要限定要查询的数据,避免返回多余的行,同时避免数据类型的隐式转换

    假设 id 为 int 类型,查询 id = 1 的数据
    反例:

    select id, name from student where id = '1';
    

    正例:

    select id, name from student where id = 1;
    

    理由

    • 需要什么数据,就去查什么数据,避免返回不必要的数据,节省开销。
    • 隐式转换会导致索引失效

    6. 静止在 where 子句中对字段进行表达式操作或函数转换,这将导致系统放弃使用索引而进行全表扫描

    假设 user 表的 age 字段,加了索引,对其进行数据查询

    反例:

    select name, age from user where age - 1 = 20;
    

    正例:

    select name, age from user where age = 21;
    

    理由

    • age 加了索引,但是因为对它进行运算查询,导致索引不生效,大大的降低效率。

    7. 尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。(Mysql中适用)

    反例:

    select age,name from user where age <> 18;
    

    正例:

    # 可以考虑分开两条sql写,联合查询
    select age,name from user where age < 18
    union
    select age,name from user where age > 18
    

    理由:

    • 使用 !=<> 很可能会让索引失效

    8. 对查询优化,应考虑在where及order by涉及的列上建立索引,尽量避免全表扫描。

    反例:

    select name, age, address from user where address ='深圳' order by age ;
    

    正例:添加索引再查询

    alter table user add index idx_address_age (address,age)
    

    9. where子句中考虑使用默认值代替 null

    反例:(这种会全查所有数据)

    select user_id, name, age from user where age is not null;
    

    正例:

    -- 表字段 age 设置0为默认值代替null
    select user_id, name, age from user where age > 0;
    

    理由

    • 并不一定使用 is nullis not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。
      如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件 !=,<> isnull,is not null 经常让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃索引的。
    • 如果把 null 值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。

    10. 如果查询结果只有一条或者只需要一条记录(可能最大/小值),建议使用 limit 1

    假设现在有student学生表,要找出一个名字叫 Tom 的人.

    create table 'student' (
        'id' int(11) not null,
        'name' varchar(50) default null,
        'age' int(11) default null,
        'date' datetime default null,
        'sex' int(1) default null,
    primary key ('id')
    ) engine=innodb default charset=utf8mb4;
    

    反例:

    select id,name from student where name='Tom '
    

    正例

    select id,name from employee where name='Tom ' limit 1;
    

    理由

    • 加上 limit 1 分页后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高
    • 如果name是唯一索引的话,是不必要加上 limit 1,因为limit的存在主要就是为了防止全表扫描,从而提高性能,如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大。

    11. 优化 limit 分页语句

    我们日常做分页需求时,一般会用 limit 实现,但是当偏移量特别大的时候,查询效率就变得低下

    反例:

    select id,name,age from student limit 10000,10
    

    正例:

    -- 方案一 :返回上次查询的最大记录(偏移量)
    select id,name from student where id > 10000 limit 10;
    
    -- 方案二:order by + 索引
    select id,name from student order by id  limit 10000,10;
    
    -- 方案三:在业务允许的情况下限制页数:
    

    理由:

    • 当偏移量大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量 + 要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的
    • 如果使用优化方案一,返回上次最大查询记录(偏移量),这样可以跳过偏移量,效率提升不少。
    • 方案二使用 order by+索引,也是可以提高查询效率的。
    • 方案三的话,建议跟业务讨论,有没有必要查这么后的分页。因为绝大多数用户都不会往后翻太多页。

    12. 尽量避免向客户端返回过多数据量,使用limit分页

    假设业务需求是,用户请求查看自己最近一年观看过的电影数据。
    反例:

    -- 一次性查询所有数据回来
    select *
    from LivingInfo
    where watchId = useId
      and watchTime >= Date_sub(now(), Interval 1 Y)
    

    正例:

    -- 分页查询
    select *
    from LivingInfo
    where watchId = useId
      and watchTime >= Date_sub(now(), Interval 1 Y)
      limit offset,pageSize
    
    -- 如果是前端分页,可以先查询前两百条记录,因为一般用户应该也不会往下翻太多页
    select *
    from LivingInfo
    where watchId = useId
      and watchTime >= Date_sub(now(), Interval 1 Y) limit 200;
    

    DATE_SUB 函数:从日期减去指定的时间间隔
    语法: DATE_SUB(date, INTERVAL expr type)
    date 参数是合法的日期表达式。expr 参数是时间间隔类型
    时间间隔类型取值:

    microsecond、second、minute、hour、day、week、month、quarter、year、second_microsecond、minute_microsecond、minute_second、hour_microsecond、hour_second、hour_minute、day_microsecond、day_second、day_minute、day_hour、year_month


    13. 优化 like 语句

    当用到模糊关键字查询使用 like 时,like很可能让索引失效

    反例:

    select *
    from student
    where name like '%strive_day';
    -- 或者使用 % 包裹
    select *
    from student
    where name like '%strive_day%';
    

    正例:

    select * from student
    where name like 'strive_day%';
    

    理由

    • % 放前面,不会走索引查询。
    • % 放关键字后面,会走索引进行查询。
    • % 包裹关键字,也不会走索引查询。
    • 无前置 %,只有后置 % 才会走索引查询

    14. 尽量避免在索引列上使用mysql的内置函数

    案例:查询最近七天内登陆过的用户(假设 loginTime 字段加了索引)

    反例:

    select *
    from system_user user
    where date_add(user.logintime, interval 7 day) >= now();
    

    正例:

    select *
    from system_user user
    where user.logintime >=date_add(now(), interval - 7 day);
    

    理由

    • 索引列上使用mysql的内置函数,索引会失效
    • 如果索引列不加内置函数,会走索引查询

    15. 使用联合索引时,注意索引列的顺序,一般遵循 最左匹配原则

    假设有一个联合索引 (user_id, age)user_id 在前,age 在后。

    反例:

    select user_id, name, age from user where age = 10;
    

    正例:

    # 符合最左匹配原则
    select user_id, name, age from user where user_id = 1 and age = 21;
    # 符合最左匹配原则
    select user_id, name, age from user where user_id = 1;
    

    理由

    • 当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则
    • 联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的。

    16. 在适当时候,使用覆盖索引。

    覆盖索引能够使得你的SQL语句不需要 回表,仅仅访问索引就能够得到所有需要的数据,大大提高了查询效率。

    反例:

    # like模糊查询,不走索引
    select user_id, name, age from user where user_id like '%123%'
    
    # id为主键,那么为普通索引,即覆盖索引。
    select user_id, name, age from user where user_id like '%123%';
    

    17. 删除冗余和重复索引

    反例:

      key 'idx_userid' ('userid')
      key 'idx_userid_age' ('userid','age')
    

    正例:

      key 'idx_userid_age' ('userid','age')
    --  删除 userid 的索引(key 'idx_userid_age' ('userid','age'))
    --  因为组合索引(a,b)相当于创建了(a)和(a,b)索引。
    

    理由:

    • 重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能

    18. Inner join 、left join、right join,优先使用Inner join,如果是left join,左边表结果尽量小

    Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集

    left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录

    right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录

    都满足SQL需求的前提下,优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。

    反例:

    select name, age 
    from tab1 t1 
    left join tab2 t2  
    on t1.age = t2.age 
    where t1.id = 2;
    

    正例:

    select name, age 
    from (select * from tab1 where id = 2) t1 
    left join tab2 t2 on t1.age = t2.age;
    

    理由:

    • 如果 inner join 是等值连接,或许返回的行数比较少,所以性能相对会好一点
    • 使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少

    19. 如果插入数据过多,考虑 批量插入

    反例:

    for(user u :list)
    { insert into user(name,age) values(name, age)}
    

    正例:

    //一次500批量插入,分批进行
    insert into user(name,age) values
    <foreach collection="list" item="item" index="index" separator=",">
    	(#{item.name},#{item.age})
    </foreach>
    

    理由

    • 批量插入性能好,减少时间损耗。

    20. 尽量少用 distinct 关键字

    distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率

    反例:

    -- 去重多个字段
    select distinct * from  user;
    

    正例:

    select distinct name from user;
    

    理由

    distinct 的语句 cpu 时间和占用时间都高于不带distinct的语句。
    因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较、过滤的过程会占用系统资源,cpu时间。


    21. 不要有超过5个以上的表连接

    理由

    • 连表越多,编译的时间和开销也就越大
    • 连表可读性差,把连接表拆开成较小的几个执行,可读性更高

    22. 数据量大的时候,如何优化更新语句。

    数据量大的时候,需要避免同时修改或删除过多数据,同时会造成cpu利用率过高,从而影响别人对数据库的访问。

    反例:

    # 一次删除10万或者100万+条数据
    delete from user where id < 1000000;
    # 或者采用单一循环操作,效率低,时间漫长
    forUser user:list){delete from user;}
    

    正例:

    # 分批进行删除,如每次500   
    delete user where id < 500
    delete user where id >= 500 and id < 1000...
    delete user where id >= 999500 and id < 1000000;
    

    理由

    • 一次性 删除/更新 太多数据,可能会有 lock wait timeout exceed 的错误,所以建议分批操作。

    23. 合理使用 exist 和 in

    假设表A表示某企业的员工表,表B表示部门表,查询所有部门的所有员工SQL

    反例::

    select * from A where deptId in (select deptId from B);
    

    这样写等价于:

    先查询部门表B
    select deptId from B
    再由部门deptId,查询A的员工
    select * from A where A.deptId = B.deptId

    可以抽象成这样的一个循环语句:

    List<> resultSet ;    
    for(int i = 0; i < B.length; i ++) {
    	for(int j = 0; j < A.length; j ++) {
        	if(A[i].id == B[j].id) {
            	resultSet.add(A[i]);
                break;          
            }       
         }    
     }
    

    我们也可以用exists实现一样的查询功能

    select * from A where exists 
    	(select 1 from B where A.deptId = B.deptId);
    

    上述代码等价于:

    select * from A,先从A表做循环
    select * from B where A.deptId = B.deptId,再从B表做循环.

    因为exists查询的理解就是,先执行主查询,获得数据后,再放到子查询中做条件验证,根据验证结果(true或者false),来决定主查询的数据结果是否得以保留

    同理,可以抽象成这样一个循环:

    List<> resultSet;    
    for(int i = 0; i < A.length; i ++) {
    	for(int j = 0; j < B.length; j ++) {
        	if(A[i].deptId == B[j].deptId) {
            	resultSet.add(A[i]);
                break;          
                }       
            }    
        }
    

    理由

    • 数据库最费劲的就是跟程序链接释放。假设链接了两次,每次做上百万次的数据集查询,查完就走,这样就只做了两次;相反如果每次单独查询,建立了上百万次链接,申请链接释放反复重复
    • mysql优化原则,就是小表驱动大表,小的数据集驱动大的数据集,从而让性能更优
    • 我们要选择最外层循环小的,也就是,如果B的数据量小于A,适合使用 in,如果B的数据量大于A,即适合选择exist

    24. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型

    反例:

    'king_id' varchar20not null comment '123'
    

    正例:

     'king_id' int(11) not null comment '123'
    

    理由

    • 相对于数字型字段,字符型会降低查询和连接的性能,并会增加存储开销。

    25. 尽量用 union all 替换 union

    如果检索结果中不会有重复的记录,推荐 union all 替换 union

    反例:

    select * from user where userid = 1
    union
    select * from user where age = 20
    

    正例:

    select * from user where userid = 1
    union all
    select * from user where age = 20
    

    理由

    如果使用union,不管检索结果有没有重复,都会尝试进行合并,然后在输出最终结果前进行排序。
    如果已知检索结果没有重复记录,使用 union all 代替 union,这样会提高效率。


    26. 如果字段类型是字符串,where时一定用引号括起来,否则将导致索引失效

    反例:

    select * from user where userid = 1;
    

    正例:

    select * from user where userid ='1';
    

    理由

    • 第一条语句未加单引号就不走索引,这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。

    使用explain 分析你 SQL 的计划

    日常开发写SQL的时候,尽量养成一个习惯。用explain分析一下你写的SQL,尤其是走不走索引这一块。

    展开全文
  • 导致员工工作效率低下的8大原因中国总裁培训网www.chinaceot.com 杨鑫许多中小型成长企业往往有这样的怪现象出现:企业内几乎每个人都忙不过来,但就是整体效率低下!而且也很难找出原因,不知该如何改善。本文告诉...
  • 研发部门如何提高工作效率的管理探讨 第一、几个基本的工作准则 1、组织协同,避免缺乏组织,让事情放任自流,一盘散沙,这是产品日常管理责任人、项目负责人的责任。 2、难点问题即时支援攻克,避免员工工作粘滞...