为什么要阅读源代码?如何有效的阅读源代码? 选一些比较优秀的开源产品作为源代码阅读对象?

来源:互联网 发布:做微信淘客用什么软件 编辑:程序博客网 时间:2024/05/16 17:09

转载自:http://blog.csdn.net/ajian005/article/details/7921507

注:删减了原作者关于Linux方面的内容,如有需要,请点击上面的链接!!!


一、为什么要阅读源代码?

很多作家成名之前都阅读过大量的优秀文学作品,经过长期的阅读和写作积累,慢慢的才有可能写出一些好的、甚至是优秀的文学作品。 而程序员与此类似,很多程序员也需要阅读大量的优秀程序或产品,经过不断阅读和实践积累,然后可能写出好的程序或产品。养成阅读高品质代码的习惯 可以提高编写代码的能力。

第一个好处是可以学习到很多编程的方法,看好的源代码,对于提高自己的编程水平,比自己写源代码的帮助更大。当然不是说不用自己写,而是说,自己写代码的同时,可以从别人写的好的源代码中间学习到更多的编程方法和技巧。

第二个好处是,可以提高自己把握大规模源代码的能力。一个比较大型的程序,往往都是经过了很多个版本很长的时间,有很多人参与开发,修正错误,添加功能而发展起来的。所以往往源代码的规模都比较大,少则10-100多k, 多的有好几十个MB. 在阅读大量源代码的时候,能够提高自己对大的软件的把握能力,快速了解脉络,熟悉细节,不仅仅是编程技巧,还能在程序的架构,设计方面提高自己的能力。(这里说一句题外话,<<设计模式>>这本书相信很多人都看过,而且很多人对它推崇备至,奉为经典。现在也出了不少书,都是冠以”设计模式”这一名称。在书中就提到,设计模式并不是一本教材,不是教你如何去编程序,而是把平时编程中一些固定的模式记录下来,加以不断的测试和改进,分发给广大程序员的一些经验之谈。我在看这本书的时候,有一些地方一些设计方法往往让我有似曾相识的感觉,另外一些则是我以前就常常用到的。而这些经验的获得,一部分得益于自己的编码过程,另外一个很重要的来源就是阅读别人写的源代码。)

阅读源代码第三个好处,就是获得一些好的思想。比如,有很多人在开始一个软件项目之前都喜欢到sourceforge.net上去找一下,是否有人以前做过相同或者相似的软件,如果有,则拿下来读一读,可以使自己对这个软件项目有更多更深的认识。我以前曾经想找一本关于如何阅读源代码的书来看看,却没有找到。相反,倒是找到了不少分析源代码的书,比如Linux kernel, Apache source, 等等。

第四个好处是,修复、检查、改进代码、当程序出现bug时或半截接手别人的项目时,需要阅读源代码后更改bug或推进项目。带着目的阅读代码。

第五个好处是,阅读的源代码多了,发现了解开源软件的运行机理,提取可重用的材料加以利用。他山之石,可以攻玉,阅读源代码进而从现有的优秀代码、算法、设计、架构中汲取营养,提高自身的开发与设计能力。


二、如何有效的阅读源代码?

(1)摘自《代码阅读方法与实践》书籍的知识点总结

(原书目录从微观到宏观,个人习惯先宏观:文档、架构、代码阅读工具、应对大型项目、一个完整的例子……微观,实际阅读代码的流程)

第一章 导论

  1. 要养成一个习惯,经常花时间阅读别人编写的高品质代码。 (读好书、多读书)

  2. 要有选择地阅读代码,同时,还要有自己的目标。您是想学习新的模式、编码风格、还是满足某些需求的方法? (搞清楚为什么要阅读代码? 学习架构、学习业务、学习模式、学习编码风格、学习类库… … 制定目标,对自己要有要求)

  3. 要注意并重视代码中特殊的非功能性需求,这些需求也许会导致特定的实现风格。(如 可维护性、可伸缩性、性能、)

  4. 在现有的代码上工作时,请与作者或维护人员进行必须的协调,以避免重复劳动或因此而产生厌恶情绪。

  5. 请将从开放源码软件中得到的益处看作是一项贷款,尽可能地寻找各种方式来回报开放源码社团。(比如、报告bug,提出问题,解决bug,添加功能贡献代码及文档… … 人人为我,我为人人)

  6. 多数情况下,如果您想要了解“别人会如何完成这个功能呢?”,除了阅读代码以外,没有更好的方法。

  7. 在寻找 BUG时,请从问题的表现形式到问题的根源来分析代码。不要沿着不相关的路径(误入岐途) (先搞清楚问题,然后找与问题相关部分代码阅读,记住带有目的性的阅读)

  8. 我们要充分利用调度器,编译器给出的警告或输出的符号代码,系统调用跟踪器,数据库结构化查询语言的日志机制、包转储工具和Windows的消息侦查程序,定出BUG的位置。(要充分利用线索。 侦探破案、医生诊断 都是利用线索或现象的)

  9. 对于那些大型且组织良好的系统,您只需要最低限度地了解它的全部功能,就能够对它做出修改。(一个系统可能有几十个模块,只需要了解与问题相关的模块,就有可能把问题解决,但首先必须模块设计合理)

  10. 当向系统中增加新功能时,首先的任务就是找到实现类似特性的代码,将它作为待实现功能的模板。(做一个新功能首先考虑到的是有没有类似的 模板或例子,有类似模板或例子可以提供借鉴,让你如何实现该功能有更加深入的认识)

  11. 从特性的功能描述到代码的实现,可以按照字符串消息,或使用关键词来搜索代码。

  12. 在移植代码或修改接口时, 您可以通过编译器直接定位出问题涉及的范围,从而减少代码阅读的工作量。

  13. 进行重构时,您从一个能够正常工作的系统开始做起,希望确保结束时系统能够正常工作。一套恰当的测试用例可以帮助您满足此项约束。(测试用例很关键)。

  14. 阅读代码寻找重构机会时,先从系统的构架开始,然后逐步细化,能够获得最大的效益。(由宏观到微观,由上到下一种方法)

  15. 代码的可重用性是一个诱人的,但难以掌握的思想;降低期望就不会感到失望。

  16. 如果您希望重要的代码十分棘手,难以理解与分离,可以试着寻找粒度更大一些的包,甚至其他代码。

  17. 在复查软件系统时,要注意,系统是由很多部分组成的,不仅仅只是执行语句。还要注意分析以下内容:文件和目录结构、生成和配置过程、用户界面和系统的文档。

  18. 可以将软件复查作为一个学习、讲授、援之以手和接受帮助的机会。

