声明:关于代码阅读的研究,很多思想和文字是来自《代码阅读》这本书,再加上自己的学习和工作经验。可以说是类似读书笔记的,我把它作为了毕业论文的第8章,并结合了自己的毕设作品进行解释,毕设源代码github下载地址:https://github.com/chinaran/A-LL1-Compiler。
9.1 基本编程元素多数情况下,代码阅读是个自底向上的活动。这一章将讲述一些需要注意的基本编程元素,并概述如何对其进行阅读与思考。
(1)对于一些已经提供的系统函数,其执行效率可能达不到我们的要求,不过可以对其重新封装。例如,标准C函数strcmp(字符串比较函数),可以定义一个宏STREQ(a, b),#define STREQ(a, b) (*(a) == *(b)&& strcmp((a),(b)) == 0),这样就可以减少字符比较次数,另外,宏中的参数用小括号括起来是个良好的保护性编程实践。
(2)使用未初始化的变量可能导致问题,所以要在定义变量的同时就赋初值。你可能会说不是有默认值吗,干嘛还这么麻烦。即使这样,为了方便别人阅读代码,不至于导致混乱,而且不同语言对未赋值的变量处理不同。例如C语言中 int n; 则n为随机值,若程序中出现这样的代码,极易导致程序崩溃。
图 9-1 Java类中定义变量的同时就赋初值(现C++11也支持这种用法)
(3)在分析一个重要程序时,最好找出其中重要的部分,例如全局变量(作用域大)。
(4)在C语言中,只用于单个文件的全局变量最好声明为static(防止多个文件同命名)。C++中使用命名空间,如 using namespace std; Java中使用包,如package compilers.javabean;
(5)查看一个函数的方法:
● 根据函数名猜测。
● 阅读函数开头的注释。
● 分析该函数是如何使用的。
● 阅读函数体的代码。
● 查询外部程序文档。
● 与函数的编写者或使用者交流。
图 9-2 如何查看函数示例
(6)使用渐进式的阅读方法,即先理解较容易的部分,然后可能使其他部分变得容易理解。如果代码的作者,例如工作中的同事在身边的活,去询问他理解困难的地方,可以获得极大的帮助(不要不好意思问,因为这将节约整个项目组的时间)。
(7)开发者要养成碰到库函数就阅读文档的习惯,这有助于增强自身的代码阅读和编写能力。例如微软的MDDN,Unix/Linux的man,Java的API文档(笔者编写该系统时,就经常查阅Java API文档)。
(8)switch的case会处理到break,如果需要共享,最好添加注释/* FALLTHROUGH */。另外,使用default处理忽然遇到的意外值,请记住,不管需不需要default,最好都添加上,这是一个良好的保护性编程实践。
图 9-3 switch case 注意事项(代码来自 NetBSD)
(9)for( ; ; )这种无限循环来表达循环开始或结束时退出条件无法确定的循环。不过也有真正的无限循环,如果你学过操作系统或看过Linux内核,就会知道进程的实质就是无限循环(死循环)。
(10)使用德·摩根法则简化比较逻辑:!(a || b) <=> !a&& !b; !(a && b) <=> !a || !b。由于比较采用的是短路求值,应该把概率大的放在最左边。
图 9-4 简化比较操作
(11)有时,创造性的代码布局可以提高代码的可读性。反面的例子很多,可以在国际混换C代码竞赛网站上(www.ioccc.org)找到很多,而且它们大多很有趣。
(12)添加空格,使用临时变量,使用小括号都可以提高表达式的可读性。
例如 if(! isTrue(a, b))。
(13)在阅读由自己控制的代码时,要养成添加注释的习惯。如下图是编译系统主函数入口注释,为标明该类含有main函数。
图 9-5 main函数块注释
(14)一个项目应采用一种标准的风格(如GNU或BSD),并始终如一的使用,详见8.4节编码规范和约定。
(15)if(a = b)可能存在“=” “==” 错误,避免这类错误的方法是,对于常量,可以放在左边,例如if(0 == a)。不过对于java、C#不需要担心此类错误,因为这类语言的流程语句只接受布尔值。
(16)while(column& 7) [......],这样的代码并不好理解。若b = 2n – 1,则 a & b(逻辑与操作)可以理解为 a % (b + 1) ,这样做的目的是使无符号的计算更高效。实际上,现在的优化编译器可以识别出这种情况并自己做替换,而除法和按位与操作在现代处理器上差距没有那么大了。因为,应会读这些代码,但避免写出这样的代码(另一个移位常用的操作是 *2n或 /2n ,对应左移和右移)。
(17)阅读控制结构代码时,可以将注意力集中在如何在抽象层对他们推理,即保存主要的控制结构,if, else, for, while 等,屏蔽无关细节代码。
9.2 常用数据结构程序通过对算法的应用作用于数据之上,因而数据内部组织结构对算法的执行而言十分重要。
(1)理解动态内存管理的基本操作有助于我们更加清晰地阅读程序代码。例如为结构体分配内存空间,先定义宏#define new(type) (type *)calloc(sizeof(type), 1),再分配空间node = new (structcodeword_entry),并且一定要检查malloc函数的返回值。
(2)在函数参数中使用引用时,为明确参数的值是否会被函数影响,可以使用/* IN */ 或 /* OUT */注释。例如 void getNum(char a, int *n /* OUT */ )。
(3)使用全局变量或者静态局部变量的函数在大多数情况下是不可重入的,即不可并发访问,这点在编写分布式程序时尤其要注意。
(5)结构体中各个字段的存储顺序与编译器和机器架构相关,并且每个元素的表示方法取决于具体的体系架构和操作系统。因为用结构体来映射外部数据有着与生俱来的不可移植性。
(6)C程序的typedef声明用来增强语言的抽象能力、提高代码的可读性和可移植性。
(7)C中对数组的操作,初始化为0:static char buf[128]; memset(buf,0, sizeof(buf));复制:memcpy(dest,src, sizeof(desc)); 这里需要判断desc和src是否指向同一内存空间,否则使用memmove()。
(8)若被调用函数中含有数组参数,如果访问数组外界元素,会导致缓冲区溢出。开发者对于那些可能重写其缓冲区的函数,诸如strcat, strcpy, spintf, gets 和 scanf等,应当谨慎使用或不用,或用其安全替代,strncat, strncpy, snprintf, fgets等。据不完全统计,针对微软Windows的攻击,75%都是利用缓冲区溢出攻击,你也可以在计算机安全的课上学到如何利用缓冲区溢出。
9.3 其他编程技巧笔者在这篇文章中列举的编程技巧都是非常巧妙的或者之前没有注意到的,还有更多常见的技巧,可以查阅像林锐博士写的《高质量C++/C编程》之类的书籍。
(1)在有错误返回的函数中,尽量把错误判断放在最前,一是效率较高,二是控制流程嵌套较少,阅读体验更好,如下图:
图 9-6 函数内部先处理错误