-
要养成一个习惯,经常花时间阅读别人编写的高品质代码。
-
要有选择地阅读代码,同时,还要有自己的目标。您是想学习新的模式、编码风格、还是满足某些需求的方法?
-
要注意并重视代码中特殊的非功能性需求,这些需求也许会导致特定的实现风格。
-
在现有的代码上工作时,请与作者或维护人员进行必须的协调,以避免重复劳动或因此而产生厌恶情绪。
-
请将从开放源码软件中得到的益处看作是一项贷款,尽可能地寻找各种方式来回报开放源码社团。
-
多数情况下,如果您想要了解“别人会如何完成这个功能呢?”,除了阅读代码以外,没有更好的方法。
-
在寻找 BUG时,请从问题的表现形式到问题的根源来分析代码。不要沿着不相关的路径(误入岐途)
-
我们要充分利用调度器,编译器给出的警告或输出的符号代码,系统调用跟踪器,数据库结构化查询语言的日志机制、包转储工具和Windows的消息侦查程序,定出BUG的位置。
-
对于那些大型且组织良好的系统,您只需要最低限度地了解它的全部功能,就能够对它做出修改。
-
当向系统中增加新功能时,首先的任务就是找到实现类似特性的代码,将它作为待实现功能的模板。
-
从特性的功能描述到代码的实现,可以按照字符串消息,或使用关键词来搜索代码。
-
在移植代码或修改接口时, 您可以通过编译器直接定位出问题涉及的范围,从而减少代码阅读的工作量。
-
进行重构时,您从一个能够正常工作的系统开始做起,希望确保结束时系统能够正常工作。一套恰当的测试用例可以帮助您满足此项约束。
-
阅读代码寻找重构机会时,先从系统的构架开始,然后逐步细化,能够获得最大的效益。
-
代码的可重用性是一个诱人的,但难以掌握的思想;降低期望就不会感到失望。
-
如果您希望重要的代码十分棘手,难以理解与分离,可以试着寻找粒度更大一些的包,甚至其他代码。
-
在复查软件系统时,要注意,系统是由很多部分组成的,不仅仅只是执行语句。还要注意分析以下内容:文件和目录结构、生成和配置过程、用户界面和系统的文档。
-
可以将软件复查作为一个学习、讲授、援之以手和接受帮助的机会。
-
第一次分析一个程序时,main是一个好的起始点。
-
层叠if-else if-... -else 序列可以看作是由互斥选择项组成的选择结构。
-
有时,要想了解程序在某一方面的功能,运行它可能比阅读源代码更为恰当。
-
在分析重要的程序时,最好首先识别出重要的组成部分。
-
了解局部的命名约定,利用它们来猜测变量和函数的功能用途。
-
当基于猜测修改代码时,您应该设计能够验证最初假设的过程。这个过程可能包括用编译器进行检查、引入断言、或者执行适当的测试用例。
-
理解了代码 的某一部分,可能帮助您理解余下的代码。
-
解决困难的代码要从容易的部分入手。
-
要养成遇到库元素就去阅读相关文档的习惯;这将会增强您阅读和编写代码的能力。
-
代码阅读有许多可选择的策略:自底向上和自顶向下的分析、应用试探法和检查注释和外部文档,应该依据问题的需要尝试所有这些方法。
-
for(i=0; i<n; i++) r 形式的循环执行n次;其他任何形式都要小心。
-
涉及两项不等测试(其中一项包括相等条件)的比较表达式可以看作是区间成员测试。
-
我们经常可以将表达式应用在样本数据上,借以了解它的含义。
-
使用De Morgan法则简化复杂的逻辑表达式。
-
在阅读逻辑乘表达式时,总是可以认为正在分析的表达式以左的表达式均为true;在阅读逻辑和表达式时,类似地,可以认为正在分析 的表达式以左的表达式均为false。
-
重新组织您控制的代码,使之更为易读。
-
将使用条件运算符?:的表达式理解为if代码。
-
不需要为了效率,牺牲代码的易读性。
-
高效的算法和特殊的优化确实有可能使得代码更为复杂,从而更难理解,但这并不意味着使代码更为紧凑和不易读会提高它的效率。
-
创造性的代码而已可以用来提高代码的易读性。
-
我们可以使用空格、临时变量和括号提高表达式的易读性。
-
在阅读您所控制的代码时,要养成添加注释的习惯。
-
我们可以用好的缩进以及对变量名称的明智选择,提高编写欠佳的程序的易读性。
-
用diff程序分析程序的修订历史时,如果这段历史跨越了整体重新缩排,常常可以通过指定-w选项,让diff忽略空白差异,避免由于更改了缩进层次而引入的噪音。
-
do循环的循环体至少执行一次。
-
执行算术运算时,当b=2^n-1时,可以将a&b理解为a%(b+1)。
-
将a<<n理解为a * k , k = 2 ^ n。
-
每次只分析一个控制结构,将它的内容看作是一个黑盒。
-
将每个控制结构的控制表达式看作是它所含代码的断言。
-
return, goto, break 和continue语句,还有异常,都会影响结构化的执行流程。由于这些语句一般都会终止或重新开始正在进行的循环,因此要单独推理它们的行为。
-
用复杂循环的变式和不变式,对循环进行推理。
-
使用保持含义不变的变换重新安排代码,简化代码的推理工作。
-
了解特定语言构造所有服务的功能之后,应能够更好地理解使用它们的代码。
-
识别并归类使用指针的理由。
-
在C程序中,指针一般用来构造链式数据结构、动态分配的数据结构、实现引用调用、访问和迭代数据元素、传递数组参数、引用函数、作为其他值的别名、代表字符串、以及直接访问系统内存。
-
以引用传递的参数可以用来返回函数的结果,或者避免参数复制带来的开销。
-
指向数组元素地址的指针,可以访问位于特定索引位置的元素。
-
指向数组元素的指针和相应的数组索引,作用在二者上的元素具有相同的语义。
-
使用全局或static局部变量的函数大多数情况都不可重入(reentrant)。
-
字符指针不同于字符数组。
-
识别和归类应用结构或共用体的每种理由。
-
C语言中的结构将多个数据元素集合在一起,使得它们可以作为一个整体来使用。用来从函数中返回多个数据元素,构造链式数据结构、映射数据在硬件设备、网络链接和存储介质上的组织方式、实现抽象数据类型,以及以面向对象的方式编程。
-
共用体在C语言中主要用于优化存储空间的利用,实现多态、以及访问数据不同的内部表达方式。
-
一个指针,在初始化为指向N个元素的存储空间之后,就可以作为N个元素的数组来使用。
-
动态分配的内存块可以显式地释放,或在程序结束时释放,或由垃圾回收器来完成回收;在栈上分配的内存块当分配它的函数退出后释放。
-
C程序使用typedef声明促进抽象,并增强代码的易读性,从而防范可移植性问题。并模拟C++和Java的类声明行为。
-
可以将typedef声明理解成变量定义:变量的名称就是类型的名称;变量的类型就是与该名称对应的类型。
-
根据底层的抽象数据类型理解的数据结构操作。
-
C语言中,一般使用内建的数组类型实现向量,不再对底层实现进行抽象。
-
N个元素的数组可以被序列for(i=0; i<N; i++)完全处理;所有其他变体都应该引起警惕。
-
表达式sizeof(x)总会得到用memset或memcpy处理数组x(不是指针)所需的正确字节数。
-
区间一般用区间内的第一个元素和区间后的第一个元素来表示。
-
不对称区间的高位边界等于低位边界代表区间的第一个元素:高位边界代表区间外的第一个元素。
-
结构的数组常常表示由记录和字段组成的表。
-
指向结构的指针常常表示访问底层记录和字段的游标。
-
动态分配的矩阵一般存储为指向数组列的指针或指向元素指针的指针:这两种类型者可以执照二维数据进行访问。
-
以平面数组形式存储的动态分配矩阵,用自定义访问函数定们它们的元素。
-
抽象数据类型为底层实现元素的使用(或误用)提供了一种信心的量度。
-
数组用从0开始的顺序整数为键,组织查找表。
-
数组经常用来对控制结构进行高效编码,简化程序的逻辑。
-
通过在数组中每个位置存储一个数据元素和一个函数指针(指向处理数据元素的函数),可以将代码与数据关联起来。
-
数组可以通过存储供程序内的抽象机(abstract machine)或虚拟机(virtual machine)使用的数据或代码,控制程序的动作。
-
可以将表达式sizeof(x)/sizeof(x[0])理解为数组x中元素的个数。
-
如果结构中含有指向结构自身,名为next的元素,一般说来,该结构定义的是单向链表的结点。
-
指向链表结点的持久性(如全局、静态或在堆上分配)指针常常表示链表的头部。
-
包含指向自身的next和prev指针的结构可以是双向链表的结点。
-
理解复杂数据结构的指针操作可能将数据元素画为方框、指针画为箭头。
-
递归数据结构经常用递归算法来处理。
-
重要的数据结构操作算法一般用参数参数或模板参数来参数化。
-
图的结点常常顺序地存储在数组中,链接到链表中,或通过图的边链接起来。
-
图中的边一般不是隐式地通过指针,就是显式地作为独立的结构来表示。
-
图的边经常存储为动态分配的数组或链表,在这两种情况下,边都锚定在图在的结点上。
-
在无向图中,表达数据时应该将所有的结点看作是等同的。类似地,进行处理任务的代码也不应该基于它们的方向来区分连。
-
在非连通图中,执行代码应该能够接通孤立的子图。
-
处理包含回路的图时,代码应该避免在处理图的回路时进入循环。
-
复杂的图结构中,可能隐藏着其他类型的独立结构。
-
采用递归定义的算法和数据结构经常用递归的函数定义来实现。
-
推理递归函数时,要从基准范例测试开始,并论证每次递归调用如何逐渐接近非递归基准范例代码
-
简单的语言常常使用一系列遵循该语言语法结构的函数进行语法分析。
-
推理归调用等同于一个回到函数开始处的循环。
-
将throws子句从方法的定义中移除,然后运行Java编译器对类的源代码进行编译,就容易地找到那些可能隐式地生成异常的方法。
-
在多处理器计算机上运行的代码常常围绕进程或线程进行组织。
-
工作群并行模型用于在多个处理器间分配工作,或者创建一个任务池,然后将大量需要处理的标准化的工作进行分配。
-
基于线程的管理者/工人并行模型一般将耗时的或阻塞的操作分配给工人子任务,从而维护中心任务的响应性。
-
基于进程的管理者/工人并行模型一般用来重用现有的程序,或用定义良好的接口组织和分离粗粒度的系统模块。
-
基于流水线的并行处理中,每个任务都接收到一些输入,对它们进行一些处理,并将生成的输出传递给下一个任务,进行不同的处理。
-
竞争条件很难捉摸,相关的代码常常会将竞争条件扩散到多个函数或模块;因而,很难隔离由于竞争条件导致的问题。
-
对于出现在信号处理品中的数据结构操作代码和库调用要保持高度警惕。
-
在阅读包含宏的代码时,要注意,宏既非函数,也非语句。
-
do ... while(0)块中的宏等同于控制块中的语句。
-
宏可以访问在它的使用点可见的所有局部变量。
-
宏调用可以改变参数的值。
-
基于宏的标记拼接能够创建新的标记符。
-
我们可以通过浏览项目的源代码树--包含项目源代码的层次目录结构,来分析一个项目的组织方式。源码树常常能够反映出项目在构架和软件过程上的结构。
-
应用程序的源代码树经常是该应用程序的部署结构的镜像。
-
不要被庞大的源代码集合吓倒,它们一般比小型的专门项目组织得更出色。
-
当您首次接触一个大型项目时,要花一些时间来了解项目的目录树结构。
-
项目的源代码远不只是编译后可以获得可执行程序的计算机语言指令;一个项目的源码树一般还包括规格说明、最终用户和开发人员文档、测试脚本、多媒体资源、编译工具、例子、本地化文件、修订历史、安装过程和许可信息。
-
大型项目的编译过程一般声明性地借助依赖关系来说明。依赖关系由工具程序,如make及其派生程序,转换成具体的编译行动。
-
大型项目中,制作文件常常由配置步骤动态地生成;在分析制作文件之前,需要先执行项目特定的配置。
-
检查大型编译过程各个步骤时,可以使用make程序的-n形状进行预演。
-
修订控制系统提供从储存库中获取源代码最新版本的方式。
-
可以使用相关的命令,显示可执行文件中的修订标识关键字,从而将可执行文件与它的源代码匹配起来。
-
修订日志中出现的bug跟踪系统内的编号,可以在bug跟踪系统的数据库中找到有关问题的说明。
-
可以使用修订控制系统的版本储存库,找出特定的变更是如何实现的。
-
定制编译工具用在软件开始过程的许多方面,包括配置、编译过程管理、代码的生成、测试和文档编制。
-
程序的调度输出可以帮助我们理解程序控制流程和数据元素的关键部分。
-
跟踪语句所在的地点一般也是算法运行的重要部分。
-
可以用断言来检验算法动作的步骤、函数接收的参数、程序的控制流程、底层硬件的忏悔和测试的结果。
-
可以使用对算法进行检验的断言来证实您对算法动作的理解,或将它作为推理的起来。
-
对函数参数和结果的断言经常记录了函数的前置条件和后置条件。
-
我们可以将测试整个函数的断言作为为每个给定函数的规格说明。
-
测试可以部分地代替函数规格说明。
-
可以使用测试的输入数据对源代码序列进行预演。
-
阅读代码时,应该尽可能地得到任何能够得到的文档。
-
阅读一小时代码所得到的信息只不过相当于阅读一分钟文档。
-
使用系统的规格说明文档,了解所阅读代码的运行环境。
-
软件需求规格说明是阅读和评估代码的基准。
-
可以将系统的设计规格说明作为认知代码结构的路线图,阅读具体代码的指引。
-
测试规格说明文档为我们提供可以用来对代码进行预演的数据。
-
在接触一个未知系统时,功能性的描述和用户指南可以提供重要的背景信息,从而更好地理解阅读的代码所处的上下文。
-
从用户参考手册中,我们可以快速地获取,应用程序在外观与逻辑上的背景知识,从管理员手册中可以得知代码的接口、文件格式和错误消息的详细信息。
-
得到文档可以快捷地获取系统的概况,了解提供特定特性的代码。
-
文档经常能够反映和提示出系统的底层结构。
-
文档有助于理解复杂的算法和数据结构。
-
算法的文字描述能够使不透明(晦涩,难以理解)的代码变得可以理解。
-
文档常常能够阐明源代码中标识符的含义。
-
文档能够提供非功能性需求背后的理论基础。
-
文档还会说明内部编程接口。
-
由于文档很少像实际的程序代码那样进行测试,并受人关注,所以它常常可能存在错误、不完整或过时。
-
文档也提供测试用例,以及实际应用的例子。
-
文档常常还会包括书籍的实现问题或bug。
-
环境中已知的缺点一般都会记录在源代码中。
-
文档的变更能够标出那些故障点。
-
对同一段源代码重复或互相冲突的理性,常常表示存在根本性的设计缺陷,从而使得维护人员需要用一系列的修补程序来修复。
-
相似的修复应用到源代码的不同部分,常常表示一种易犯的错误或疏忽,它们同样可能会在其他地方存在。
-
文档常常会提供不恰当的信息,误导我们对源代码的理解。
-
要警惕那些未归档的特性:将每个实例归类为合理、疏忽或有害,相应地决定是否修复代码或文档。
-
有时,文档在描述系统时,并非按照已完成的实现,而是按照系统应该的样子或将来的实现。
-
在源代码文档中,单词gork的意思一般是指“理解”。
-
如果未知的或特殊用法的单词阻碍了对代码的理解,可以试着在文档的术语表(如果丰硕的话)、New Hacker's Dictionary[Ray96]、或在Web搜索引擎中查找它们。
-
总是要以批判的态度来看待文档,注意非传统的来源,比如注释、标准、出版物、测试用例、列表、新闻组、修订日志、问题跟踪数据库、营销和源代码本身。
-
总是要以批判的态度来看待文档;由于文档永远不会执行,对文档的测试和正式复查也很少达到对代码的同样水平,所以文档常常会误导读者,或者完全错误。
-
对于那些有缺陷的代码,我们可以从中推断出它的真实意图。
-
在阅读大型系统的文档时,首先要熟悉文档的总体结构和约定。
-
在对付体积庞大的文档时,可以使用工具,或将文本输出到高品质输出设备上,比如激光打印机,来提高阅读的效率。
-
一个系统可以(在重大的系统中也确定如此)同时展示出多种不同的构架类型。以不同的方式检查同一系统、分析系统的不同部分、或使用不同级别的分解,都有可能发现不同的构架类型。
-
协同式的应用程序,或者需要协同访问共享信息或资源的半自治进程,一般会采用集中式储存库构架。
-
黑板系统使用集中式的储存库,存储非结构化的键/值对,作为大量不同代码元件之间的通信集线器。
-
当处理过程可以建模、设计和实现成一系列的数据变换时,常常会使用数据流(或管道--过滤器)构架。
-
在批量进行自动数据处理的环境中,经常会采用数据流构架,在对数据变换工具提供大量支持的平台上尤其如此。
-
数据流构架的一个明显征兆是:程序中使用临时文件或流水线(pipeline)在不同进程间进行通信。
-
使用图示来建模面向对象构架中类的关系。
-
可以将源代码输入到建模工具中,逆向推导出系统的构架。
-
拥有大量同级子系统的系统,常常按照分层构架进程组织。
-
分层构架一般通过拥有标准化接口的软件组件来实现。
-
系统中每个层可以将下面的层看作抽象实体,并且(只要该层满足它的需求说明)不关心上面的层如何使用它。
-
层的接口既然可以是支持特定概念的互补函数族,也可以是一系列支持同一抽象接口不同底层实现的可一互换函数。
-
用C语言实现的系统,常常用函数指针的数组,表达层接口的多路复用操作。
-
用面向对象的语言实现的系统,使用虚方法调用直接表达对层接口的多路复用操作。
-
系统可以使用不同的、独特的层次分解模型跨各种坐标轴进行组织。
-
使用程序切片技术,可以将程序中数据和控制之间依赖关系集中到一起。
-
在并发系统中,一个单独的系统组件起到集中式管理器的作用,负责启动、停止和协调其他系统进程和任务的执行。
-
现实的系统都会博采众家之长。当处理此类系统时,不要徒劳地寻找无所不包的构架图;应该将不同构架风格作为独立但相关的实体来进行定位、并了解。
-
状态变迁图常常有助于理清状态机的动作。
-
在处理大量的代码时,了解将代码分解成单独单元的机制极为重要。
-
大多数情况下,模块的物理边界是单个文件、组织到一个目录中的多个文件或拥有统一前缀的文件的集合。
-
C中的模块,由提供模块公开接口的头文件和提供对应实现的源文件组成。
-
对象的构造函数经常用来分配与对象相关的资源,并初始化对象的状态。函数一般用来释放对象在生命期中占用的资源。
-
对象方法经常使用类字段来存储控制所有方法运行的数据(比如查找表或字典)或维护类动作的状态信息(例如,赋给每个惟一标识符的计数器)。
-
在设计良好的类中,所有的字段都应声明为private,并用公开的访问方法提供对它们的访问。
-
在遇到friend方法时,要停下来分析一下,看看绕过类封装在设计上的理由。
-
可以有节制地用运算符增强特定类的可用性,但用运算符重载,将类实现为拥有内建算术类型相关的全部功能的类数字实体,是不恰当的。
-
泛型实现不是在编译期间通过宏替换或语言所支持的功能(比如C++模板和Ada的泛性包)来实现,就是在运行期间通过使用数据元素的指针和函数的指针、或对象的多态性来实现。
-
抽象数据类型经常用来封装常用的数据组织方案(比如树、列表或桟),或者对用户隐藏数据类型的实现细节。
-
使用库的目标多种多样:重用源代码或目标代码,组织模块集合,组织和优化编译过程,或是用来实现应用程序各种特性的按需载入。
-
大型的、分布式的系统经常实现为许多互相协作的进程。
-
对于基于文本的数据储存库,可以通过浏览存储在其中的数据,破译出它的结构。
-
可以通过查询数据字典中的表,或使用数据库专用的SQL命令,比如show table,来分析关系型数据库的模式。
-
识别出重要的构架元素后,可以查找其最初的描述,了解正确地使用这种构架的方式,以及可能出现的误用。
-
要详细分析建立在某种框架之上的应用程序,行动的最佳路线就是从研究框架自身开始。
-
在阅读向导生成的代码时,不要期望太高,否则您会感到失望。
-
学习几个基本的设计模式之后,您会发现,您查看代码构架的方式会发生改变:您的视野和词汇将会扩展到能够识别和描述许多通用的形式。
-
频繁使用的一些模式,但并不显式地指出它们的名称,这是由于构架性设计的重用经常先于模式的形成。
-
请试着按照底层模式来理解构架,即便代码中并没有明确地提及模式。
-
大多数解释器都遵循类似的处理构架,围绕一个状态机进行构建,状态机的操作依赖于解释器的当前状态、程序指令和程序状态。
-
多数情况下,参考构架只是为应用程序域指定一种概念性的结构,具体的实现并非必须遵照这种结构。
-
词汇工具可以高效地在一个代码文件中或者跨多个文件查找某种模式。
-
使用程序编辑器和与此同时表达式命令,浏览庞大的源代码文件。
-
以只读方式浏览源代码文件
-
使用正则表达式 ^function name可以找出函数的定义。
-
使用正则表达式的字符类,可以查找名称遵循特定模式的变量。
-
使用正则表达式的否定字符类,可以避免非积极匹配。
-
使用正则表达式symbol-1.*symbol-2,可以查找出现在同一行的符号。
-
使用编译器的tags功能,可以快速地找出实体的定义。
-
可能用特定的tag创建工具,增加编辑器的浏览功能。
-
使用编辑器的大纲视图,可以获得源代码结构的鸟瞰图。
-
使用您的编辑器来检测源代码中圆括号、方括号和花括号的匹配。
-
使用grep定位符号的声明、定义和应用。
-
当您不能精确地要查找的内容时,请使用关键单词的词干对程序的源代码进行查找。
-
用grep过滤其他工具生成的输出,分离出您要查找的项。
-
将grep的输出输送到其他工具,使复杂处理任务自动化。
-
通过对grep的输出进行流编辑,重用代码查找的结果。
-
通过选取与噪音模式不匹配的输出行(grep -v),过滤虚假的grep输出。
-
使用fgrep在源代码中查找字符串列表。
-
查找注释,或标识符大小写不敏感的语言编写的代码时,要使用大小写不敏感的模式匹配(grep -i)。
-
使用grep -n命令行开关,可以创建与给定正则表达式匹配的文件和等号的检查表。
-
可以使用diff比较文件或程序不同版本之间的差别。
-
在运行diff命令时,可以使用diff -b,使文件比较算法忽略结尾的空格,用-w忽略所有空白区域的差异,用-i使文件比较对大小写不敏感。
-
不要对创建自己的代码阅读工具心存畏惧。
-
在构建自己的代码阅读工具时:要充分利用现代原型语言所提供的能力;从简单开始,根据需要逐渐改进;使用利用代码词汇结构的各种试探法;要允许一些输出噪音或寂静(无关输出或缺失输出);使用其他工具对输入进行预处理,或者对输出进行后期处理。
-
要使编译器成为您的朋友;指定恰当级别的编译器警告,并小心地评估生成的结果。
-
使用C预处理器理清那些滥用预处理器特性的程序。
-
要彻底地了解编译器如何处理特定的代码块,需要查看生成的符号(汇编)代码。
-
通过分析相应目标文件中的符号,可以清晰地源文件的输入和输出。
-
使用源代码浏览器浏览大型的代码集合以及对象类型。
-
要抵制信按照您的编码规范对外部代码进行美化的诱惑;不必要的编排更改会创建不同的代码,并妨碍维护工作的组织。
-
优美打印程序和编辑器语法着色可以使得程序的源代码更为易读。
-
cdecl程序可以将难以理解的C和C++类型声明转换成纯英语(反之亦然)。
-
实际运行程序,往往可以更深刻地理解程序的动作。
-
系统调用、事件和数据包跟踪程序可以增进对程序运作的理解。
-
执行剖析器可以找出需要着重优化的代码,验证输入数据的覆盖性,以及分析算法的运作。
-
通过检查从未执行的代码行,可以找出测试覆盖的弱点,并据此修正测试数据。
-
要探究程序动态运行时的每个细节,需要在调度器中运行它。
-
将您觉得难以理解的代码打印到纸上。
-
可以试着向别人介绍您在阅读的代码,这样做一般会增进您对代码的理解。
-
理解复杂的算法或巧妙的数据结构,要选择一个安静的环境,然后聚精会神地考虑,不要借助于任何算机化或自动化的帮助。