第二章 文档(原书:第八章:文档)

  1. 阅读代码时, 应该尽可能地利用任何能够得到的文档.

  2. 阅读一小时代码所得到的信息只不过相当于阅读一分钟文档.

  3. 使用系统的规格说明文档, 了解所阅读代码的运行环境.

  4. 软件需求规格说明是阅读和评估代码的基准.

  5. 可以将系统的设计规格说明作为认知代码结构的路线图, 阅读具体代码的指引.

  6. 测试规格说明文档为我们提供可以用来对代码进行预演的数据.

  7. 在接触一个未知系统时, 功能性的描述和用户指南可以提供重要的背景信息,从而更好地理解阅读的代码所处的上下文.

  8. 从用户参考手册中, 我们可以快速地获取, 应用程序在外观与逻辑上的背景知识, 从管理员手册中可以得知代码的接口|文件格式和错误消息的详细信息.

  9. 利用文档可以快捷地获取系统的概况, 了解提供特定特性的代码.

  10. 文档经常能够反映和提示出系统的底层结构.

  11. 文档有助于理解复杂的算法和数据结构.

  12. 算法的文字描述能够使不透明(晦涩, 难以理解)的代码变得可以理解.

  13. 文档常常能够阐明源代码中标识符的含义.

  14. 文档能够提供非功能性需求背后的理论基础.

  15. 文档还会说明内部编程接口.

  16. 由于文档很少像实际的程序代码那样进行测试, 并受人关注, 所以它常常可能存在错误|不完整或过时.

  17. 文档也提供测试用例, 以及实际应用的例子.

  18. 文档常常还会包括已知的实现问题或bug.

  19. 环境中已知的缺点一般都会记录在源代码中.

  20. 文档的变更能够标出那些故障点.

  21. 对同一段源代码重复或互相冲突的更改, 常常表示存在根本性的设计缺陷, 从而使得维护人员需要用一系列的修补程序来修复.

  22. 相似的修复应用到源代码的不同部分, 常常表示一种易犯的错误或疏忽, 它们同样可能会在其他地方存在.

  23. 文档常常会提供不恰当的信息, 误导我们对源代码的理解.

  24. 要警惕那些未归档的特性: 将每个实例归类为合理|疏忽或有害, 相应地决定是否应该修复代码或文档.

  25. 有时, 文档在描述系统时, 并非按照已完成的实现, 而是系统应该的样子或将来的实现.

  26. 在源代码文档中, 单词gork的意思一般是指”理解”.

  27. 如果未知的或特殊用法的单词阻碍了对代码的理解, 可以试着在文档的术语表(如果存在的话)|New Hacker’s Dictionary[Ray96]|或在Web搜索引擎中查找它们.

  28. 总是要以批判的态度来看待文档, 注意非传统的来源, 比如注释|标准|出版物|测试用例|邮件列表|新闻组|修订日志|问题跟踪数据库|营销材料|源代码本身.

  29. 总是要以批判的态度来看待文档; 由于文档永远不会执行, 对文档的测试和正式复查也很少达到对代码的同样水平, 所以文档常常会误导读者, 或者完全错误.

  30. 对于那些有缺陷的代码, 我们可以从中推断出它的真实意图.

  31. 在阅读大型系统的文档时, 首先要熟悉文档的总体结构和约定.

  32. 在对付体积庞大的文档时, 可以使用工具, 或将文本输出到高品质输出设备上, 比如激光打印机, 来提高阅读的效率.

