代码检视技能属于开发人员的基本功,能够很大程度地反应出开发人员的能力水平,前面4.4.1节已经讲过提高评审检视的方法。下面以实际的C/C++语言方面的代码来讲解代码检视的一些基本关注点和重点检查的内容。
从C/C++语言开发的代码来讲,检视时主要关注以下一些方面:
- 与详细设计的一致性
- 编译设置
- 头文件检查
- 宏定义检查
- 常量
- 全局变量与共享变量
- 静态变量和函数
- 数据结构
- 初始化
- 字符串
- 输入校验
- 内存分配和释放
- 边界条件
- 计算
- 类型转换
- 指针使用
- 数组使用
- 函数
- 系统和标准库调用
- 规范性
- 冗余代码
- 判断循环条
- 注释文字
- 资源释放
- 特殊的语法规则
- 可移植性
- 网络功能
下面对这些需要关注的方面进行更进一步的说明:
1、与详细设计的一致性
只要将检视的代码对照详细设计进行比较就很容易检查出代码是否和详细设计一致,采用逐行逐字阅读进行比较的方法进行。
2、编译设置
编译设置主要检查以下方面:
- 是否使用了优化选项,优化类型和项目所需要的类型是否一致
- 是否正确使用了编译预定义的宏?比如在VC 中使用winsock2.0时要定义WIN32_LEAN_AND_MEAN
宏
- 是多线程还是单线程模式
- 结构体对齐字节数设置是否正确,跨平台时会不会有问题
- 调用系统的库是静态编译进程序内还是使用动态库调用形式?比如使用MFC就存在这个问题
- 发行版中连接的库是否误连接了调试版本的库(可能会出现将调试版设置拷贝到发行版,导致发行版本中连接了调试版本的库)
- include路径和lib路径设置是否正确,否则装有多个编译器的情况下有可能使用了不正确的头文件和库
3、头文件检查
头文件检查主要关注以下方面:
- 是否包含有多余的其他头文件
- 头文件是否内聚,即是否多个模块共用一个头文件
- 多个头文件的引用是否有先后顺序问题
- 头文件注释是否规范
- 头文件内的内容是否清晰,是否分类排放好并给出了足够的注释
- 包含的系统头文件是否有系统兼容性和移植性的问题
- 是否使用了象 #ifdef __LIST_H__ 之类的宏定义保证头文件不被重复引用
4、宏定义检查
- 宏定义中有参数和表达式时,参数和表达式是否都用括号括起来了。例如:
#define ADD(a, b) (a + b) //正确的应该是 ((a) + (b))
这个定义中就没有将参数a和b括起来,如果使用时a和b是表达式的话,就会因为运算符顺序问题而出问题。
- 续行符/是否使用正确
- 引号“”是否使用正确
- 代码中编译或调试开关的宏是否正确设置
5、常量
常量方面主要检查的主要问题如下:
- 常量是否书写正确,两种典型错误,一是数字或字母由于键盘失误写错,比如2写成3或1等。另一种是常量里有位顺序,将位顺序搞错了。
- 常量是否使用了宏来进行定义
- 程序中是否存在魔法数字
- 16进制数据是否在前面加上了0x
- 常量是否来自规格
- 不来自规格的常量的值是否合理
6、全局变量与共享变量
全局变量与共享变量需要检查的主要问题如下:
- 全局变量是否必须的,是否可以改成局部变量?
- 是否有多个任务访问共享变量,是否进行了有效的保护?
- 当全局变量只限于本文件内使用时,是否定义成静态的?
- 多个任务读写共享变量时,是否可以将读写操作封装成独立函数,而不是在每个模块里都进行加锁解锁操作
7、静态变量和函数
静态变量和函数检视时主要问题如下:
- 静态变量的使用是否正确
- 每次使用静态变量时是否需要重新初始化
- 对不需要重新初始化的静态变量在多次使用后是否有溢出的问题。
- 文件内部使用的函数是否定义成静态的
8、数据结构
数据结构方面考虑的主要问题如下:
- 数据结构里的成员类型定义是否正确
- 结构体里面变量顺序安排是否合理,数据是否对齐
- 是否存在冗余未用的成员变量。
- 类里面是否有私有变量和私有函数放到了公有的定义里去了
- 是否有多个任务调用了数据结构的操作时,是否存在数据重入问题
9、初始化
初始化考虑的主要问题如下:
- 变量使用前是否需要初始化
- 类的构造函数中是否对需要初始化的成员都进行了初始化(使用成员初始序列进行初始化或在函数体内部进行赋值进行初始化)
- 初始化的值是否书写正确
- 数组的初始化是否正确
- 内存或数组在每次使用前是否需要初始化清零
- 多个变量初始化赋值时是否存在顺序问题
- 静态变量和全局变量的初始化是否存在初始化顺序问题
- 字符串数组是否有不需要初始化清零,而只需操作完后在尾部添加’/0’的情况
10、字符串
检视字符串时考虑的主要问题如下:
- 字符串是否以’/0’结尾
- 字符串是否会超长
- 字符串使用的空间大小是否存在差1问题
- 使用字符串指针时,指向的位置是否存在差1问题
- 输入的字符串前后有空格TAB键、回车键等特殊字符时,程序中是否将前后这些特殊字符删除掉。
- 字符串指针是否可以为空,为空时会有什么现象?
- 字符串内容为空(即第一个字符为’/0’)时会发生什么现象?
- 字符串中如果有转义字符“/”字符时,是否正确地写成了“//”
- 字符串中有斜杠‘/’时,是否误写成反斜杠‘/’
- 在对字符串进行拷贝或连接操作时,是否对空间大小进行校验?
- 是否有大小写的问题?
11、输入校验
输入校验需要检视的主要问题如下:
- 函数参数是否需要进行了校验?
- 从文件读取的数据是否进行了校验?
- 使用全局数据时是否需要进行校验?
- 通信收到的数据是否需要进行校验?
- 从消息中接受到的数据是否需要进行校验?
12、内存分配和释放
内存分配方面需要检查的有以下几点:
- 分配的大小是否正确,是否分配了过大的内存或者分配的内存大小不足,分配的内存大小是否存在差1错误
- 内存分配是否经过判断或者进行异常处理
- 重新分配一块内存时,是否将原有内存释放
- 分配的内存是否需要初始化清零
- 是否有在大循环中不断分配内存导致可能出现系统内存不足情况
释放方面需要检查的有以下几点:
- 所有的分支路径上是否将分配的内存进行了释放
- 是否将已经释放的内存重复释放
- 释放的是否是空指针
- 是否错误释放了另外一个相似的指针
- 释放多块内存时是否存在释放的先后顺序问题
- 是否将动态库中分配的内存在动态库外部释放掉或者动态库外部分配的内存却在动态库内释放
使用realloc()时要考虑以下几点:
- 新增空间是否需要初始化清零?
- 是否还有指针指向老的内存块,并在realloc()后使用指向老的内存块的指针。
13、边界条件
凡是牵涉边界条件的地方都需要进行边界检查,以下的一些问题供参考:
- 循环变量上的边界是否正确
- 变量的取值是否有边界条件限制,边界是否给出并书写正确?
- 空间边界,如内存大小,数组大小是否正确,是否存在差1和越界情况?
- 数据结构边界,如链表的头一条记录和最后一条记录等边界情况
- 服务器连接数量最大是多少
- 断掉网线或打印机缺纸时会发生什么?
关于边界的具体情况,请参阅本书第3章的3.6.2节,里面有对整数和字符串边界的详细材料。
14、计算
计算错误也是程序中经常遇到的一个问题,大部分计算错误可以经过测试发现,但并不是所有的计算错误都可以很容易通过测试来发现,以下的一些问题供检视时参考:
- 计算表达式或公式是否书写正确,需要逐字符地进行确认没有输入错误
- 表达式中运算符顺序是否书写正确,同优先级运算符运算时是否存在自左至右结合或自右至左结合运算结果不同的问题
- 是否需要使用括号来保证运算顺序的正确性和增加程序的可读性
- 表达式中括号过多时,括号书写是否正确
- 是否存在计算溢出情况,如两个整数相乘结果超出整数最大范围等情况
- 截断误差和舍入误差是否会引起问题,误差是否会累积下去导致误差越来越大?
- 是否存在除零问题(即零做分母的问题),或者两个整数相除结果得到零然后再和其他整数相乘。
- 加减号是否写错,这两个符号在键盘相邻位置,很容易造成键盘输入失误导致写错
- 是否可以对算法进行优化提高效率
- 是否存在某个变量会累积增加导致长时间运行后的溢出
- 计算结果是否存在差1错误
15、类型转换
类型转换的检查有以下问题供参考:
- 类型转换是否采用安全的转换机制
- 当采用强制转换时是否会出问题
- signed 和unsigned转换是否存在问题
- 转换前是否进行安全校验
- 是否将小空间的类型转换成了大空间的类型
- 类型转换是否会造成截断、溢出或越界
16、指针使用
指针在C/C++中是使用最广泛的一种语法,这也是C/C++有别于JAVA,fortran,basic等语言的地方,指针使得语言的功能强大起来,但也给程序质量带来了很大麻烦,使用指针时是极易出错的,可以说C/C++代码中的缺陷大部分都与指针有关,下面给出检视指针的一些问题参考:
- 指针是否初始化
- 指针类型定义是否正确
- 使用前是否申请了内存
- 引用是否正确,是否引用了释放掉的空间
- 指向的空间是否正确
- 是否存在使用野指针现象
- 释放后再使用时是否需要重新初始化
- 是否使用了空指针,函数指针是否为空就被调用
- 指针是否需要校验
- 指针进行类型转换时是否会引起问题
- 指针地址运算是否有误,在地址相加时是否考虑了相加的数字要乘以指针类型所占空间的大小。比如int
*p; p+1相比p的大小不是大于1,而是大于一个整数所占空间的字节数。
17、数组使用
数组的使用也是很容易出错的一种,不幸的是现在还没有足够好的方法能保证数组越界一类的问题得到完美的解决,所以通过对数组的检视来保证质量就很重要了,下面给出检视数组的一些建议:
- 类型是否正确
- 多维数组是否数据存放顺序正确
- 数组使用时是否会越界,空间大小是否存在差1错误
- 作用域是否正确
- 数组大小是否太大导致浪费
18、函数
函数方面的一些检视建议如下:
- 函数调用的参数传递是否正确,
- 是否有形参和实参使用错误的问题,
- 调用函数前是否需要校验,
- 函数的返回值和输出是否需要校验,
- 调用的函数是否对全局数据产生影响
- 函数功能是否单一,是否在函数里处理了多个不同的功能
- 函数参数是否需要定义为const
- 回调函数原型是否和定义一致
- 函数是否过长(一般以不超过200行为宜)
19、系统和标准库调用
调用系统函数和库函数时,以下一些检视建议供参考:
- 系统调用是否正确,调用参数设置是否正确
- 是否按照标准文档中的要求和注意事项进行了调用
- 对于存在BUG的系统函数是否采取了规避措施进行调用
- 对调用系统函数是否需要在调用前进行了输入校验?
- 调用后是否需要对输出进行校验?
20、规范性
规范性方面的一些检视建议如下:
- 是否符合内部的编码规范
- 是否和业界的编码规范兼容
- 注释格式规范是否符合要求
- 代码修改时的注释是否记录了时间和修改者信息以便于跟踪
- 变量命名是否易于理解
- 多个变量名是否容易造成混淆?是否有多个命名相似的变量在一起?
- 是否有全局范围内变量和局部范围内变量重名情况
21、冗余代码
冗余代码在程序中虽然不直接影响质量,但会影响程序的可读性,给后续维护增加困难,因此程序中的冗余的代码最好都删除掉,检查冗余代码时,以下建议供参考:
- 代码中是否存在无用的调试和测试代码
- 是否存在废弃不用的函数代码
- 是否存在注释掉的一些垃圾代码,不仅要检查使用注释符号/*…*/ 和//注释掉的代码,还要检查使用宏定义注释掉的代码,如#if
0注释掉或 #ifdefined (__MACRO__) 之类的宏定义的注释掉的代码
22、判断循环条件
在程序中的判断和循环条件中,也存在着一些有时通过测试难以发现的问题,主要的检视建议如下:
- 是否将>,<两个符号写错,这两个符号在键盘相邻位置,很容易造成键盘输入失误
- 逻辑运算符是否正确,如| 和||,&和&&运算符是否搞混淆掉或键盘失误写错,
- 逻辑等号==是否误写成等号=
- 运算符顺序是否正确,运算符| 、&,||、&&,=、== 的运算顺序需要特别注意
- 循环判断中的表达式是否正确地使用了括号将运算顺序区分开,并增加可读性
- 表达式运算是否存在逻辑上的错误
- 对浮点数是否误用了精确相等进行比较
- 循环变量是否进行了初始化
- 循环的中止条件是否在某些情况下无法到达而造成死循环
- 循环的边界上是否会造成问题
- 判断条件是否会恒真或恒假
23、注释文字
注释和文字方面的一些检视建议如下:
- 代码中的注释是否达到一定比例,一般要求20~30%左右的注释,即注释行占整个代码行的比例要达到20~30%左右。当然根据不同的项目类型和编码风格,注释率要求会有所不同
- 是否要求按一定的格式进行注释,有些工具可以将代码中的注释导出来形成文档,比如Doc++工具就可以将符合一定注释规范的注释导出来形成文档
- 函数头的注释检查,函数内容描述是否足够帮助理解函数的行为,参数和返回值描述是否足够帮助调用函数的人如何使用函数,参数的范围是否进行了描述
- 程序中的处理是否和注释中的描述一致
- 函数中的关键地方是否都进行注释,特别是一些难于理解的地方是否有注释
- 修改的地方是否进行了注释说明修改原因和记录修改的时间和修改人 n 程序中的信息文字是否都集中放在一个地方,便于本地化。
24、资源释放
资源释放方面的一些检视建议如下:
- 所有的资源是否都进行了释放
- 释放前是否要进行合法性检查以避免重复释放或释放掉还未分配的资源
- 要检查是否存在某条路径遗漏了释放
- 打开文件是否关闭了,信号量是否释放,句柄是否关闭,锁资源是否释放,是否存在死锁问题
- 全局的资源是否存在随时间累积增加不减少的问题?
- 其他各种资源如网络socket等是否在各条对应路径上进行了关闭
- 类的析构函数中是否对类中需要释放的成员进行了释放
- 是否存在在一个模块中分配却在另外一个模块释放现象
25、特殊的语法规则
C++中有许多的语法规则需要注意,稍有不慎就会造成问题,以下几点建议供参考:
- 类中是否需要拷贝构造函数
- 函数重载时是否会导致调用到错误的成员函数
- 是否更多的用包含来替代继承
- 基类中的析构函数定义成虚函数了吗
- 静态变量的初始化和使用是否正确
- 重载new和delete时,是否将原型定义置于所有全局变量和相应头文件之前
- 构造函数中调用虚函数时是否考虑了虚机制不起作用的问题,调用的只是本地版本
- 使用setjmp()和longjmp()时是否考虑了析构函数不被调用的问题
- 异常处理里是否调用了被进行了异常处理的代码
26、可移植性
以下检查建议供参考:
- 与系统相关的调用是否都被封装在专门的模块里
- 非系统功能封装模块中是否调用了某个系统特有的API或函数
- 是否兼容老的版本
- 程序中的变量或数据是否存在大小字节顺序问题
- 结构体数据对齐格式是否兼容各个系统
- 是否使用了行内汇编代码
27、网络功能
以下检查建议供参考
- 连接的IP地址、端口参数等是否进行网络字节顺序转换
- 收发数据是否都进行了正确的数据字节顺序转换,需要将收到的数据从网络字节序转成主机字节序,而将发送的数据在发送前先从主机字节序转换成网络字节序进行处理
- 网络通信时的连接、收发等操作是否进行了超时处理
- 对收发的网络数据是否进行了大小限制
- 接受数据的缓冲区大小是否足够,接受长度参数是否写错,是否存在差1错误
- 发送数据的大小参数设置是否和实际发送数据大小一样
- 收发数据前是否判断了可以收发数据
- 对网络连接是否需要进行适当的限制,有些服务器软件会限制一个客户只能建立一个连接,防止某个用户占用过多资源而影响其他用户的网络速度
- 网络操作失败时的处理是否正确
- 当处理完一个连接中的所有请求时,是否在所有相应路径上将连接关闭掉了
- 通信是阻塞方式的还是异步方式的?如果是阻塞方式是否会导致程序一直等待
以上列出的代码检视要点只是部分建议,并非完整的内容,里面的内容细节由于牵涉到C/C++编程和软件设计等方面的知识,不属于本书的范畴,所以没有进行详细讲解。事实上要做一个完整的检查列表是一个不小的工作量,可能一本书也不一定能够将所有要检查的内容讲清楚,涉及的知识不仅限于C/C++语言方面的知识,还包括安全性编程、防御性编程、软件设计、各种专业知识、业务知识等方面的内容。因此要提高检视水平的话还需要多加强专业知识和业务知识的学习。
另外一个要说明的问题是,不要期望通过检视能发现绝大部分的缺陷,事实上检视发现的问题数量并不比测试发现的多,但是检视的好处在于有很多通过检视发现的问题是无法通过测试来发现或很难通过测试来发现的,并且有些问题通过检视来发现的代价远低于测试。
|