微信开发网站建设程序,做个网站哪里可以做,做网站文案用哪个软件,oa办公系统怎么使用《代码大全》CODECOMPLETE2 apollowangjunbo/Code-Complete-reading-note: 《代码大全》读书笔记 (github.com) 第一章 欢迎进入软件构建的世界
软件开发过程中各种不同的活动#xff1a;
定义问题#xff08;problem definition#xff09;需求分析#xff08;requireme…《代码大全》CODECOMPLETE2 apollowangjunbo/Code-Complete-reading-note: 《代码大全》读书笔记 (github.com) 第一章 欢迎进入软件构建的世界
软件开发过程中各种不同的活动
定义问题problem definition需求分析requirements development规划构建construction plannning软件架构software architecture或 高层设计high-level design详细设计detailed design编码调试coding and debugging单元测试unit testing集成测试integration testing集成integration系统测试system testing保障维护corrective maintenance 软件构建是软件开发的核心活动 构建活动是每个项目中唯一一项必不可少的工作。 软件构建的主要活动包括 详细设计、编码、调试、集成、开发者测试developer testing包括单元测试和集成测试。 构建也常被称为“编码”或“编程”。 构建活动的质量对软件的质量有着实质性的影响。
第二章 用隐喻来更充分地理解软件开发 隐喻的重要性通过把你不太理解的事物和一些你较为理解、且十分类似的事物做比较你可以对这些不太理解的东西产生更加深刻的理解。这种使用隐喻的方法叫做“建模modeling” 写作的隐喻对于个人规模的软件开发工作乃至小型项目是足够了然而对于较大的软件开发过程来说太过简单。写作的隐喻暗示着软件开发过程是一种代价昂贵的试错。 培植系统的隐喻优点在于增量思维缺点在于培植的过程无法人为干预而软件开发是可以的。 通过把软件的构建过程比作是房屋的建设过程我们可以发现仔细地准备是必要的而大型项目和小型项目之间也是有差异的。 软件架构-建筑学不同意个人感觉建筑结构更贴切 支撑性测试代码-脚手架 构件-建设 建造一个房子的时候你不会去试着建造那些能买得到的现成的东西。当开发软件时你也会这么做的。你会大量使用高级语言所提供的功能而不会自己去编写操作系统层次的代码。你可能还要用些现成的程序库比如说一些容器类container classes、科学计算函数、用户界面组件、数据库访问组件等等。总之自己编写那些能买得到的现成的代码通常是没有意义的。 建筑业中盖间仓库或者工具房或是一座医院或者核反应站你在规划、设计及质量保证方面所需达到的程度是不一样的。盖一座学校、一幢摩天大楼或一座三居室的小别墅所用的方法也不会相同。同理在软件开发中通常你只需要用灵活的、轻量级的lightweight方法但有时你就必须得用严格的、重量级的开发方法以达到所需的安全性目标或其他什么目标。 软件的变动在建筑领域也有类似事物。把一堵承重墙移动半尺所需花费的成本肯定要比仅仅移动一面隔墙更高。同样对软件进行结构性的修改所需花费的成本肯定也比仅仅增删一些周边功能更高。 通过把软件开发中的实践比作是智慧工具箱中的工具我们又发现因地制宜地选择正确工具是成为有效编程的程序员的关键。
第三章 三思而后行前期准备
3.1 前期准备的重要性
就像修建建筑物一样项目的成败很大程度上在构建活动开始之前就已经注定了。如果地基没打好或者计划不充分那么你在构建期间能做得无非是尽量让损害最小罢了。软件构建活动差不多占整个项目成本的65%。最糟糕的软件项目最终会进行两三次甚至更多构建。而良好的前期准备可以很大程度降低这种风险。其实它的中心目标就是降低风险。首先确保你在做正确的事情后续正确地做事(构建)才有意义和产生正向价值。与“先做一个错误的东西出来然后扔掉并从头来过”的成本相比花费比理想情况下更多的力气找出他们真正想要的东西这种方式成本更加低廉更值得投入。
3.2 辨明你所从事的软件的类型
不同种类的软件项目需要在“准备工作”和“构建活动”之间做出不同的平衡。迭代方法往往能够减少“前期准备不足”造成的负面影响但是它不能完全消除此影响。
3.3 问题定义的先决条件
问题定义-需求-架构-构建-系统测试-将来的改进“未能定义问题”的处罚是你浪费了大量时间去解决错误的问题这是床冲出发因为你也没有解决正确的问题。
3.4 需求的先决条件
明确的需求有助于确保是用户驾驭系统的功能而不是程序员明确的需求避免程序员去猜用户想要什么。明确的需求还有助于避免争论。重视需求有助于减少开始编程开发之后的系统变更情况。需求像水如果冻结了就容易在上面开展建设一旦需求稳定项目就能以有序的可预测的平稳的方式完成从架构到编码达到测试等一系列工作。需求变更的主要来源开发过程帮助客户更好的理解自己的需求。
3.5 架构的先决条件
软件架构是软件设计的高层部分用于支撑更细节的设计框架。架构也称为“系统架构”、“高层设计”或“顶层设计”。架构的质量决定了系统的“概念完整性”决定了系统的最终质量。一个经过慎重考虑的架构为“从顶层到底层围护系统的概念完整性”提供了必备的结构和体系他将工作氛围几个部分使多个开发者或者这多个开发团队可以独立工作。维护设计的缘由至少与维护设计本身一样重要。架构应该定义程序的主要构造块根据程序规模不同各个构造块可能是但各类也可能是有许多类组成的一个子系统。应该明确定义各个造块的责任每个构造块应该负责某一个区域的事情并且对其他构造块负责的区域知道的越少越好。应该明确定义每个构造块的通信规则对于每个构造块架构应该描述他能直接调用哪些构造块能间接使用哪些构造块不能使用哪些构造块。架构应该包含或者考虑主要的类、数据设计、业务规则、用户界面设计、资源管理、安全性、性能、可伸缩性、互用性、国际化、输入输出、错误处理、容错性、可行性、过度工程、那些使用现有轮子哪些自己实现、关于该复用的决策、变更策略。
3.6 花费在前期准备上的时间长度
花费在问题定义、需求分析、架构上的时间依据项目的需要而变化。一般说来一个运作良好的项目会在需求、架构以及其他前期时间方面突入10%20%的工作量和20%30%的时间。这些数字不包括详细设计的时间——那是构建活动的一部分。
第四章 关键的构建决策
4.1选择编程语言 一套好的的符号系统能把大脑从所有非必要的工作中解脱出来、集中精力去对付更高级的问题从功效上看能够有效地提升人类的智慧。 当程序员用“使用了三年以上的语言”编写代码是生产效率比“同等经验但使用新语言”的程序员高30% 对编程语言有相当丰富经验的程序员的生产率比几乎没有经验的程序员高3倍 C语言具有机器无关性被称为“可移植的汇编语言” C在兼容C语言的基础上还提供了类、多态、异常处理、模板和更健壮的类型检查功能、以及一套内容广泛的标准库 启发:std::swap std::sort std::find 等封装好的标准方法将开发者从常用基础算法中解放出来去实现更加复杂有意义的程序逻辑同样智能指针帮助开发者管理生命周期节约更多的精力去对付更高级的问题
4.2 编程约定 编码约定的细节要达到这样的精确度在编写完软件之后几乎不可能改变翻新软件所遵循的编码约定。 成功编程的一个关键就在于避免随意地变化这样你的大脑可以专注于那 些真正需要的变化。 启发编码时应该理解并维护已有架构的约定保持统一的编程的风格
4.3 你在技术浪潮中的位置
浪潮早期的编程工具往往很原始。在浪潮的后期我们有大量的编程语言可供选择拥有能对这些语言的代码进行完善的错误检查的工具、强大的调试工具以及自动的可靠的性能优化工具。如果处在浪潮的后期你就可以计划用大部分时间稳定持续地编写新功能。如果你处在浪潮的前期可以预期你将要花很大一部分时间用来找出文档中未加说明的编程语言特性、调试程序库代码缺陷带来的错误、修订代码以适应厂商提供的新版本函数库等。“在一种语言上编程”的程序员将他们的思想限制于“语言直接支持的那些构件”。如果语言工具是初级的那么程序员的思想也是初级的。“深入一种语言去编程”的程序员首先决定他要表达的思想是什么然后决定如何使用特定语言提供的工具来表达这些思想。
4.4 选择主要的构建实践方法
“构建的实践方法”的种类比任何单个项目能用到的要多。有意识地选择最适合你的项目的实践方法。
第五章 软件构建中的设计
5.1设计中的挑战
设计是一个“险恶(wicked)的问题”即只有通过解决或部分解决才能被明确的问题设计是个了无章法的过程需要不断试错设计很难界定完成的节点设计能得出清爽的成果设计就是确定取舍和调整顺序的过程
在设计期的进行快速试错能够避免在编码期花费时间在不合理的方案上从而从整体上节约时间。
在以往的工作中总是把做出完美的设计作为设计期的目标但是每次的设计结果都不是很理想。
通过这一节的阅读设计确实很难做到完美它非常依赖经验过程中需要试错、讨论、验证、评估、取舍、迭代改进很难回答什么时候算是设计完成具有不确定性但是好的设计能够得出清爽的成果。5.2关键的设计概念
软件的首要技术使命管理复杂度
软件的首要技术使命就是管理复杂度。以简单性作为努力目标的设计方案对此最有帮助。复杂度分为偶然复杂度和本质复杂度本质复杂度不可减少源自于问题本身偶然复杂度源自于事务附属的、非必要的或偶然出现的性质。管理复杂度把任何人在同一时间需要处理的本质(essential)复杂度的量减到最少不要让偶然性accidental)的复杂度无谓地快速增长。设计技术的目标都是把复杂问题分解成简单的部分精心设计的对象关系使关注点相互分离。
本质复杂度源自于问题本身的复杂度不可以减少好的设计会将复杂的问题分解并保证需要程序员同一时间关注的本质复杂度最少降低理解成本确保有限注意力能够用于处理想要处理的问题。
好的设计会应该限制偶然复杂度的增长当没有人知道修改一处代码会对其他代码产生什么样的影响的时候维护就会变得异常艰难。理想的设计特征
理想的设计特征最小复杂度、易于维护、松散耦合、可扩展性、可复用性、高扇入、底扇出、可移植性、精简性、层次性、标准技术
避免做出“聪明”的设计方案这类方案往往难以理解难以维护应该做成简单且易于理解的。
好的设计应该尽可能多的使用标准的技术让尽可能多的人更容易理解降低学习成本和理解维护成本也能够降低偶然复杂度。设计的层次
设计的层次软件系统、分解为子系统和包、分解包中的类、分解为类中的数据和子程序、子程序内部子系统层次的通信设计应该是无环图。
在子系统层次要限制系统之间的通信和依赖从而降低系统之间的耦合降低维护时的理解成本测试成本。降低偶然复杂度。5.3设计构造块启发式方法
找出现实世界中的对象 辨识对象及其属性(方法method)和数据(data))。确定可以对各个对象进行的操作。确定各个对象能对其他对象进行的操作。确定对象的哪些部分对其他对象可见一—哪些部分可以是公用(public)的哪些部分应该是私用private)的。定义每个对象的公开接口(public interface)。 形成一致的抽象抽象是一种能让你在关注某一概念的同时可以放心地忽略其中一些细节的能在不同的层次处理不同的细节。封装实现细节封装是说不只是让你能用简化的视图来看复杂的概念同时还不能让你看到复杂概念的任何细节。你能看得到的就是你能全部得到的当继承能简化设计时就继承继承的好处在于他能很好地辅佐抽象的概念是面向对象编程中最强大的工具之一隐匿秘密信息隐藏是结构化程序设计与面向对象设计的基础之一 隐藏复杂度隐藏变化源将其影响限制在局部范围内。 找出容易变化的区域找出看起来容易变化的项目、把容易变化的项目分离出来、把看起来容易变化的项目隔离开来不要使用布尔量作为状态变量改用枚举类型因为给状态变量增加一个状态是很常见的。保持松散的耦合查阅常用的设计模式设计模式精练了众多现成的解决方案 设计模式通过现成的抽象来减少复杂度设计模式通过把常见解决方案的细节予以制度化来减少出错设计模式通过通过提供多种设计方案而带来启发性的价值设计模式通过把设计对话提升到一个更高的层次上来简化交流 常见设计模式
模式描述Abstract Factory抽象工厂)通过指定对象组的种类而非单个对象的类型来支持创建Adapter(适配器把一个类的接口转变成为另一个接口Bridge(桥接)把接口和实现分离开来使它们可以独立地变化Composite(组合)创建一个包含其他同类对象的对象使得客户代码可以与最上层对象交互而无须考虑所有的细节对象Decrorator(装饰器)给一个对象动态地添加职责而无须为了每一种可能的职责配置情况去创建特定的子类派生类)Facade(外观为没有提供一致接口的代码提供一个一致的接口Factory Method做特定基类的派生类的实例化时除了在FactoryMethod内部之外均无须了解各派生对象的具体类型Iterator(迭代器)提供一个服务对象来顺序地访问一组元素中的各个元素Observer(观察者使一组相关对象相互同步方法是让另一个对象负责在这组对象中的任何一个发生改变时由它把这种变化通知给这个组里的所有对象Singleton(单件)为有且仅有一个实例的类提供一种全局访问功能Strategy(策略定义一组算法或者行为使得它们可以动态地相互替换Template Method模板方法)定义一个操作的算法结构但是把部分实现的细节留给子类派生类)
设计模式能够提供现成的解决方案帮助更快完成易于理解的设计方案难点在于找出最合适的模式不应该为了使用设计模式而使用这就要求充分理解每一种设计模式知道其所适用于解决的问题。5.4设计实践
介绍了迭代、分而治之、自上而下、自下而上、建立实验性原型等方法论
5.5对流行的设计方法的评论
略
第六章 可以工作的类
成为高效程序员的一个关键就在于当你开发程序任一部分的代码时都能安全地忽视程序中尽可能多的其余部分。而类就是实现这一目标的首要工具。
6.1类的基础抽象数据类型ADTs
抽象数据类型(ADTabstract data type是指一些数据以及对这些数据所进行的操作的集合。使用抽象数据类型的益处可以隐藏实现细节、改动不会影响到整个程序、让接口提供更多信息、更容易提高性能、让程序的正确性更显而易见、程序更具自我说明性、无需在程序内到处传递数据、像在现实世界中那样操作实体。类抽象数据类型继承多态
6.2良好的类接口
类的接口应该展现一致的抽象层次尽可能让接口可编程一个接口中任何无法通过编译器强制实施的部分就是一个可能被误用的部分尽可能地限制类和成员的可访问性不要公开暴露成员和数据
类的接口能够帮助使用者快速理解这个类的设计初衷
编码过程中注意思考对外提供哪些接口、接口如何命名、可访问性如何、注释等可以降低代码的阅读难度6.3有关设计和实现的问题
包含
“包含”表示一个类含有一个基本的数据元素或对象是面向对象编程中的主力技术对于数据成员超过7个的类考虑要不要把这些数据成员分解为几个更小的类
继承
“继承”是说一个类是另一个累的特化继承时应该考虑成员函数是否对派生类可见、是否允许被覆盖数据成员是否对派生类可见。错误的继承给程序增加了复杂度只有符合Liskov替换原则的继承才能降低复杂度。继承的层次最好不要超过2-3层派生类的数量最好不要超过7个
相对于包含而言继承有更多地注意事项使用时有更多需要考虑清楚的事项从控制复杂度的角度来看应尽量避免使用继承成员函数和数据成员
略
构造函数
尽可能早构造函数中初始化所有数据成员防御目的单例模式下构造函数private拷贝构造逐一考虑采用深拷贝
6.4创建类的原因
创建类的理由
对现实世界中的对象建模对抽象对象建模降低复杂度隔离复杂度隐藏实现细节限制变化所影响的范围隐藏全局数据让参数传递更顺畅创建中心控制点让代码更易于重用为程序族做计划把相关操作放到一起实现特定的重构
6.5与具体编程语言相关的问题
略
6.6超越类包
略
第七章 高质量的子程序
子程序也算得上是计算机科学中一项最为重大的发明了。 子程序的使用使得程序变得更加易读,更易于理解,比任何编程语言的任何功能特性都更容易。
7.1创建子程序的正当理由
降低复杂度:可以通过创建子程序来隐藏一些信息,这样你就不必再去考虑这些信息了。缩小代码规模、改善可维护性、提高正确性引入中间、易懂的抽象:把一段代码放入一个命名恰当的子程序内,是说明这段代码用意最好的方法之一。避免代码重复支持子类化隐藏顺序隐藏指针操作提高可移植性简化复杂的逻辑判断改善性能 除此之外,创建类的很多理由也是创建子程序的理由:隔离复杂度隐藏实现细节限制变化所带来的影响隐藏全局数据形成中央控制点促成可重用的代码达到特定的重构目的
通过函数可以减少重复的代码好的函数命名能够大幅提高代码的可读性。
在工作中经常见到有的函数中只有几行甚至一行代码代码评审时会讨论这么短的函数是否有必要封成函数。通过书中的介绍明白了这样可以提高代码可读性并且在这个短函数被调用次数比较多的情况下可以实现一处修改全局统一变化的好处从而降低维护过程的复杂度。7.2 在子程序层上设计
内聚性是指子程序中各种操作之间联系的紧密程度功能的内聚性(functional cohesion)是最强也是最好的一种内聚性,也就是说让一个子程序仅执行一项操作。不够理想的内聚性 顺序上的内聚性通信上的内聚性临时的内聚性 不可取的内聚性 过程上的内聚性逻辑上的内聚性巧合的内聚性
在读代码中见到过一些函数名称中有“and”对阅读代码造成了非常大的困难这类函数具有不可取的“过程上的内聚性”因为外界的调用往往是两个过程一起顺序调用所以直接写在了一起。将and前后做的两件事情拆开写到两个函数中可以以很好的实现“功能的内聚性”让一个函数只完成一个功能降低理解的门槛。7.3 好的子程序名字
描述子程序所做的所有事情避免使用无意义的、模糊或表述不清的动词有些动词 反例Handlecalculation()、 PerformServices()、 OutputUser()、 ProcessInput()和 DealWithoutput()不要仅通过数字来形成不同的子程序名字 反例OutputUser、 OutputUser1和 OutputUser22函数命名时要对返回值有所描述对象名称清晰时可以省略宾语给过程起名时使用语气强烈的动词加宾语的形式准确使用对仗词eg begin/end insert/delete show/hide create/destroy lock/unlock source/target first/last min/max start/stop get/put next/previous up/down get/set old/new为常用操作确立命名规则
好的命名能够清晰地表述函数的作用从而封装复杂度阅读代码的人通过名称就能够明白用意 而无需关心内部的实现。
起名字时确实容易偷懒使用Handle、Process、Deal导致维护的人对函数的作用理解不清应该尽量避免。7.4 子程序可以写多长
在超过200行后,你迟早会在可读性方面遇到问题
7.5 如何使用子程序参数
按照输入修改-输出的顺序排列参数暗含了子程序的内部操作顺序考虑自己创建IN和OUT关键词利用宏说明作用编译器不强制如果几个子程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致使用所有的参数把状态或出错变量放在最后附属于程序的主要功能不要把子程序的参数用做工作变量不要修改输入变量可通过const限制在接口中对参数的假定加以说明把子程序的参数个数限制在大约7个以内考虑对参数采用某种表示输入、修改、输出的命名规则为子程序传递用以维持其接口抽象的变量或对象
参数的设置能够帮助读者理解接口的功能和用意在代码中见到过有非常多参数的接口在没有必要理由的情况下尽量避免可以将参数聚类为一个对象传入。7.6 使用函数时要特别考虑的问题
除非万不得已谨慎使用宏 替代技术 const可以用于定义常量 inline可以用于定义可被编译为内嵌的代码(n1ne《inInecode)的函数 template可以用于以类型安全的方式定义各种标准操作,如min、max等 enum可以用于定义枚举类型 typedef可以用于定义简单的类型替换
第八章 防御式编程
8.1 保护程序免遭非法输入数据的破坏
对已形成产品的软件而言,仅仅“垃圾进,垃圾出”还不够。不管进来什么,好的程序都不会生成垃圾,而是做到“垃圾进,什么都不出”、“进来垃圾,出去是出错提示”或“不许垃圾进来”。 通常有三种方法来处理进来垃圾的情况
检查所有来源于外部的数据的值检查子程序所有输入参数的值决定如何处理错误的输入数据
8.2 断言
用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况断言是用来检查永远不该发生的情况断言是用于检查代码中的bug可以把断言看做是可执行的注解用断言来注解并验证前条件和后条件前条件相当于与其余部分都形成了一份契约对于高健壮性的代码,应该先使用断言再处理错误两种措施同时使用
个人理解断言相当于能够运行的注释是代码之间的一份“契约”当其他开发者的修改破坏了“契约”断言能够主动指出修改存在问题。
但是目前使用Release模式开发的产品没有办法使用这一便利的工具。8.3 错误处理技术
断言可以用于处理代码中不应发生的错误错误处理技术处理那些预料中可能要发生的错误可以采用的措施 返回中立值换用下一个正确数据返回与前次相同的值换用最接近的有效值在日志文件中记录警告信息返回一个错误码调用错误处理子程序或对象显示出错信息或者关闭程序或把这些技术结合起来使用 权衡健壮性与正确性有的场景宁可返回也不能返回错误的数值此时需要牺牲健壮性来保证正确性相反有的时候需要有返回值来保证软件的正常运行不崩溃牺牲正确性来保证健壮性。
难点在与根据所处的场景选择合适的处理措施不合适的处理措施将会成为非常隐蔽的bug8.4 异常
异常是把代码中的错误或异常事件传递给调用方代码的一种特殊手段如一个子程序遇到了预料之外的情况,不知道该如何处理的情况,此时需要抛出异常。审慎明智地使用异常,它可以降低复杂度而草率粗心地使用时,只会让代码变得几乎无法理解。
用异常通知程序的其他部分,发生了不可忽略的错误能够避免错误扩散只在真正例外的情况下才抛出异常否则会增加复杂性、弱化封装性不能用异常来推卸责任如果某种的错误情况可以在局部处理,那就应该在局部处理掉它。避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获构造未完成会导致析构无法调用在恰当的抽象层次抛出异常在异常消息中加入关于导致异常发生的全部信息避免使用空的 catch语句至少要记录到日志里了解所用函数库可能抛出的异常未捕获可能会崩溃考虑创建一个集中的异常报告机制把项目中对异常的使用标准化考虑异常的替换方案
8.5 隔离程序,使之包容由错误造成的损害
通过对穿越安全区域边界的数据进行合法性校验,并进行清理实现隔离
8.6 辅助调试的代码
在调试时可以适当的牺牲一些运行速度和资源来换取更顺畅的开发体验通过辅助代码进攻性的暴露问题在开发阶段处理计划移除调试辅助代码发布前确保移除
8.7 确定在产品代码中该保留多少防御式代码
保留那些检查重要错误的代码去掉检查细微错误的代码去掉可以导致程序硬性崩溃的代码保留可以让程序稳妥地崩溃的代码为你的技术支持人员记录错误信息确认留在代码中的错误消息是友好的
8.8 对防御式编程采取防御的姿态
过度的防御式编程也会引起问题。
读后感商业软件需要充分考虑健壮性只是实现了主流程当用户输入异常的数据的时候就会“garbage in, garbage out”。
软件运行时出现的问题相较于主流场景比较不常见往往难以完全预料通过辅助代码可以帮助开发者在编码期间提前发现问题发现错误。
断言可以用于处理代码中不应发生的错误
错误处理技术处理预料到的能够想到处理方式的
异常用于抛出不知如何处理的错误第九章 伪代码编程过程
9.1 创建类和子程序的步骤概述
创建一个类的关键步骤如下:
创建类的总体设计创建类中的子程序复审并测试整个类
创建子程序的步骤 开始↓ 设计子程序 - 检查设计↑ ↓
复审并测试代码 - 编写子程序的代码 ↓完成9.2 伪代码
伪代码使得评审更容易伪代码支持反复迭代精化的思想伪代码使变更更加容易伪代码能使给代码作注释的工作量减到最少伪代码比其他形式的设计文档更容易维护
伪代码是比较理想的详细设计的工具
9.3 通过伪代码编程过程创建子程序
设计子程序
检查先决条件定义子程序要解决的问题承接高层次的设计决定要隐藏的信息输入输出前置条件为子程序命名决定如何测试子程序在标准库中搜寻可用的功能考虑错误处理考虑效率问题研究算法和数据类型编写伪代码考虑数据检查伪代码在伪代码中试验一些想法留下最好的想法迭代
感想事实上这也是设计期需要输出详细设计文档的条目编写子程序的代码
检查代码
收尾工作
按照需要重复上述步骤
感想本节详细介绍了编写子程序应该的执行的具体步骤好在我们处于技术浪潮的后期具有完备的IDE工具能够帮助我们处理许多繁杂的工作。9.4 伪代码编程过程的替代方案
测试先行开发重构契约式设计东拼西凑
感想代码编程过程是一个不断迭代的过程通过伪代码可以降低试错成本加快迭代周期是理想的详细设计工具。第十章 使用变量的一般事项
10.1 数据认知
感想这一节是一个认知测试自测了一下17分符合中级程序员的标准~作者很皮在测试里面加入了杜撰的选项用来测“诚实”10.2 轻松掌握变量定义
隐式变量声明对于任何一种语言来说都是最具危险性的特性之一变量混淆
感想隐式变量声明确实会带来一些难以理解的问题比如全局变量占用名称后局部使用同名变量可能期望生命一个同名的局部变量却会使用全局的变量。显示变量生命可以让编译器来发现拼写错误的问题。10.3 变量初始化原则
变量不初始化或者错误的初始化可能会引入一系列的问题建议
在声明变量的时候初始化在靠近变量第一次使用的位置初始化想情况下,在靠近第一次使用变量的位置声明和定义该变量在可能的情况下使用fina或者const可以防止该变量在初始化之后再被赋值特别注意计数器和累加器在类的构造函数里初始化该类的数据成员检查是否需要重新初始化一次性初始化具名常量;用可执行代码来初始化变量使用编译器设置来自动初始化所有变量利用编译器的警告信息检查输入参数的合法性使用内存访问检查工具来检查错误的指针在程序开始时初始化工作过内存
感想Qt的数据类型会自动初始化但是应该警惕不会自动初始化的数据类型。10.4 作用域
使变量引用局部化 避免不当地修改了这个变量,或者阅读代码的人可能会忘记该变量应有的值——主要好处是提高程序的可读性尽可能缩短变量的“存活”时间
减小攻击窗口使你能对自己的代码有更准确的认识少了初始化错误的可能代码容易拆分和重构更强的可读性
减小作用域的一般原则
在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量直到变量即将被使用时再为其赋值把相关语句放到一起把相关语句组提取成单独的子程序开始时采用最严格的可见性,然后根据需要扩展变量的作用域
有关缩小变量作用域的说明
“方便性”和“智力可管理性”两种理念之间的区别,归根结底来源于侧重写程序还是读程序之间的区别。使作用域最大化可能真的会让程序写起来比较容易,但相对于子程序功能划分明确的程序,一个允许任何其子程序在任何时间使用任何变量的程序是更难于理解的。对于这种程序,你不能只去理解一个子程序;你还必须要理解其他所有使用了相同全局数据的子程序才行。这种程序无论阅读、调试还是修改起来都很困难。
感想全局变量虽然在写代码的时候很方便但是对于阅读的人来说需要的代码才能理解用意。也不方便重构可移植性会下降。类中的私有成员变量也是一样的如果一个类特别大这个成员变量可能在各种意想不到的地方被修改给阅读造成困扰。如非必要尽可能使用作用域小的手段解决问题。10.5 持续性
“持续性”是对一项数据的生命期的另一种描述 “持续性”从短到长
特定代码段或子程序的生命期 循环里声明的变量只要你允许,它就会持续下去new出来的变量程序的生命周期static变量永久持续存到数据库里的变量
合理的“持续性”能够避免在一个变量正常的生命期结束之后访问它的数据为了确保生命周期有如下措施
调试代码或者断言判断变量取值是否合理准备抛弃变量时给它们赋上“不合理的数值”例如删除一个指针后把它的值设为null编写代码时要假设数据并没有持续性养成在使用所有数据之前声明和初始化的习惯
10.6 绑定时间
编码时(使用神秘数值)编译时(使用具名常量)加载时(从 Windows注册表、Java属性文件等外部数据源中读取数据)对象实例化时(例如在每次窗体创建的时候读取数据)即时(例如在每次窗体重绘的时候读取数据) 绑定时间越早灵活性就会越差,但复杂度也会越低
10.7 数据类型和控制结构之间的关系
序列型数据翻译为程序中的顺序语句如果你从文件中读取了员工的姓名、社会安全号码、住址、电话号码和年龄选择型数据翻译为程序中的if和case语句迭代型数据翻译成程序中的for、 repeat、 while等循环结构
10.8 为变量指定单一用途 每个变量只用于单一用途 避免让代码具有隐含含义 避免让代码具有隐含含义
第十一章 使变量名的力量
11.1选择好变量名的注意事项
为变量命名时最重要的考虑事项是,该名字要完全、准确地描述出该变量所代表的事物。获得好名字的一种实用技巧就是用文字表达变量所代表的是什么。通常,对变量的描述就是最佳的变量名这种名字很容易阅读,因为其中并不包含晦涩的缩写,同时也没有歧义。但不能太长以问题为导向一个好记的名字反映的通常都是问题,而不是解决方案。最适当的名字长度变量名的平均长度在10到16个字符调试程序所需花费的气力是最小的较长的名字适用于很少用到的变量或者全局变量,而较短的名字则适用于局部变量或者循环变量对位于全局命名空间中的名字加以限定词尽可能避免命名冲突变量名中的计算值限定词尽可能放在名字的最后一致性可以提高可读性,简化维护工作。使用常用对仗词避免产生歧义 begin/end first/last alocked/unlocked min/max next/previous old/new opened/closed visible/invisible source/target source/destination up/down
感想公司目前实行的编码规范对变量命名做了非常详尽的约定但是没有考虑变量的使用次数这个维度也应该考虑起来对于只在局部临时使用一次的变量可以起一个简短的名字而对于全局变量、成员变量这种需要一个能够完整描述清楚含义的名字。11.2为特定类型的数据命名
为循环下标命名
ijk用在比较短的循环中长的循环中建议使用更加有意义的名字。不要在其他场合使用i,j,k命名变量这些已经是深入人心的简单循环中的循环下标变量名称
为状态变量命名
为状态变量取一个比flag更好的名字
为临时变量命名
程序中大多数变量都是临时性的即使是临时变量也要尽可能提供更多信息。
为布尔变量命名
好的布尔变量命名done、error、found、success、OK 不好的布尔变量命名status 使用肯定的布尔变量名反例notFound
为枚举类型命名
通过前缀来明确表示该类型的成员都同属于一个组
为常量命名
命名需要表明常亮的含义
11.3命名规则的力量
为什么要有规则
要求你更多地按规矩行事有助于在项目之间传递知识有助于你在新项目中更快速地学习代码有助于减少名字增生避免同样的含义的变量在不同地方出现两个名字弥补编程语言的不足之处强调相关变量之间的关系
何时采用命名规则
略
正式程度
略
11.4非正式命名规则
与语言无关的命名规则的指导原则
区分变量名和子程序名字区分类和对象标识全局变量标识成员变量标识类型声明标识具名常量标识枚举类型的元素在不能保证输入参数只读的语言里标识只读参数格式化命名以提高可读性大小写和分隔符来分隔单词
与语言相关的命名规则的指导原则
以下是围绕着C编程形成的命名规则。
i、j是整数下标p是指针常量、typedef和预处理宏全部大写(ALL_CAPS)。类和其他类型的名字混合大小写(MixedUpperAndLowerCase())变量名和函数名中的第一个单词小写,后续每个单词的首字母大写,例: variableorRoutineName.不把下画线用做名字中的分隔符,除非用于全部大写的名字以及特定的前缀中(如用于标识全局变量的前缀)。
11.5标准前缀
标准化的前缀使名字变得更加紧凑
11.6创建具备可读性的短名字
缩写的一般指导原则:
使用标准的缩写(列在字典中的那些常见缩写)。去掉所有非前置元音。(computer变成cmpt, screen变成scrn, apple变成appl, integer变成 intgr。)去掉虚词and,or,the等使用每个单词的第一个或前几个字母。统一地在每个单词的第一、第二或者第三个(择最合适的一个)字母后截断。保留每个单词的第一个和最后一个字母。使用名字中的每一个重要单词,最多不超过三个。去除无用的后缀ing,ed等。保留每个音节中最引人注意的发音。确保不要改变变量的含义。反复使用上述技术,直到你把每个变量名的长度缩减到了8到20个字符,或者达到你所用的编程语言对变量名的限制字符数。 有关缩写的评论不要用从每个单词中删除一个字符的方式来缩写缩写要一致创建你能读出来的名字避免使用容易看错或者读错的字符组合使用辞典来解决命名冲突冲突的时候换同义词在代码里用缩写对照表解释极短的名字的含义在一份项目级的“标准缩写”文档中说明所有的缩写名字对于代码读者的意义要比对作者更重要
11.7应该避免的名字
避免使用令人误解的名字或缩写避免使用具有相似含义的名字避免使用标准类型、变量和子程序的名字
读后感作者在这一章非常详尽的介绍了关于变量命名的事项。代码阅读的次数远远多于编写的次数所以确保取的名字应该更侧重于阅读方便而不是编写方便。第十二章 基本数据类型
基本数据类型是构建其他所有数据类型的构造块
12.1数值概论
避免使用魔法数字方便修改更容易代码更可读循环和递增中的01不算魔法数字预防除0尽可能使用显示类型转换避免混合类型的比较注意编译器的警告
12.2整数
检查整数除法7/10不等于0.7它总是等于0检查整数溢出检查中间结果溢出
12.3浮点数
避免数量级相差巨大的数之间的加减运算避免等量判断要考虑误差处理舍入误差检查语言和函数库对特定数据类型的支持
12.4字符和字符串
避免使用神秘字符和神秘字符串神秘字符不要越界尽早决定国际化/本地化策略如果你需要支持多种语言,请使用 Unicode采用某种一致的字符串类型转换策略
12.5布尔变量
略
12.6枚举类型
用枚举类型来提高可读性用枚举类型来提高可靠性用枚举类型来简化修改将枚举类型作为布尔变量的替换方案检查非法数值定义出枚举的第一项和最后一项,以便用于循环边界把枚举类型的第一个元素留做非法值捕捉没有合理初始化
感想除非确定只有两个状态的情况下使用枚举代替布尔变量做标记会具有更好的扩展性。12.7具名常量
略
12.8数组
确保不越界考虑用容器来取代数组检查数组的边界点是否正确的找到第一个元素
12.9创建你自己的类型(类型别名)
略
第十三章 不常见的数据类型
13.1 结构体
“结构体”这一术语指的是使用其他类型组建的数据。类相对于结构体的优势除了公用数据成员外,还能利用类所提供的私密性和功能性。使用结构体的理由 用结构体来明确数据之间的关系用结构体简化对数据块的操作如交换两个结构体对象的数据用结构体来简化参数列表用结构体来减少维护参数列表传结构体添加或删除参数的时候无需调整所有的参数列表
感想在C中结构体也可以拥有自己的成员函数与类的区别在于默认情况下结构体的所有方法与成员都是public的继承时也是默认public继承13.2 指针
每一个指针都包含两个部分:内存中的某处位置,以及如何解 释该位置中的内容
正确使用指针的双向策略:
避免造成指针错误预防性措施尽快地检测出指针错误来
使用指针的一般技巧
把指针操作限制在子程序或者类里面封装复杂度同时声明和定义指针避免被错误的在定义前使用在与指针分配相同的作用域中删除指针函数里new却指望外部释放在使用指针之前检查指针先检查指针所引用的变量再使用它用狗牌字段来检测损毁的内存增加明显冗余用额外的指针变量来提高代码清晰声明名称清晰地临时变量降低理解成本简化复杂的指针表达式按照正确的顺序删除链表中的指针分配一片保留的内存后备区域粉碎垃圾数据在删除或者释放指针之后把它们设为空值在删除变量之前检查非法指针跟踪指针分配情况编写覆盖子程序,集中实现避免指针问题的策略采用非指针的技术
C 指针与引用
引用必须总是引用一个对象,而指针则可以指向空值,还有,引用所指向的对象在该引用初始化之后不能改变把指针用于“按引用传递”参数,把cons引用用于“按值传递”参数使用auto_ptr已经被 unique_ptr 取代
感想
之前见到的情况1.主流程调用的子程序new了指针A主程序没有记得释放指针A。2.主流程调用的子程序new了指针A主程序记得释放指针A但是在释放前指针的被另一个子程序拷贝给了指针B导致调用指针B的时候已经被释放了崩溃。3.主流程调用的子程序new了指针A另一个子程序对指针A所指的对象进行加工存进了指针B并对指针A进行了释放主程序不知道A已经被释放再次delete指针A崩溃。4.满世界传智能指针导致效率问题。
好的习惯初始化的时候不想new的时候初始化为nullptr释放之后顺手给指针赋值nullptr使用之前就可以直接判断指针是否等于nullptr13.3 全局数据
全局数据违背信息隐藏和模块化的原则增加理解和维护成本不推荐使用使用更好的方式取代。
全局数据带来的问题
无意间修改了全局数据与全局数据有关的奇异的和令人激动的别名问题全局变量被当作参数传入时会有两个变量实际上是一个变量与全局数据有关的代码重入(re-entrant)问题多个线程、多个程序访问同一个全局变量全局数据阻碍代码重用依赖全局环境了拆不出来与全局数据有关的非确定的初始化顺序事宜全局数据破坏了模块化和智力上的可管理性
使用全局数据的理由
保存全局数值概念上用于整个程序的数据模拟具名常量模拟枚举类型简化对极其常用的数据的使用避免出现在每一处参数列表里面消除流浪数据一个数据经过层层传递才被使用
只有万不得已时才使用全局数据
替代方案:
首先把每一个变量设置为局部的,仅当需要时才把变量设置为全局的(局部变量-成员变量-全局变量谨慎的逐步放开)区分全局变量和类变量模块化使用访问器子程序你用全局数据能做的任何事情,都可以用访问器子程序做得更好优点 获得了对数据的集中控制可以确保对变量的所有引用都得到了保护可以自动获得信息隐藏的普遍益处访问器子程序可以很容易地转变为抽象数据类型
如何使用访问器子程序
把数据隐藏到类里面。用static关键字或者它的等价物来声明该数据以确保只存在该数据的单一实例。写出让你可以查看并且修改该数据的子程序来。要求类外部的代码使用该访问器子程序来访问该数据,而不是直接操作它。
要求所有的代码通过访问器子程序来存取数据不要把你所有的全局数据都扔在一处丧失了信息隐藏和抽象数据类型所带来的好处用锁定来控制对全局变量的访问开发阶段发现多处同时使用的防范措施在你的访问器子程序里构建一个抽象层获取下一个全局id使得对一项数据的所有访问都发生在同一个抽象层上
如何降低使用全局数据的风险
创建一种命名规则来突出全局变量为全部的全局变量创建一份注释良好的清单不要用全局变量来存放中间结果不要把所有的数据都放在一个大对象中并到处传递以说明你没有使用全局变量
第十四章 组织直线型代码
14.1必须有明确顺序的语句
首先要尽力写没有顺序依赖关系的代码。其次尽力写依赖关系明显的代码。 通过名称暗示 调用顺序要求init一定最先调用计算一定在读取之后输出一定在计算之后通过传递相同的参数暗示顺序处理同一个对象上一行的输出是下一行的输入强制要求依赖 如果你还担心某一项依赖关系不够清楚,那么就用文档说明它。用断言或者错误处理代码来检查依赖关系
14.2顺序无关的语句
把相关操作放在一起使代码易于自上而下地阅读
读后感
本书最短的一章了特别喜欢通过参数列表表达依赖顺序的这个点子在调整其他人的代码的时候能够清楚的了解到参数的流动过程是一种非常理想的阅读体验第十五章 使用条件语句
15.1 if语句
使用指导原则
首先写正常代码路径;再处理不常见情况确保对于等量的分支是正确判断条件正确严谨把正常情况的处理放在if后面而不要放在else后面让if子句后面跟随一个有意义的语句不能为空考虑else子句测试else子句的正确性检查if和else子句是不是弄反了
if-then–else语句串连续的else if
利用布尔函数调用简化复杂的检测提高可读性把最常见的情况放在最前面确保所有的情况都考虑到了
15.2 case语句
为case选择最有效的排列顺序
按字母顺序或按数字顺序排列各种情况把正常的情况放在前面按执行频率排列case子句
技巧
简化每种情况对应的操作代码结构更清晰不要为了使用case语句而刻意制造一个变量把 default子句只用于检查真正的默认情况不是最后一种情况default不是else利用 default子句来检测错误避免代码执行越过一条case子句的末尾避免“聪明的”不适用break语句增加阅读困难或者用错了否则给个注释
感想之前见过为了对字符串进行switch-case对字符串进行MD5计算似乎这样做是不对的第十六章 控制循环
“循环”是一个非正式的术语,用来指代任意一种迭代控制结构(iterative control structure)任一能够导致应用程序反复执行一段代码的结构。
16.1 选择循环的种类
计数循环执行的次数是一定的连续求值的循环迭代时检查是否应该结束无限循环心脏起搏器中就会用迭代器循环对容器类里面的每个元素执行
灵活度 严格——循环执行的次数是一定的, 灵活——在每次迭代的时候检查循环有没有完成检查循环是否执行完毕的位置 循环的开始循环中的代码不一定被执行 中间检查之前的代码一定被执行 结尾处代码至少执行一次
什么时候使用 while循环
如果你预先并不知道循环要迭代多少次,那么就使用 while循环
while在开始时检查条件do-while在结束时检查条件
什么时候用带退出的循环
正常的带退出循环
在检查前的代码至少要执行一次的情况使用带退出的循环 关注细节
把所有的退出条件放在一处易读不直接支持带退出循环的语言里使用带退出循环时用注释来阐明操作意图
非正常的带退出循环
用goto中途闯进一个循环恐怖
何时使用for循环
需要简单的、固定次数的循环
不要在内部修改下标影响循环在头部写好循环条件后内部无需关注循环控制问题
何时使用 foreach循环
很适用于对数组或者其他容器的各项元素执行操作它的优势在于消除了循环内务处理算术,从而也就消除了任何由循环控制算术导致出错的可能性。
感想使用循环是程序员的基本功之一foreach用起来很方便也能够提高代码的可读性使阅读者理解编码者要对容器中每个元素进行操作。目前使用的foreach是Qt提供的std库提供了for_each模板函数个人比较喜欢使用C11标准中提供了标准的for(auto item:container){;}。16.2循环控制
循环会出显得错误
忽略或错误地对循环执行初始化忽略了对累加变量或其他与循环有关的变量执行初始化不正确的嵌套不正确的循环终止忽略或者错误地增加了循环变量的值用不正确的循环下标访问数组元素 阻止上述错误的策略减少能影响该循环各种因素的数量把循环内部当做一个子程序看待
感想“当做一个子程序看待”但不是真的循环里放一个子程序会带来没办法break的问题C11中std::for_each()函数就有这个问题循环体在lambda表达式中不能提前终止循环。进入循环
只从一个位置进入循环把初始化代码紧放在循环前面就近原则易于修改用 while(true)表示无限循环想象一下用在心脏起搏器中的场景不能使用一个大数在适当的情况下多使用for循环控制集中易于修改在 while循环更适用的时候,不要使用for循环for循环头部只放循环控制语句
处理好循环体
用“{”和“}”把循环中的语句括起来增加可读性避免空循环避免循环内部没有语句功能在控制时执行一个循环只做一件事除非影响效率
退出循环
设法确认循环能够终止使循环终止条件看起来很明显不要为了终止循环而胡乱改动for循环的下标for循环内部不要控制循环本身避免出现依赖于循环下标最终取值的代码下标作用域超出循环范围考虑使用安全计数器防御以避免死循环
提前退出循环
continue不会让程序从循环退出,而是让程序跳过循环体的余下部分,从该循环的下一次迭代的开始位置继续执行。而break会直接退出循环。考虑在 while循环中使用 break语句而不用布尔标记增加可读性在循环开始处用 continue进行判断可以避免整个循环放在if里面使用 break和 continue时要小心谨慎增加了复杂度除非必要尽量少用
检查端点
脑海中模拟并计算确保边界条件正确 感想低效的程序员会通过尝试写出能够正确工作的代码不知道代码为什么正确后面就更不知道为什么为什么出错了使用循环变量
用整数或者枚举类型表示数组和循环的边界别用浮点数在嵌套循环中使用有意义的变量名来提高其可读性用有意义的名字来避免循环下标串话避免用错下标把循环下标变量的作用域限制在本循环内
循环应该有多长
循环要尽可能地短,以便能够一目了然把嵌套限制在3层以内把长循环的内容移到子程序里要让长循环格外清晰
感想通过这一节的阅读我发现编程语言对循环的限制确实不多还能够写出这么多奇形怪状的循环作者始终在强调避免使用这些奇怪的方式提高代码的可读性和可维护性。事实上循环应该有的样子已经非常深入人心了奇怪的写法不单读起来头大写起来也很别扭的。目前只见过foreach循环容器的过程中删除容器元素一种比较难受的情况下。16.3轻松创建循环由内而外
先写单一情况再在外面套上循环
16.4循环和数组的关系
略
读后感使用主流的方式使用循环不要写“聪明”代码保证代码的可读性和可维护性。第十七章 不常见的控制结构
17.1 子程序中的多处返回
如果能增强可读性,那么就使用 return得到结果立即返回用防卫子句来简化复杂的错误处理避免过多的嵌套提前用return掉异常场景然后在处理正常场景减少每个子程序中 return的数量
17.2 递归
在递归( recursion)里面,一个子程序自己负责解决某个问题的一小部分,它还把问题分解成很多的小块,然后调用自己来分别解决每一小块。当问题的小部分很容易解决,而问题的大部分也很容易分解成众多的小部分时,常常会用到递归。 对于某一小范围内的问题,使用递归会带来简单、优雅的解。在稍大一些范围里,使用递归会带来简单、优雅但是难懂的解对于大多数问题,它所带来的解将会是极其复杂的——在那些情况下,使用简单的迭代通常会比较容易理解。因此要有选择地使用递归。 使用递归的技巧
确认递归能够停止通过检查避免无穷递归使用安全计数器防止出现无穷递归
感想记得python中就设置了默认递归上限C中可以自己通过assert实现把递归限制在一个子程序内留心栈空间 避免栈溢出特别要留意那些内存消耗大的对象用new在堆(heap)上创建对象,而不要让编译器在栈上面自动创建对象不要用递归去计算阶乘或者斐波纳契数列 速度缓慢,并且无法预测运行期间的内存使用状况、更难理解递归可以做到的同样也可以用栈和循环来做到注意方案选择
17.3 goto
用不用goto是一个信仰问题
感想这一节中作者尽可能中立的描述了goto的优点和缺点作者个人持有的观点是除非万不得已尽可能不使用goto。事实上goto绝大多数情况下是可被替代的在C语言多数情况下有不止一种手段能够替换掉gotogoto影响阅读体验不合理的使用可能带来许多问题个人不赞成使用。17.4 针对不常见控制结构的观点
略
第十八章 表驱动法
从表里面查找信息而不使用逻辑语 句(if和case)。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。对简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力。
18.1 表驱动法使用总则
在适当的环境下,采用表驱动法,所生成的代码会比复杂的逻辑代码更简单、更容易修改,而且效率更高。 使用表驱动法的两个问题
怎么从表中查询条目 直接访问索引访问阶梯访问 应该在表里存些什么
18.2 直接访问表
可以将数据作为键值直接访问表 当数据和键值不是一一对应的时候的办法
复制信息从而能够直接使用键值产生冗余信息增加存在错误的可能转换键值以使其能够直接使用把键值转换提取成独立的子程序
18.3 索引访问表
优点
数据很大时可以节约空间降低操作成本易于维护同一张表可以有多个索引表增加查询方式
18.4 阶梯访问表
比索引访问表节省空间适合处理无规则数据 注意
留心端点考虑用二分查找取代顺序查找考虑用索引访问来取代阶梯技术减少访问耗时把阶梯表查询操作提取成单独的子程序
18.5 表查询的其他示例
略
读后感这是读这本书以来第一个对标题没有概念的章节。也是第一次听说表驱动法。表驱动法通过表查询的方式取缔了大量同级别的if语句从而帮助开发者将精力用在解决实际问题上而不是处理逻辑判断。通过表驱动法可以实现数据驱动即将表保存在文件中程序启动时主动加载这样在需要修改时修改数据文件而无需修改代码影响范围会变得更为可控。第十九章 一般控制问题
19.1布尔表达式
用true和 false做布尔判断
(而不要用0和1等数值,易混淆可读性差)
隐式地比较布尔值与true和 false要while(ab),不要while((ab)true)
简化复杂的表达式
拆分复杂的判断并引入新的布尔变量把复杂的表达式做成布尔函数即使只调用一次把复杂的判断逻辑放在命名良好函数里也能大大提高代码的可读性用决策表代替复杂的条件表驱动法
编写肯定形式的布尔表达式
多重否定增加阅读难度可以使用狄摩根定理简化否定的布尔判断(!A||!B)!(AB)
用括号使布尔表达式更清晰
理解布尔表达式是如何求值的
感想C采用短路的方式计算布尔表达式的值所以平时才能方便的这样写 if((pContainer ! nullptr) (pContainer-hasChildren)) {}在第一个条件为是的时候才会执行第二个语句如果没有这个机制执行第二个语句的时候可能因为访问空指针崩溃 按照数轴的顺序编写数值表达式
从左到右从小大大。“min x x max” 比 x min max x更容易理解。
与0比较的指导原则
编程语言把0用做很多目的。它是一个数值,是字符串中的零终止符,是空 指针的取值,是枚举的第一个元素的取值,是逻辑表达式中的 false既然它有 如此多的用途,因此你写的代码中就应该彰显0的特定用法。
隐式地比较逻辑变量把数和0相比较(表达变量数据类型)把指针与NULL相比较C11推荐使用nullptr
感想比较的过程也要考虑数据类型不能偷懒都按数字去比较尽可能给读代码的人更多确定的信息布尔表达式的常见问题
C家族语言中,应该把常量放在比较的左端通过编译器报错来避免误把“”写成“”
感想布尔判断是开发者最常用的控制方式事实上工作中遇到的相当多的bug是由布尔判断出错造成的。优化判断语句的可读性对于代码的阅读和维护都有很大的帮助。19.2复合语句(语句块)
“复合语句”或“语句块”指的是一组语句,该组语句被视为一条单一的语 句,用于控制程序流。
把括号对一起写出先写好一堆括号再填充内容避免落下用括号来把条件表达清楚if后面执行语句在括号里哪怕只有一句
19.3空语句
C中一个“”就是一个空语句可以用来占位如while后必须有一条语句可以用“”。
小心使用空语句为空语句创建一个 DoNothing()预处理宏或者内联函数(说明确实什么都不想做)考虑如果换用一个非空的循环体,是否会让代码更清晰
感想我能想到的场景读文本文件一开始希望先跳过前面的几行通过循环执行n次的read使文件指针指向希望行数。19.4驯服危险的深层嵌套
软件首要技术使命管理复杂度很少有人能够理解超过3层的if嵌套 避免深层嵌套的方法
通过重复检测条件中的某一部分来简化嵌套的if语句用 break块来简化嵌套if把嵌套if转换成一组if-then-else语句把嵌套if转换成case语句把深层嵌套的代码抽取出来放进单独的子程序使用一种更面向对象的方法利用继承和多态重新设计深层嵌套的代码用状态变量重写代码用防卫子句来退出子程序,从而使代码的主要路径更为清晰使用异常
感想深层的嵌套写起来方便读起来难受代码读的次数要比写的次数多所以写的时候要更多的考虑读者的感受。另一方面深层的嵌套会影响自动化的代码覆盖率比较内层的条件判断很难被覆盖。19.5编程基础:结构化编程
一个应用程序应该只采用一些单入单出的控制结构不会做不可预知的随便跳转
结构化编程的三个组成部分
顺序选择迭代 结构化编程的中心论点是,任何一种控制流都可以由顺序、选择和迭代这三种结构生成
19.6控制结构与复杂度
控制流是影响复杂度的最大的因素之一永远也不可能有能力应对如此巨大的复杂度,因此只有尽可能地采取措施来降低复杂度
读后感非常喜欢本章的一句话“程序员有时候会倾向于使用那些更方便的语
言结构,但是编程这一领域却似乎更多地是在对我们能用编程语言做些什么加以
限制的过程中取得发展的。”。对于我而言读代码的时间一般都比写代码的时间还要长改bug、扩展功能都需要对原油代码充分的理解。结构化编程的思想将结构限制在了顺序、选择和迭代三种使得复杂度得以被控制阅读代码比goto满天飞的时代要轻松很多减少了花样写法的偶然复杂度有更多的精力去应对业务逻辑带来的实际复杂度。第二十章 软件质量概述
20.1软件质量的特性
20.2改进软件质量的技术
20.3质量保证技术的相对效能
20.4何时进行质量保证
20.5软件指令的普遍原理
第二十一章 协同构建
21.1协同开发实践概述
21.2结对编程
21.3正式审查
21.4其他类型的协同开发实践
第二十二章 开发者测试
测试是最常见的改善质量的活动
单元测试(Unit testing)组件测试( Component testing)集成测试(Integration testing)回归测试(Regression testing)系统测试(System testing) 测试也分为黑盒测试和白盒测试
22.1开发者测试在软件质量中的角色
对于任何软件质量规划来说,测试都是一个重要的组成部分每个独立的测试步骤通常只能够找到现有错误的50%不到测试的目标是找出错误与其他开发活动的目标背道而驰测试永远不可能彻底证明程序中没有错误测试本身并不能改善软件的质量测试时要求你假设会在代码里面找到错误自我实现开发者测试应该占整个项目时间的8%~25%
怎样利用开发者测试的结果
评估正在开发的产品的可靠性用于指导对软件的修正帮助于你归纳出程序中最常见错误的类型帮助去选择适当的培训课程、指引今后的技术复查活动,设计未来的测试用例。
构建中测试
白盒测试的好处除了观察它的输入输出,还要察看内部的源代码。如果知道盒子里面的情况,可以更彻底地测试这个类 黑盒测试的好处能测出开发过程中的盲点
独立运行测试相较于继承后的测试能够更简单的发现问题
感想1. 有的时候开发人员与测是人员沟通起来会觉得测试的场景难以想象实际上这就是黑盒测试带来的好处能够找出开发过程中的盲点2. 子程序、类甚至是子模块在集成之前的测试通常会更加容易的发现问题在集成之前这部分代码 不受外部条件的影响条件相对简单更容易观察。另一方面没有连接界面之前测试用例的运行效率接口测试会高很多相较于Ui自动化。对于开发者而言独立运行的部分编译和调试的速度都会快很多能够快速试错更容易的修正一个问题。但是独立运行需要剥离开外部的环境依赖需要从设计时就考虑清楚。22.2开发者测试的推荐方法
对每一项相关的需求进行测试,以确保需求都已经被实现对每一个相关的设计关注点进行测试,以确保设计已经被实现用基础测试( basis testing)来扩充针对需求和设计的详细测试用例增加数据流测试(data–flow test),然后补充其他所需的测试用例使用一个检查表,其中记录着你在本项目迄今为止所犯的,以及在过去的项目中所犯的错误类型
在设计产品的时候设计测试用例,这样可以帮助避免在需求和设计中产生错 误,修正这些错误的代价往往比修正编码错误更昂贵越早修复这些缺陷,成本 就越低,因此,要尽可能早地对测试进行规划并找出缺陷。
测试先行还是测试后行答案测试先行
首先写测试用例可以将从引入缺陷到发现并排除缺陷之间的时间缩减至最短在开始写代码之前先写测试用例,并不比之后再写要多花功夫,只是调整了一下测试用例编写活动的工作顺序而已假如你首先编写测试用例,那么你将可以更早发现缺陷,同时也更容易修正它们首先编写测试用例,将迫使你在开始写代码之前至少思考一下需求和设计先编写测试用例,能更早地把需求上的问题暴露出来
开发者测试的局限性
开发者测试倾向于“干净测试”开发人员往往去做一些检验代码能否工作的测试(干净测试, clean tests),而不是做所有可能让代码失效的测试(肮脏测试,dirty tests)肮脏应该更多开发者测试对覆盖率有过于乐观的估计开发者测试往往会忽略一些更复杂的测试覆盖率类型所有分支覆盖 开发者测试是有价值的,但对于提供足够的质量保证而言,仅仅进行开发者测试是不够的。我们需要补充其他的实践,包括独立测试(independent testing)技术以及协同构建collaborative construction)技术。
22.3测试技巧锦囊
通过测试来证明程序的正确性是不可能的呢
不完整的测试
需要集中注意力挑选出那些能告诉你不同答案的测试用例,而不选出一堆总是告诉你相同答案的测试用例。
结构化的基础测试
你需要去测试程序中的每一条语句至少一次如果语句是一个逻辑语句例如if语句或者 while语句,那么你就需要根据if者中表达式的复杂程度 来修改测试,以确保这个语句完全经过了测试要确保你已经覆盖了所有的基础情况这种测试能够向你保证所有的代码都得到执行但它并不能说明数据的变化情况。
数据流测试
数据使用的出错几率至少不亚于控制流数据的状态应该按照“已定义”-“已使用”-“已销毁”的顺序变化重复的定义和销毁、错乱的顺序都会引起bug
等价类划分
一个好的测试用例应该覆盖可输入数据中的很大一部分如果两个用例能揭示的错误完全相同,那么只要一个就够了。
猜测错误
在猜测程序会在哪里出错使用软件隐的基础之上建立测试用例基于直觉或者过去的经
边界值分析
可以发现分析off-by-one错误
复合边界值
当边界条件涉及到互相关联的多个变量的时例如,两个变量相乘,它们的值都是大的正数
几类坏数据
数据太少(没有数据)太多的数据错误的数据情况(无效数据)长度错误的数据未初始化的数据
eg:薪水是负数、人数是负数
几类好数据
测试用例需要测试正常的数据是否能够正常工作包括
正常的情形大路正中间,所期望的值最小的正常局面最大的正常局面与旧数据的兼容性
采用容易手工检查的测试用例
手工计算过程犯错的几率跟你在程序中发现错误的几率差不多会增加测试的难度选择容易手动计算的情况也能帮助发现同样的错误
22.4典型错误
哪些类包含最多的错误
80%的错误存在于项目20%的类或者子程序当中50%的错误被发现存在于项目5%的类当中项目中20%的子程序占用了80%的开发成本提高质量就能缩短开发周期,同时降低开发成本避免卷入到那些烦人的子程序中,序,就是那些极那么你就可以省下近80%的成本从而节约一大段开发时间
错误的分类
大多数错误的影响范围是相当有限的85%的错误可以在修改不超过一个子程序的范围内得以修正许多错误发生在构建的范畴之外缺乏应用领域知识,频繁变动且相互矛盾的需求,以及沟通和协调的失效大多数的构建期错误是编程人员的失误造成的程序员造成的占95%系统2%其他软件2%硬件1%笔误(拼写错误)是一个常见的问题根源占36%错误理解设计占16%-19%大多数错误都很容易修正大约85%的错误可以在几个小时的时间内修正
不完善的构建过程引发错误所占的比例
在小型项目里面, 75%的错误由编码造成10%的错误源自需求,以及15%源自设计无论项目规模如何,构建缺陷至少占了总缺陷的35%
你期望能发现多少错误
软件质量的普遍原则:开发高质量的软件,比开发低质量软件然后修正的成本要低廉
测试本身的错误
开发人员在编写测试用例在没有经过仔细地设计和构建的前提下测试用例可能包含同被测代码同样多,甚至是更多的错误应对建议
谨慎的开发测试用例并检查开发软件的时候就要计划好测试用例保留你的测试用例将单元测试纳入测试框架
22.5测试支持工具
为测试各个类构造脚手架
在软件中搭建脚手架只有一个目的,那就是更方便地测试代码。 脚手架可以实现
立刻返回控制权,不做任何动作检查传给它的数据;输出诊断信息,可能是显示所传入的参数,或者是将信息记录到日志文件中;返回用户交互输入的值;不管输入是什么都返回一个标准的响应;消耗原本分配给真实对象或者真实子程序的时钟周期以某种慢速、臃肿、简单或粗略的方式实现真实对象或者子程序的功能。
另一种脚手架类型,是调用待测试的真实函数的伪造函数。这种脚手架称为 “驱动函数”,有时也称为“测试夹具”。这种脚手架可以:
用固定的一组输入调用对象;提示用户输入,然后根据输入去调用对象;从命令行取得参数(如果操作系统支持)去调用对象从文件中读入参数,并据此调用对象;用一集预先定义的输入数据去多次调用有关的对象。
感想目前在做的造价云适配任务里面就有一个前人搭好的脚手架GMPCloudServiceMoc能够模拟造价云的返回结果起作用是不管输入是什么都返回一个标准的响应使得测试过程变得更加便利Diff Tools
一个能自动对比实际输出与期望输出的工具便于进行回归测试这个在重构的时候应该很有用保证重构后的程序关键输出不变
测试数据生成器
作者在开发加密解密程序的时候使用了这个技术覆盖到了意想不到的场景而且可以不消耗人力持续测试
覆盖率监视器
用来检测现有测试用例是否能够彻底地对代码进行测试那些没有测量代码覆盖率的测试,通常只测试到了大约50%到60%的代码
数据记录器/日志记录器
略
符号调试器
完全按照计算机的方式来演绎代码的执行这个不应该算是下一章调试的内容么
系统干扰器
发现忘记初始化的问题
内存填充内存抖动选择性内存失败内存访问检查
错误数据库
指的是目前在使用的jira
22.6改善测试过程
有计划的测试
重新测试(回归测试)
自动化测试
自动化测试发生错误的几率比手动测试要小。一旦你把一个测试自动化了,那么你只需稍下功夫,就很容易在项目的剩余部分继续实施自动化。如果测试是自动进行的,那么就可以频繁地运行自动化测试可以提高问题刚产生就被发现的可能性为大规模代码修改提供了一张安全网
22.7保留测试记录
指的是目前在使用的jira通过记录数据更宏观的观察项目的趋势。
读后感目前尝试过单元测试、接口测试、记录日志等自测手段有了测试手段之后修改或者重构就更加安心原来可改可不改的代码就有更足够的底气去重构了否则有可能为了遵从良知拉着整个团队下水。在平台更深刻地感觉到提早发现问题的好处一个接口及得错误一旦发布给产品产品发布给用户修改起来需要适配还可能需要处理用户数据的升级本来在发布之前发现可能需要1个小时就能够改正的错误发现的晚甚至可能消耗一周。第二十三章 调试
调试是确定错误根本原因并纠正此错误的过程
23.1调试概述
调试在软件质量中所扮演的角色
同测试一样,调试本身并不是改进代码质量的方法,而是诊断代码缺陷的一种方法。软件的质量必须从开始逐步建立开发高质量软件产品的最佳途径是精确描述需求,完善设计,并使用高质量的代码编写规范。调试只是迫不得已时采用的手段。
调试效率的巨大差异
经验丰富的程序员找出缺陷所用的时间大约只是缺乏经验的程序员们的1/20
让你有所收获的缺陷
程序中的错误为你提供了学习很多东西的绝好机会
理解你正在编写的程序明确你犯了哪种类型的错误从代码阅读者的角度分析代码质量审视自己解决问题的方法审视自己修正缺陷的方法
调试其实是一片极其富饶的土地,它孕育着你进步的种子
一种效率低下的调试方法
错误的调试方法猜测、不理解问题本身、用最唾手可得的方式修正错误、迷信式调试
23.2寻找缺陷
调试包括了寻找缺陷和修正缺陷。寻找缺陷——并且理解缺陷通常占到了整个调试工作的90%
科学的调试方法
一种寻找缺陷的有效方法
将错误状态稳定下来bug要可重复可稳定复现确定错误的来源 a. 收集产生缺陷的相关数据 b. 分析所收集的数据,并构造对缺陷的假设 c. 确定怎样去证实或证伪这个假设,可以对程序进行测试或是通过检查代码 d. 对假设做出最终结论修补缺陷对所修补的地方进行测试查找是否还有类似的错误
寻找缺陷的一些小建议
在构造假设时考虑所有的可用数据提炼产生错误的测试用例自己的单元测试族中测试代码采用多种不同的方法重现错误用更多的数据生成更多的假设利用否定性测试用例的结果对可能的假设尝试头脑风暴把需要尝试的事情逐条列出缩小嫌疑代码的范围对之前出现过缺陷的类和子程序保持警惕检查最近修改过的代码扩展嫌疑代码的范围增量式集成同其他人讨论问题小黄鸭调试法组织语言的时候就理清了最近改的一个bug就用的这个抛开问题,休息一下
蛮力调试
快速尝试可能解决蛮力调试包括直接重写往往会奏效。花费2个小时去调试一个30分钟就能写出来的代码就是坚持快速尝试的后果有的时候蛮力调试更有效。
语法错误
不要过分信任编译器信息中的行号不要迷信编译器信息不要轻信编译器的第二条信息先看第一条分而治之找出没有配对的注释或者引号
23.3修正缺陷
在动手之前先要理解问题理解程序本身,而不仅仅是问题验证对错误的分析放松一下压力大匆忙提交可能会引入新的问题这条对于系统测试凌晨改bug很适用欲速则不达保存最初的源代码方便回滚治本,而不是治标修改代码时一定要有恰当的理由一次只做一个改动检查自己的改动增加能暴露问题的单元测试搜索类似的缺陷
23.4调试中的心理因素
对熟悉东西的先入为主的判断可能是错误的需要保持警惕
23.5调试工具
源代码比较工具编译器的警告消息增强的语法检查和逻辑检查执行性能剖测器测试框架/脚手架调试器
读后感调试技巧能够大幅提高定位问题的速度我在第一次知道调试时左侧的箭头可以前后拖动的时候感觉打开了新世界的大门从此很多场景不用因为一不小心错过了重新操作一遍复现路径了类似的还有条件断点。第二十四章 重构
在整个项目生命周期中代码都会不断地演化
24.1软件演化的类型
程序构建过程中的演化系统处于高度动态阶段,出现错误的代价较小维护过程中的修改 软件演化是无法避免且具有重要意义的现象演化一开始就充满危险,但同时也是使你的软件开发接近完美的天赐良机软件演化的基本准则就是,演化应当提升程序的内在质量。
24.2重构简介
在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解 并便于修改
重构的理由
代码重复
感想“复制粘贴即设计之谬”修改代码时不得不记得同时修改多处后人维护的时候一旦忘记就会引入bug之前做过一个小需求修改软件显示的两个字原本只需要改一个字符串结果全局好多处都在通过这个字符串做条件判断这两个字在软件中应用场景很多又不能通过搜索替换的凡是批量处理。实际上我们有很多方式能够避免重复子程序、继承、模板 甚至动态库静态库。子程序太长
感想模块化封成更短的功能单一的子程序之前写过一个600行的初始化UI的子程序自己都不想看循环太长或者嵌套太深循环内部的复杂代码常常具备转换为子程序的潜质类的内聚性太差某个类大包大揽了许多彼此无关的任务类的接口的抽象层次不一致参数表中参数太多参数是否应该封为结构体类的内部修改往往局限于某个部分类是否能够拆分为多个类需要对多个类进行并行修改类是否可以重新组织对继承体系的并行修改需要对多个case语句进行并行修改采用继承也许更合适相关的数据项只是被放在一起,没有组织到类中成员函数更多地使用了其他类的功能,而非自身类的这个函数也许不应该属于这个类过多使用基本数据类型一个类不做什么事重构之后没清理后面的程序员会困惑一连串传递流浪数据的子程序中间人对象什么也不干某个类同其他类关系过于密切子程序的命名太差数据成员被设置为公用派生类仅仅使用了基类的一小部分成员函数是否应该把派生类改为基类的数据成员用注释来掩饰拙劣的代码使用了全局变量在子程序调用前使用设置代码,调用后使用收尾代码接口的抽象不合理程序包含的某些代码似乎在将来某个时候才会被用到不应该超前设计尽可能将满足当前需求的代码清晰直白地表现出来,使未来的程序员理解这些代码到底完成了什么功能,没有完成什么功能
感想编码很难在一开始就想清楚所有的事情随着编码的进行开发者对事情逻辑的理解会逐渐深入
或者完成编码后的一段时间回头看可能会有新的理解
或者一段程序经过太多补丁处理已经非常难以理解了这些都是可以是重构的理由
重构的目的一定是要提升代码的质量。24.3特定的重构
数据级的重构
用具名常量来代替神秘数值用更明确或更具信息量的名字来重命名变量将表达式内联化用函数来代替表达式引入中间变量将多用途变量转换为多个单一用途变量使用局部变量实现局部用途而不是使用参数将基础数据类型转化为类将一组类型码转化为类或是枚举类型将一组类型码转化为含派生类的类将数组转化为对象封装群集用数据类替代传统记录
语句级的重构
分解布尔表达式将复杂的的布尔表达式转换为命名精确的布尔函数将条件语句中不同部分中的重复代码合并使用 break或 return而不是循环控制变量在嵌套的if-thenlse语句中一旦知道结果就立刻退出,而不是仅仅赋一个返回值用多态来代替条件语句(尤其是重复的case语句)创建并使用空对象代替对空值的检测
子程序级的重构
提取子程序将子程序代码内联化将冗长的子程序转化为类用简单的算法替代复杂算法增加参数减少参数将查询操作同修改操作区分开来合并功能相似的子程序,并用参数来区分他们通过传递不同的参数使子程序体现不同的功能传递整个对象而非特定成员传递特定成员而非整个对象封装向下转型操作
类实现的重构
将值对象改为引用对象将引用对象改为值对象用数据初始化来代替虚函数改变成员函数或数据的位置将特定代码提出生成派生类将相似的代码合并起来放到基类中
类接口的重构
将某成员子程序放到另一个类中将一个类转化成两个删除某个类隐藏委托关系去掉中间人用委托代替继承用继承代替委托引入外部子程序引入扩展类封装暴露在外的成员变量对不能修改的成员去掉set()函数隐藏在类的外部不会使用的成员函数封装不会用到的成员函数如果基类和派生类的代码实现相似,将二者合并
系统级的重构
为无法控制的数据创建明确的索引源将单向类联系改为双向类联系将双向的类联系改为单向类联系使用工厂函数而非简单的构造函数用异常代替错误代码,或者反其道而行之
24.4安全的重构
重构的步伐请小些同一时间只做一项重构把要做的事情一条条列出来利用编译器警告信息重新测试增加测试用例检查对代码的修改根据重构风险级别来调整重构方法避免用重构代替重写
24.5重构策略
感想
真实的世界是复杂的我们的代码经常需要处理复杂的业务规则而混乱
我们通过接口将复杂的因素处理成理想的情况在通过清晰的逻辑去处理理想的情况
在重构的过程中尽可能扩大逻辑清晰的理想情况的代码的边界
减少对接复杂业务逻辑的混乱的代码。读后感经过最近的任务更深刻的理解了重构的代价底层平台的重构中台需要适配产品有也需要适配如果产品已经发版还需要考虑升级可能一个非常小的接口改动都需要配置产品环境适配代码打版测试*n的工作量。但是如果一个平台因为这些负担停止了代码的演化则无法持续交付价值所以需要在不被负担拖垮的前提下重构开发阶段重构的成本是最低的所以需要尽早发现问题。第二十五章 代码调整策略
本章讨论程序性能够调整-效率与程序的可读性和可维护性取舍的策略
25.1 性能概述
质量特性和性能
相对于代码质量,用户更关心的是程序的外在特性。对用户来说,程序员按时交付软件,提供一个清爽的用户界面,避免系统死机常常比程序的性能更为重要。
性能和代码调整
程序需求
在花费时间处理一个性能问题之前,请想清楚你的确是在解决一个确实需要解决的问题。
程序的设计
在架构上考虑程序的的速度与资源占用目标
类和子程序的设计
程序同操作系统的交互
代码编译
优秀的编译器能将清晰的高级语言代码转换为经过优化后的机器码。如果选 择了合适的编译器,你可能无须再考虑如何进一步优化程序的运行速度,直接就 能获得满意的程序性能了。往往容易被人忽视
硬件
代码调整
以上6个层次的调整都会直接影响程序的性能而且是累乘关系
出现性能问题往往第一想到的是代码调整可以多考虑一下其他层次
有一次切SDK爆出一堆性能问题结果只是打SDK没关优化
个人觉得性能分析考虑用接口测试覆盖更合适从本节可以看出影响程序性能的因素非常多
通过接口测试可以屏蔽部分干扰让性能测试的结果更准确更好地帮助开发者发现和修改问题25.2 代码调整简介
不是改进性能的最为有效的方法不是最方便的改善性能的方法不是成本最低的方法但是可以给开发者带来成就感 法则
小部分子程序占用了程序大部分的执行时间当某个程序绝大多数的代码都是由诸如 Python这样的解释型语言编写时,程序员同样应该把其中最关键的部分用C这样的编译型语言重写程序员们首先应该实现程序应该具备的所有功能,然后再使程序臻于完美在高级语言中,减少代码的行数与运行速度没有直接关系程序员应当使用高质量的设计,把程序编写正确。使之模块化并易于修改,让后期的维护工作变得很容易。在程序经完成并正确之后,再去检查系统的性能。如果程序运行迟钝,那么再设法让它更快更小除非你对需要完成的工作每一种编译器都拥有和别的编译器所不同的优势和弱点与那些充满技巧的代码相比,编译器的优化功能对那些平铺直叙的代码更见效
25.3 蜜糖和哥斯拉
这一节介绍加快程序运行速度和优化体积的方法
输入/输出操作 访问内存比访问文件快上1000倍引发操作系统交换内存页面的运算会比在内存同一页中进行的运算慢许多调用系统子程序的代价常常是十分可观的解释型语言作者给的参考数据Python速度比C慢100倍程序性能的终极麻烦就是代码中的错误没有去掉调试代码、忘了释放内存、数据库表设计失误、轮询并不存在的设备直至超时
25.4 性能测量
经验对性能优化也没有太大的帮助需要通过测量来证明有的时候看起来低效的代码经过编译器优化可以一样高效且更易懂
25.5 反复调整
大部分优化方法单独看起来都收效甚微,但累计起来,效果是惊人的
25.6 代码调整方法总结
用设计良好的代码来开发软件,从而使程序易于理解和修改。如果程序性能很差。 a. 保存代码的可运行版本,这样你才能回到“最近的已知正常状态”; b. 对系统进行分析测量,找出热点; c. 判断性能拙劣是否源于设计、数据类型或算法上的缺陷,确定是否应该 做代码调整,如果不是,请跳回到第一步; d. 对步骤c中所确定的瓶颈代码进行调整; e. 每次调整后都对性能提升进行测量; f. 如果调整没有改进代码的性能,就恢复到步骤a保存的代码(通常而言,超过一半的调整尝试都只能稍微改善性能甚至造成性能恶化)重复步骤2
读后感通过这一章的阅读让我意识到了平时非常无感的编译器优化的作用与重要性编译器优化能够带来40%以上的效率提升能够帮助我们实现代码可读性与性能的双丰收同时作者给出了几个有意思的结论1. 经验对性能优化也没有太大的帮助2. 代码调整既不是最为有效、也不是最方便还不是成本最低的提高效率的方法只是开发者比较青睐3. 极少部分代码运行占用了程序大部分时间找出并优化这一下部分往往能够解决实际的问题4. 在不是对效率要求特别敏感的时候先写好程序再考虑运行效率即可第二十六章 代码调整技术
以牺牲程序内部结构的某些特性来换取更高的性能
26.1逻辑
在知道答案后停止判断
多个条件判断的时候其中一个条件已经能够决定最重的个结果不必计算后续条件循环或者子程序已经得到了想要的结果的时候无需继续运行后续计算
按照出现频率来调整判断顺序
安排判断的顺序,让运行最快和判断结果最有可能为真的判断首先被执行
相似逻辑结构之间的性能比较
通过测量结果选择更高效的方法
用查询表替代复杂表达式
使用惰性求值
26.2循环
将判断外提
合并多个循环
展开
尽可能减少在循环内部做的工作
哨兵值
找到需要的结果后直接退出循环
把最忙的循环放在最内层
削减强度
削减强度意味着用多次轻量级运算来代替一次代价高昂的运算
26.3数据变换
使用整型数而不是浮点数
数组维度尽可能少
尽可能减少数组引用
如果需要多次访问同一个值用临时变量接一下
使用辅助索引
使用缓存机制
这种方式用空间换时间而且会增加缓存内容维护的工作过量恰当地使用可以避免重复的读写和计算从而给程序提升效率
26.4表达式
利用代数恒等式
如sqrt(x) sqrt(y) 可以用更高效的 x y 代替
削弱运算强度
编译期初始化
编码时计算结果并存到常量中
小心系统函数
26.5子程序
将子程序重写为内联
26.6用低级语言重写代码
略
26.7变得越多,事情反而越没变
代码调整无可避免地为性能改善的良好愿望而付出复杂性、可读性、简单性、 可维护性方面的代价。由于每一次调整后需要对性能进行重新评估,代码调整还 引入了巨额的管理维护开销。
本章介绍的做法与前面章节的标准相反甚至给出了一些作者不推荐的结果。
性能优化有的时候会牺牲代码的可读性破坏一些封装
不应该在在不必要和没有证明确确实有效之前使用
事实上至今为止我还没有接触到需要牺牲可读性来换性能的工作
遇到的性能问题多数出在不良的设计。
作者再次强调测量效率比经验更为重要