第三章 系统构架(原书:第九章: 系统构架)、设计模式、编码惯例

  1. 一个系统可以(在重大的系统中也确实如此)同时出多种不同的构架类型. 以不同的方式检查同一系统|分析系统的不同部分|或使用不同级别的分解, 都有可能发现不同的构架类型.

  2. 协同式的应用程序, 或者需要协同访问共享信息或资源的半自治进程, 一般会采用集中式储存库构架.

  3. 黑板系统使用集中式的储存库, 存储非结构化的键/值对, 作为大量不同代码元件之间的通信集线器.

  4. 当处理过程可以建模|设计和实现成一系列的数据变换时, 常常会使用数据流(或管道—过滤器)构架.

  5. 在批量进行自动数据处理的环境中, 经常会采用数据流构架, 在对数据工具提供大量支持的平台上尤其如此.

  6. 数据流构架的一个明显征兆是: 程序中使用临时文件或流水线(pipeline)在不同进程间进行通信.

  7. 使用图示来建模面向对象构架中类的关系.

  8. 可以将源代码输入到建模工具中, 逆向推导出系统的构架.

  9. 拥有大量同级子系统的系统, 常常按照分层构架进行组织.

  10. 分层构架一般通过堆叠拥有标准化接口的软件组件来实现.

  11. 系统中每个层可以将下面的层看作抽象实体, 并且(只要该层满足它的需求说明)不关心上面的层如何使用它.

  12. 层的接口既可以是支持特定概念的互补函数族, 也可以是一系列支持同一抽象接口不同底层实现的可互换函数.

  13. 用C语言实现的系统, 常常用函数指针的数组, 表达层接口的多路复用操作.

  14. 用面向对象的语言实现的系统, 使用虚方法调用直接表达对层接口的多路复用操作.

  15. 系统可以使用不同的|独特的层次分解模型跨各种坐标轴进行组织.

  16. 使用程序切片技术, 可以将程序中的数据和控制之间依赖关系集中到一起.

  17. 在并发系统中, 一个单独的系统组件起到集中式管理器的作用, 负责启动|停止和协调其他系统进程和任务的执行.

  18. 许多现实的系统都会博采众家之长. 当处理此类系统时, 不要徒劳地寻找无所不包的构架图; 应该将不同构架风格作为独立但相关的实体来进行定位|识别并了解.

  19. 状态变迁图常常有助于理清状态机的动作.

  20. 在处理大量的代码时, 了解将代码分解成单独单元的机制极为重要.

  21. 大多数情况下, 模块的物理边界是单个文件|组织到一个目录中的多个文件或拥有统一前缀的文件的集合.

  22. C中的模块, 由提供模块公开接口的头文件和提供对应实现的源文件组成.

  23. 对象的构造函数经常用来分配与对象相关的资源, 并初始化对象的状态. 函数一般用来释放对象在生命期中占用的资源.

  24. 对象方法经常使用类字段来存储控制所有方法运作的数据(比如查找表或字典)或维护类运作的状态信息(例如, 赋给每个对象一个标识符的计数器).

  25. 在设计良好的类中, 所有的字段都应在声明为private, 并用公开的访问方法提供对它们的访问.

  26. 在遇到friend声明时, 要停下来分析一下, 看看绕过类封装在设计上的理由.

  27. 可以有节制地用运算符增强特定类的可用性, 但用运算符重载, 将类实现为拥有内建算术类型相关的全部功能的类实体, 是不恰当的.

  28. 泛型实现不是在编译期间通过宏替换或语言所支持的功能(比如C++模板和Ada的泛型包)来实现, 就是在运行期间通过使用数据元素的指针和函数的指针|或对象的多态性实现.

  29. 抽象数据类型经常用来封装常用的数据组织方案(比如树|列表或栈), 或者对用户隐藏数据类型的实现细节.

  30. 使用库的目的多种多样: 重用源代码或目标代码, 组织模块集合, 组织和优化编译过程, 或是用来实现应用程序各种特性的按需载入.

  31. 大型的|分布式的系统经常实现为许多互相协作的进程.

  32. 对于基于文本的数据储存库, 可以通过浏览存储在其中的数据, 破译出它的结构.

  33. 可以通过查询数据字典中的表, 或使用数据库专有的SQL命令, 比如show table, 来分析关系型数据库的模式.

  34. 识别出重用的构架元素后, 可以查找其最初的描述, 了解正确地使用这种构架的方式, 以及可能出现的误用.

  35. 要详细分析建立在某种框架之上的应用程序, 行动的最佳路线就是从研究框架自身开始.

  36. 在阅读向导生成的代码时, 不要期望太高, 否则您会感到失望.

  37. 学习几个基本的设计模式之后, 您会发现, 您查看代码构架的方式会发生改变: 您的视野和词汇将会扩展到能够识别和描述许多通用的形式.

  38. 频繁使用的一些模式, 但并不显式地指出它们的名称, 这是由于构架性设计的重用经常先于模式的形成.

  39. 请试着按照底层模式来理解构架, 即使代码中并没有明确地提及模式.

  40. 大多数解释器都遵循类似的处理构架, 围绕一个状态机进行构建, 状态机的操作依赖于解释器的当前状态|程序指令和程序状态.

  41. 多数情况下, 参考构架只是为应用程序域指定一种概念性的结构, 具体的实现并非必须遵照这种结构.

第四章 代码阅读工具 (原书:第十章: 代码阅读工具)

  1. 词汇工具可以高效地在一个大代码文件中或者跨多个文件查找某种模式.

  2. 使用程序编辑器和正则表达式查找命令, 浏览庞大的源代码文件.

  3. 以只读方式浏览源代码文件.

  4. 使用正则表达式 ^function name 可以找出函数的定义.

  5. 使用正则表达式的字符类, 可以查找名称遵循特定模式的变量.

  6. 使用正则表达式的否定字符类, 可以避免非积极匹配.

  7. 使用正则表达式 symbol-1. *symbol-2, 可以查找出现在同一行的符号.

  8. 使用编辑器的 tags 功能, 可以快速地找出实体的定义.

  9. 可以用特定的 tag 创建工具, 增加编辑器的浏览功能.

  10. 使用编辑器的大纲视图, 可以获得源代码结构的鸟瞰图.

  11. 使用您的编辑器来检测源代码中圆括号|方括号和花括号的匹配.

  12. 使用 grep 跨多个文件查找代码模式.

  13. 使用 grep 定位符号的声明|定义和应用.

  14. 当您不能精确地表述要查找的内容时, 请使用关键单词的词干对程序的源代码进行查找.

  15. 用 grep 过滤其他工具生成的输出, 分离出您要查找的项.

  16. 将 grep 的输出输送到其他工具, 使复杂处理任务自动化.

  17. 通过对 grep 的输出进行流编辑, 重用代码查找的结果.

  18. 通过选取与噪音模式不匹配的输出行(grep-v), 过滤虚假的 grep 输出.

  19. 使用 fgrep 在源代码中查找字符串列表.

  20. 查找注释, 或标识符大小写不敏感的语言编写的代码时, 要使用大小写不敏感的模式匹配(grep -i).

  21. 使用 grep –n 命令行开关, 可以创建与给定正则表达式匹配的文件和行号的检查表.

  22. 可以使用 diff 比较文件或程序不同版本之间的差别.

  23. 在运行 diff 命令时, 可以使用 diff –b, 使文件比较算法忽略结尾的空格, 用 –w 忽略所有空白区域的差异, 用 –i 使文件比较对大小写不敏感.

  24. 不要对创建自己的代码阅读工具心存畏惧.

  25. 在构建自己的代码阅读工具时: 要充分利用现代快速原型语言所提供的能力; 从简单开始, 根据需要逐渐改进; 使用利用代码词汇结构的各种试探法; 要允许一些输出噪音或寂静(无关输出或缺失输出); 使用其他工具对输入进行预处理, 或者对输出进行后期处理.

  26. 要使编译器成为您的: 指定恰当级别的编译器警告, 并小心地评估生成的结果.

  27. 使用C预处理器理清那些滥用预处理器特性的程序.

  28. 要彻底地了解编译器如何处理特定的代码块, 需要查看生成的符号(汇编)代码.

  29. 通过分析相应目标文件中的符号, 可以清晰地了解源文件的输入和输出.

  30. 使用源代码浏览器浏览大型的代码集合以及对象类型.

  31. 要抵制住按照您的编码规范对外部代码进行美化的诱惑; 不必要的编排更改会创建不同的代码, 并妨碍工作的组织.

  32. 优美打印程序和编辑器语法着色可以使得程序的源代码为易读.

  33. cdecl 程序可以将难以理解的C和C++类型声明转换成纯英语(反之亦然).

  34. 实际运行程序, 往往可以更深刻地理解程序的动作.

  35. 系统调用|事件和数据包跟踪程序可以增进对程序动作的理解.

  36. 执行剖析器可以找出需要着重优化的代码, 验证输入数据的覆盖性, 以及分析算法的动作.

  37. 通过检查从未执行的代码行, 可以找出测试覆盖的弱点, 并据此修正测试数据.

  38. 要探究程序动态动作时的每个细节, 需要在调试器中运作它.

  39. 将您觉得难以理解的代码打印到纸上.

  40. 可以绘制图示来描绘代码的动作.

  41. 可以试着向别人介绍您在阅读的代码, 这样做一般会增进您对代码的理解.

  42. 理解复杂的算法或巧妙的数据结构, 要选择一个安静的环境, 然后聚精会神地考虑, 不要借助于任何计算机化或自动化的帮助.

第五章 应对大型项目 (第六章: 应对大型项目)

  1. 我们可以通过浏览项目的源代码树—包含项目源代码的层次目录结构, 来分析一个项目的组织方式. 源码树常常能够反映出项目在构架和软件过程上的结构.

  2. 应用程序的源代码树经常是该应用程序的部署结构的镜像.

  3. 不要被庞大的源代码集合吓倒; 它们一般比小型的专门项目组织得更出色.

  4. 当您首次接触一个大型项目时, 要花一些时间来熟悉项目的目录树结构.

  5. 项目的源代码远不只是编译后可以获得可执行程序的计算机语言指令; 一个项目的源码树一般还包括规格说明|最终用户和开发人员文档|测试脚本|多媒体资源|编译工具|例子|本地化文件|修订历史|安装过程和许可信息.

  6. 大型项目的编译过程一般声明性地借助依赖关系来说明. 依赖关系由工具程序, 如make/ant/Maven及其派生程序, 转换成具体的编译行动.

  7. 大型项目中, 制作文件常常由配置步骤动态地生成; 在分析制作文件之前, 需要先执行项目特定的配置.

  8. 检查大型编译过程的各个步骤时, 可以使用make程序的-n开关进行预演.

  9. 修订控制系统提供从储存库中获取源代码最新版本的方式.

  10. 可以使用相关的命令, 显示可执行文件中的修订标识关键字, 从而将可执行文件与它的源代码匹配起来.

  11. 使用修订日志中出现的bug跟踪系统内的编号, 可以在bug跟踪系统的数据库中找到有关的问题的说明.

  12. 可以使用修订控制系统的版本储存库, 找出特定的变更是如何实现的.

  13. 定制编译工具用在软件开发过程的许多方面, 包括配置|编译过程管理|代码的生成|测试和文档编制.

  14. 程序的调试输出可以帮助我们理解程序控制流程和数据元素的关键部分.

  15. 跟踪语句所在的地点一般也是算法运行的重要部分.

  16. 可以用断言来检验算法运作的步骤|函数接收的参数|程序的控制流程|底层硬件的属性和测试用例的结果.

  17. 可以使用对算法进行检验的断言来证实您对算法运作的理解, 或将它作为推理的起点.

  18. 对函数参数和结果的断言经常记录了函数的前置条件和后置条件.

  19. 我们可以将测试整个函数的断言作为每个给定函数的规格说明.

  20. 测试用例可以部分地代替函数规格说明.

  21. 可以使用测试用例的输入数据对源代码序列进行预演.

第六章 一个完整的例子 (第十一章: 一个完整的例子)

  1. 模仿软件的功能时, 要依照相似实体的线路(类|函数|模块). 在相似的现有实体中, 为简化对源代码库的文本查找, 应选取比较罕见的名称.

  2. 自动生成的文件常常会在文件的开关有一段注释, 说明这种情况.

  3. 如果试图精确地分析代码, 一般会陷入数量众多的类|文件和模块中, 这些内容会很快将我们淹没; 因此, 我们必须将需要理解的代码限定在绝对必需的范围之内.

  4. 采用一种广度优先查找策略, 从多方攻克代码阅读中存在的问题, 进到找出克服它们的方法为止.

第七章基本编程元素 (第二章:基本编程元素)

  1. 第一次分析一个程序时, main是一个好的起始点.

  2. 层叠if-else if-…-else序列可以看作是由互斥选择项组成的选择结构.

  3. 有时, 要想了解程序在某一方面的功能, 运行它可能比阅读源代码更为恰当.(运行,并debug可以降低理解动态流程的难度)

  4. 在分析重要的程序时, 最好首先识别出重要的组成部分. (打蛇打三寸,抓住关键部分,可以快速理解)

  5. 了解局部的命名约定, 利用它们来猜测变量和函数的功能用途.(类名用名词或代词或动名词,最好不要用动词,方法用动词)

  6. 当基于猜测修改代码时, 您应该设计能够验证最初假设的过程. 这个过程可能包括用编译器进行检查|引入断言|或者执行适当的测试用例.

  7. 理解了代码的某一部分, 可能帮助你理解余下的代码.

  8. 解决困难的代码要从容易的部分入手.

  9. 要养成遇到库元素就去阅读相关文档的习惯; 这将会增强您阅读和编写代码的能力.

  10. 代码阅读有许多可选择的策略: 自底向上和自顶向下的分析|应用试探法和检查注释和外部文档, 应该依据问题的需要尝试所有这些方法.

  11. for (i = 0; i < n; i++) 形式的循环执行n次; 其他任何形式都要小心.

  12. 涉及两项不等测试(其中一项包括相等条件)的比较表达式(‘0’ <= c && c <= ‘9’)可以看作是区间成员测试(0 <= c <= 9).

  13. 我们经常可以将表达式应用在样本数据上, 借以了解它的含义. (用用例去验证表达式)

  14. 使用De Morgan法则简化复杂的逻辑表达式. (?)

  15. 在阅读逻辑乘表达式(&&)时, 问题可以认为正在分析的表达式以左的表达式均为true; 在阅读逻辑和表达式(||)时, 类似地, 可以认为正在分析的表达式以左的表达式均为false.

  16. 重新组织您控制的代码, 使之更为易读.

  17. 将使用条件运行符? :的表达式理解为if代码.

  18. 不需要为了效率, 牺牲代码的易读性.

  19. 高效的算法和特殊的优化确实有可能使得代码更为复杂, 从而更难理解, 但这并不意味着使代码更为紧凑和不易读会提高它的效率.

  20. 创造性的代码布局可以用来提高代码的易读性.

  21. 我们可以使用空格|临时变量和括号提高表达式的易读性.

  22. 在阅读您所控制的代码时, 要养成添加注释的习惯. (在阅读中比较复杂或难懂的地方要加注释备注)

  23. 我们可以用好的缩进以及对变量名称的明智选择, 提高编写欠佳的程序的易读性.(养成好的编码习惯)

  24. 用diff程序分析程序的修订历史时, 如果这段历史跨越了整体重新缩排, 常常可以通过指定-w选项, 让diff忽略空白差异, 避免由于更改了进层次而引入的噪音.(SVN/Git是否也有这个功能呢?)

  25. do循环的循环体至少执行一次.

  26. 执行算术运算时, 当b=2n-1时, 可以将a&b理解为a%(b+1). (?)

  27. 将a << n理解为a*k, k=2n.

  28. 将a>>n理解为a/k, k=2n.

  29. 每次只分析一个控制结构, 将它的内容看作是一个黑盒.

  30. 将每个控制结构的控制表达式看作是它所包含代码的断言.

  31. return, goto, break和continue语句, 还有异常, 都会影响结构化的执行流程. 由于这些语句一般都会终止或重新开始正在进行的循环,因此要单独推理它们的行为.

  32. 用复杂循环的变式和不变式, 对循环进行推理.

  33. 使用保持含义不变的变换重新安排代码, 简化代码的推理工作.

第八章 高级C数据类型 (第三章: 高级C数据类型)

  1. 了解特定语言构造所服务的功能之后, 就能够更好地理解使用它们的代码.

  2. 识别并归类使用指针的理由.

  3. 在C程序中, 指针一般用来构造链式数据结构|动态分配的数据结构|实现引用调用|访问和迭代数据元素|传递数组参数|引用函数|作为其他值的别名|代表字符串|以及直接访问系统内存.

  4. 以引用传递的参数可以用来返回函数的结果, 或者避免参数复制带来的开销.

  5. 指向数组元素地址的指针, 可以访问位于特定索引位置的元素.

  6. 指向数组元素的指针和相应的数组索引, 作用在二者上的运算具有相同的语义.

  7. 使用全局或static局部变量的函数大多数情况都不可重入(reentrant).

  8. 字符指针不同于字符数组.

  9. 识别和归类应用结构或共用体的每种理由.

  10. C语言中的结构将多个数据元素集合在一起, 使得它们可以作为一个整体来使用, 用来从函数中返回多个数据元素|构造链式数据结构|映射数据在硬件设备|网络链接和存储介质上的组织方式|实现抽象数据类型|以及以面向对象的方式编程.

  11. 共用体在C程序中主要用于优化存储空间的利用|实现多态|以及访问数据不同的内部表达方式.

  12. 一个指针, 在初始化为指向N个元素的存储空间之后, 就可以作为N个元素的数组来使用.

  13. 动态分配的内在块可以电焊工地释放, 或在程序结束时释放, 或由垃圾回收器来完成回收; 在栈上分配的内存块当分配它的函数退出后释放.

  14. C程序使用typedef声明促进抽象, 并增强代码的易读性, 从而防范可移植性问题, 并模拟C++和Java的类声明行为.

  15. 可以将typedef声明理解成变量定义: 变量的名称就是类型的名称; 变量的类型就是与该名称对应的类型.

第九章 C数据结构 (第四章: C数据结构)

  1. 根据底层的抽象数据类型理解显式的数据结构操作.

  2. C语言中, 一般使用内建的数组类型实现向量, 不再对底层实现进行抽象.

  3. N个元素的数组可以被序列for (i=0; i < N; i++)完全处理; 所有其他变体都应该引起警惕.

  4. 表达式sizeof(x)总会得到用memset或memcpy处理数组x(不是指针)所需的正确字节数.

  5. 区间一般用区间内的第一个元素和区间后的第一个元素来表示.

  6. 不对称区间中元素的数目等于高位边界与低位边界的差.

  7. 当不对称区间的高位边界等于低位边界时, 区间为空.

  8. 不对称区间中的低位边界代表区间的第一个元素; 高位边界代表区间外的第一个元素.

  9. 结构的数组常常表示由记录和字段组成的表.

  10. 指向结构的指针常常表示访问底层记录和字段的游标.

  11. 动态分配的矩阵一般存储为指向数组列的指针或指向元素指针的指针; 这两种类型都可以按照二维数组进行访问.

  12. 以数组形式存储的动态分配矩阵, 用自定义访问函数定位它们的元素.

  13. 抽象数据类型为底层实现元素的使用(或误用)方式提供一种信心的量度.

  14. 数组用从0开始的顺序整数为键, 组织查找表.

  15. 数组经常用来对控制结构进行高效编码, 简化程序的逻辑.

  16. 通过在数组中每个位置存储一个数据元素和一个函数指针(指向处理数据元素的函数), 可以将代码与数据关联起来.

  17. 数组可以通过存储供程序内的抽象机(abstract machine)或虚拟机(virtual machine)使用的数据或代码, 控制程序的运作.

  18. 可以将表达式sizeof(x) / sizeof(x[0])理解为数组x中元素的个数.

  19. 如果结构中含有指向结构自身|名为next的元素, 一般说来, 该结构定义的是单向链表的结点.

  20. 指向链表结点的持久性(如全局|静态或在堆上分配)指针常常表示链表的头部.

  21. 包含指向自身的next和prev指针的结构可能是双向链表的结点.

  22. 理解复杂数据结构的指针操作可以将数据元素画为方框|指针画为箭头.

  23. 递归数据结构经常用递归算法来处理.

  24. 重要的数据结构操作算法一般用函数参数或模板参数来参数化.

  25. 图的结点常常顺序地存储在数组中, 链接到链表中, 或通过图的边链接起来.

  26. 图中的边一般不是隐式地通过指针, 就是显式地作为独立的结构来表示.

  27. 图的边经常存储为动态分配的数组或链表, 在这两种情况下, 边都锚定在图的结点上.

  28. 在无向图中, 表达数据时应该将所有的结点看作是等同的, 类似地, 进行处理任务的代码也不应该基于它们的方向来区分边.

  29. 在非连通图中, 执行遍历代码应该能够接通孤立的子图.

  30. 处理包含回路的图时, 遍历代码应该避免在处理图的回路进入循环.

  31. 复杂的图结构中, 可能隐藏着其他类型的独立结构.

第十章 高级控制流程 (第五章: 高级控制流程)

  1. 采用递归定义的算法和数据结构经常用递归的函数定义来实现.

  2. 推理递归函数时, 要从基准落伍测试开始, 并认证每次递归调用如何逐渐接近非递归基准范例代码.

  3. 简单的语言常常使用一系列遵循该语言语法结构的函数进行语法分析.

  4. 推理互递归函数时, 要基于底层概念的递归定义.

  5. 尾递归调用等同于一个回到函数开始处的循环.

  6. 将throws子句从方法的定义中移除, 然后运行Java编译器对类的源代码进行编译, 就可以容易地找到那些可能隐式地生成异常的方法.

  7. 在多处理器计算机上运行的代码常常围绕进程或线程进行组织.

  8. 工作群并行模型用于在多个处理器间分配工作, 或者创建一个任务池, 然后将大量需要处理标准化的工作进行分配.

  9. 基于线程的管理者/工人并行模型一般将耗时的或阻塞的操作分配给工人子任务, 从而维护中心任务的响应性.

  10. 基于进程的管理者/工人并行模型一般用来重用现有的程序, 或用定义良好的接口组织和分离粗粒度的系统模块.

  11. 基于流水线的并行处理中, 每个任务都接收到一些输入, 对它们进行一些处理, 并将生成的输出传递给下一个任务, 进行不同的处理.

  12. 竞争条件很难捉摸, 相关的代码常常会将竞争条件扩散到多个函数或模块; 因而, 很难隔离由于竞争条件导致的问题.

  13. 对于出现在信号处理器中的数据结构操作代码和库调用要保持高度警惕.

  14. 在阅读包含宏的代码时, 要注意, 宏既非函数, 也非语句.

  15. do…while(0)块中的宏等同于控制块中的语句.

  16. 宏可以访问在它的使用点可见的所有局部变量.

  17. 宏调用可改变参数的值

  18. 基于宏的标记拼接能够创建新的标记符.

第十一章 编码规范和约定 (第七章: 编码规范和约定)

  1. 了解了给定代码库所遵循的文件组织方式后, 就能更有效率地浏览它的源代码.

  2. 阅读代码时, 首先要确保您的编辑器或优美打印程序的tab设置, 与代码遵循的风格规范一致.

  3. 可以使用代码块的缩进, 快速地掌握代码的总体结构.

  4. 对编排不一致的代码, 应该立即给予足够的警惕.

  5. 分析代码时, 对标记为XXX, FIXME和TODO的代码序列要格外注意: 错误可能就潜伏在其中.

  6. 常量使用大写字母命名, 单词用下划线分隔.

  7. 在遵循Java编码规范的程序中, 包名(package name)总是从一个顶级的域名开始(例如, org, com), 类名和接口名由大写字母开始, 方法和变量名由小写字母开始.

  8. 用户界面控件名称之前的匈牙利记法的前缀类型标记可以帮助我们确定它的作用.

  9. 不同的编程规范对可移植构造的构成有不同的主张.

  10. 在审查代码的可移植性, 或以某种给定的编码规范作为指南时, 要注意了解规范对可移植性需求的界定与限制.

  11. 如果GUI功能都使用相应的编程结构来实现, 则通过代码审查可以轻易地验证给定用户界面的规格说明是否被正确地采用.

  12. 了解项目编译过程的组织方式与自动化方式之后, 我们就能够快速地阅读与理解对应的编译规则.

  13. 当检查系统的发布过程时, 常常可以将相应发行格式的需求作为基准.


(2)如何阅读linux源代码

随着linux的逐步普及,现在有不少人对于Linux的安装已经比较熟悉了。与Linux的蓬勃发展相适应,想深入了解Linux的也越来越多。而要想深入了解Linux,就需要阅读和分析linux内核的源代码。

Linux的内核源代码可以从很多途径得到。一般来讲,在安装的linux系统下,/usr/src/linux目录下的东西就是内核源代码。另外还可以从互连网上下载,解压缩后文件一般也都位于linux目录下。内核源代码有很多版本,目前最新的稳定版是3.0.3。

许多人对于阅读Linux内核有一种恐惧感,其实大可不必。当然,象Linux内核这样大而复杂的系统代码,阅读起来确实有很多困难,但是也不像想象的那么高不可攀。只要有恒心,困难都是可以克服的。也不用担心水平不够的问题,事实上,有很多事情我们不都是从不会到会,边干边学的吗?再就是根据自己的工作需要针对某一部分进行深入理解,毕竟现在linux内核太庞大了,全面了解的困难不小。

任何事情做起来都需要有方法和工具。正确的方法可以指导工作,良好的工具可以事半功倍。对于Linux内核源代码的阅读也同样如此。下面我就把自己阅读内核源代码的一点经验介绍一下,最后介绍Window平台下的一种阅读工具。

对于源代码的阅读,要想比较顺利,事先最好对源代码的知识背景有一定的了解。对于linux内核源代码来讲,我认为,基本要求是:
a、操作系统的基本知识;
b、对C语言比较熟悉,最好要有汇编语言的知识和GNU C对标准C的扩展的知识的了解。

另外在阅读之前,还应该知道Linux内核源代码的整体分布情况。我们知道现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序、网络等组成。看一下Linux内核源代码就可看出,各个目录大致对应了这些方面。

Linux内核源代码的组成如下(假设相对于linux目录):

arch:这个子目录包含了此核心源代码所支持的硬件体系结构相关的核心代码。如对于X86平台就是i386。

include:这个目录包括了核心的大多数include文件。另外对于每种支持的体系结构分别有一个子目录。

init:此目录包含核心启动代码。

mm:此目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下,如对应于X86的就是arch/i386/mm/fault.c

drivers:系统中所有的设备驱动都位于此目录中。它又进一步划分成几类设备驱动,每一种也有对应的子目录,如声卡的驱动对应于drivers/sound。

ipc:此目录包含了核心的进程间通讯代码。

fs:Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext2文件系统对应的就是ext2子目录。

kernel:主要核心代码。同时与处理器结构相关代码都放在arch/*/kernel目录下。

net:核心的网络部分代码。里面的每个子目录对应于网络的一个方面。

lib:此目录包含了核心的库代码。与处理器结构相关库代码被放在arch/*/lib/目录下。

scripts:此目录包含用于配置核心的脚本文件。

Documentation:此目录是一些文档,起参考作用。

清楚了源代码的结构组成后就可以着手阅读。对于阅读方法或者说顺序,有所谓的纵向与横向之分。所谓纵向就是顺着程序的执行顺序逐步进行;所谓横向,就是分模块进行。其实他们之间不是绝对的,而是经常结合在一起进行。对于Linux源代码来讲,启动的代码就可以顺着linux的启动顺序一步一步来,它的大致流程如下(以X86平台为例):

./larch/i386/boot/bootSect.S–>./larch/i386/boot/setup.S–>
./larch/i386/kernel/head.S–>./init/main.c中的start_kernel()。

而对于象内存管理等部分,则可以单独拿出来进行阅读分析。我的体会是:开始最好按顺序阅读启动代码,然后进行专题阅读,如进程部分,内存管理部分等。在每个功能函数内部应该一步步来。实际上这是一个反复的过程,不可能读一遍就理解。

俗话说:“工欲善其事,必先利其器”。 阅读象Linux核心代码这样的复杂程序令人望而生畏。它象一个越滚越大的雪球,阅读核心某个部分经常要用到好几个其他的相关文件,不久你将会忘记你原来在干什么。所以没有一个好的工具是不行的。由于大部分爱好者对于Window平台比较熟悉,并且还是常用Window系列平台,所以在此我介绍一个Window下的一个工具软件:Understand。关于使用参见上一篇博客有兴趣的朋友可以装一个Understand,那样你阅读源代码的效率会有很大提高的。怎么样,试试吧!


最后,对于在这篇文章中提到的分析源代码程序的一些方法做一下小结,以作为本文的结束。

分析一个源代码,一个有效的方法是:

  1. 阅读源代码的说明文档,比如本例中的README, 作者写的非常的详细,仔细读过之后,在阅读程序的时候往往能够从README文件中找到相应的说明,从而简化了源程序的阅读工作。

  2. 如果源代码有文档目录,一般为doc或者docs, 最好也在阅读源程序之前仔细阅读,因为这些文档同样起了很好的说明注释作用。

  3. 从makefile文件入手,分析源代码的层次结构,找出哪个是主程序,哪些是函数包。这对于快速把握程序结构有很大帮助。

  4. 从main函数入手,一步一步往下阅读,遇到可以猜测出意思来的简单的函数,可以跳过。但是一定要注意程序中使用的全局变量(如果是C程序),可以把关键的数据结构说明拷贝到一个文本编辑器中以便随时查找。

  5. 分析函数包(针对C程序),要注意哪些是全局函数,哪些是内部使用的函数,注意extern关键字。对于变量,也需要同样注意。先分析清楚内部函数,再来分析外部函数,因为内部函数肯定是在外部函数中被调用的。

  6. 需要说明的是数据结构的重要性:对于一个C程序来说,所有的函数都是在操作同一些数据,而由于没有较好的封装性,这些数据可能出现在程序的任何地方,被任何函数修改,所以一定要注意这些数据的定义和意义,也要注意是哪些函数在对它们进行操作,做了哪些改变。

  7. 在阅读程序的同时,最好能够把程序存入到cvs之类的版本控制器中去,在需要的时候可以对源代码做一些修改试验,因为动手修改是比仅仅是阅读要好得多的读程序的方法。在你修改运行程序的时候,可以从cvs中把原来的代码调出来与你改动的部分进行比较(diff命令), 可以看出一些源代码的优缺点并且能够实际的练习自己的编程技术。

  8. 阅读程序的同时,要注意一些小工具的使用,能够提高速度,比如vi中的查找功能,模式匹配查找,做标记,还有grep,find这两个最强大最常用的文本搜索工具的使用。

对于一个Unix/Linux下面以命令行方式运行的程序,有这么一些套路,大家可以在阅读程序的时候作为参考。

  1. 在程序开头,往往都是分析命令行,根据命令行参数对一些变量或者数组,或者结构赋值,后面的程序就是根据这些变量来进行不同的操作。

  2. 分析命令行之后,进行数据准备,往往是计数器清空,结构清零等等。

  3. 在程序中间有一些预编译选项,可以在makefile中找到相应部分。

  4. 注意程序中对于日志的处理,和调试选项打开的时候做的动作,这些对于调试程序有很大的帮助。

  5. 注意多线程对数据的操作。(这在本例中没有涉及)

结束语:

   当然,在这篇文章中,并没有阐述所有的阅读源代码的方法和技巧,也没有涉及任何辅助工具(除了简单的文本编辑器),也没有涉及面向对象程序的阅读方法。我想把这些留到以后再做讨论。也请大家可以就这些话题展开讨论。

三、选一些比较优秀的开源产品作为源代码阅读对象?

优秀的产品及项目:

1、大型产品:

1.1 C/C++/Object-C:
   Linux内核(C):
      《Linux内核代码》
      《深入理解LINUX内核》
      《Linux内核完全剖析》
      《Linux内核源代码情景分析(上册)》
      《莱昂氏UNIX源代码分析》
      《Linux内核设计与实现》
      《Linux内核设计的艺术》
      《自己动手写嵌入式操作系统》
      《自己动手写操作系统》
      《Orange’S:一个操作系统的实现》
      《TCP/IP协议栈》
      《TCP/IP详解》
     
   JVM(C/C++)、OpenJDK7or8:OpenJDK源码阅读导航:http://rednaxelafx.iteye.com/blog/1549577
      《深入Java虚拟机(原书第2版)》
      《Java Virtual Machine Specification》
      《深入理解Java虚拟机》 —-14
     
  GCC:
      《垃圾收集》
      《An Introduction to GCC》
     
  DB: MySQL (C/C++) MySQL JDBC Driver (mysql-connector-java-5.1.13)
      《MySQL技术内幕》, PostgreSQL代码结构清晰

  Nginx:
      《实战Nginx》
      《深入理解Nginx:模块开发与架构解析》 — 14

  Apache(C++):
      《Apache源代码全景分析第1卷》
      《Apache Server源代码分析》

1.2 Java/C#:

JDK类库(尤其util(concurrent)包集合容器等) — 14
Eclipse
OSGI
Servlet规范
JSP规范
Spring 《Spring揭秘》《Spring技术内幕—深入解析Spring架构与设计原理》
Struct2,JDBC连接池,Ibatis
Tomcat 《深入剖析Tomcat》《How Tomcat Works》
消息中间件:ActiveMQ
Hadoop 《Hadoop实战》

1.3 Other:
PHP
Python
Linux工具集/Shell/Perl
Erlang编译器
ruby
人机交互、函数式语言、并发语言、逻辑语言

2、中小型产品:

2.1 C/C++/Object-C:
  STL:
     《C++标准程序库》
     《标准模板库自修教程与参考手册(STL进行C++编程第2版)》
     《Effective STL中文版》
     《STL源码剖析》
     《泛型编程与STL》
     《C++ Templates》

  Boost:
     《超越C++标准库:Boost库导论》
     《Boost程序库完全开发指南》
     
  ACE:
     《C++网络编程(卷1)》
     《C++网络编程(卷2)》
     《ACE技术内幕》
  Cache :
     Redis
     Memcached

  安全:
     OpenSSL

2.2 Java/C#:
Netty(I/O)
Jetty
JDBC
Hibernate
Ibatis iBATIS框架源码剖析 《iBatis in Action》《iBATIS框架源码剖析》
Struts(MVC): 《Struts 2实战》
WebWork
Velocity1.6
cache:OSCache

2.3 Other:
C#/Flex客户端
JQuery(JS) 《jQuery基础教程》
Python 《Python源码剖析》

3、小型产品:
3.1 C/C++/Object-C:
接入、分发、ptypes

3.2 Java/C#:
log4j
httpclient
JUnit4
MegaClient/MegaServer
数据库连接池:

3.3 Other:
V8 Node

四、参考
[1] 摘自代码阅读方法与实践书籍的知识点总结
[2] 如何阅读linux源代码
[3] 如何阅读源代码(1)
[4] 如何阅读源代码Linux开发者:http://wenku.baidu.com/view/56270184b9d528ea81c77968.html

0 0