C++代码检查方法
译自 An Abbreviated C++ Code Inspection Checklist
John T. Baldwin October 27, 1992
by realfun, 2003-7-20
(sloc/hr : source lines of code per hour,每小时代码行数)
一、 2-5名检查人员,不包括作者
二、 分块打印代码,一定要打印行数,每人次不超过250行代码,包括注释,不包括空行
。
三、 概观:代码作者花上20-40分钟解释代码的编排。检查人员不得问问题(从代码中找答案)代码作者最好拣精要的地方说,时间尽量少。
四、 单独检查:每一名检查者用一个用于检查的列表来促进检查。这个过程应当单独进行。每小时大约70-120行,不要太快,也不要太慢。(统计发现122行以上效果就会迅速降低)一行行读代码,尽量理解它。对每行、每块代码都用检查列表来对照。对每个可行的问题,找到答案是“是”的地方,这基本上代表着一个错误,记下来。你会发现有些代码错误是句法级别的,有些则是理解级别的,做好经常切换注意力的思想准备。
五、 开会:对某一个模块执行检查的所有小组成员进行开会。如果作者出席,记住其目的是收集反馈信息,而不是“捍卫”、“解释”自己的代码。记住:一个主要的检查任务是保证代码的自解释性。 每个会议严格控制在2个小时(包括中场休息)。因为效率随时间加长而降低。尽量不要打断会议进程。一次会议中不同的检查者会涉及不同的代码区域。因此,单个会议上可以进行(5检查者)×(120sloc/hr)×(2hrs) = 1200行代码。实际上有些人员的代码会重复,检查量减小。如果两个小时还进行不完,结束会议。记录者应该提交当前存在的问题给作者或维护人员。残留问题放在下一次会议解决。
六、 重写:污点代码提交给作者或维护者来重写。可以改动代码、添加删除注释、改变?br> ?br> 构等等。但是,不能在会议上讨论解决方案(这样既不高效也没必要)。改动后,应该召集部分或所有检查人员开一个短会。会议由作者主持,他可以决定接不接受与会者的建议。
七、 选一个仲裁者,其责任是监视所有的污点代码的改正。其正确性鉴定可以在一个短?br> ?br> 上或者在后来代码检查过程中。
八、 记录:为了客观地跟踪污点代码的检测以及改正,会议上的记录非常必要。为了消?br> ?br> 记录被用来评价代码作者的可能性,可以不记录作者的名字和模块名称(用模块代号来代替)。文档在这段工作完成后销毁。
------------------------------
常用的检查用列表:
1. 变量声明:
1.1 数组
1.1.1 数组维度是否是写死在代码里的?
int intarray[13];
应为:
int intarray[TOT_MONTHS + 1];
...
private:
enum { MAX_FOO_BUFFERS = 40; }
...
}
用static可以限制一个类只有一个常量实例。
静态成员变量有个不足之处:不能用它来做静态数组的size。
1.3 标量变量
1.3.1 是否定义了一个有符号的变量,而它只在非负情况下有意义?
int age;
应为:
unsigned int age;
(注:此处我觉得不太合理,C++编译器一般不对unsigned 和 signed报警,如果这样用,不小心赋了一个-1给unsigned int, 会自动转化,变成一个很大正整数,掩盖了错误,不利于改正。 如果用int的话,还可以检测是否为负,使用断言assert也可以。realfun)
1.3.2 代码是否假设char施signed 或者是 unsigned?
typedef char SmallInt;
smallInt mumble = 254;
应为:
typedef signed char smallInt;
1.3.3 程序是否用了不必要的float或者double?
Double acct_balance;
应为
unsigned long acct_balance;
1.4 类
1.4.1 类是否有虚函数?如果有,是否是虚析构函数?
1.4.2 类是否有:拷贝构造函数、赋值操作符、析构函数?如果有,一般情况下要一起出现。
2. 数据使用
2.1 字符串
2.1.1 是否不以null(‘/0’)结尾?
2.1.2 是否将strXXX()一类的函数用于没有结尾的char array?
2.2 缓冲区
2.2.1 每次向缓冲区写入时是否总有一个大小检查?
2.2.2 缓冲区大小是否会不够用?
2.3 位域(bit field)
2.3.1 真的需要使用位域吗?
2.3.2 有没有顺序问题(考虑到代码移植性)?
3. 初始化
3.1 局部变量
3.1.1 局部变量使用前是否未经初始化?
3.1.2 C++是否初始化生成以后,再初始化?(浪费时间而且不清晰)
3.2 丢失重初始化
3.2.1 可否将一个变量的原来的值代入下一个运算(不重新初始化)?
4. 宏
4.1 如果宏变量被多次使用,变量是否有副作用?
例如:
#define max(a,b) ( (a) > (b) ? (a) : (b) )
max(i++, j); //i 被加了两次
4.2 宏里的表达式是否未加上括号?
例如:
#define max(a, b) (a) > (b) ? (a) : (b)
result = max(i, j) + 3;
被扩展为:result = (i) > (j) ? (i) : (j)+3;
4.3 宏中的变量是否未加上括号?
例如:
#define IsXBitSet(var) (var && bitmask)
result = IsXBitSet( i || j );
扩展为:result = (i || j && bitmask); //不是我们所期望的
应该定义宏为:#define IsXBitSet(var) ((var) && (bitmask))
5. 数据大小
5.1 调用函数时,缓冲区(参数)大小不等于size参数的值.
例如: memset(buffer1, 0, sizeof(buffer2);
5.2 sizeof的对象是否不正确?
sizeof(*ptr)还是sizeof(ptr)
sizeof(*array)还是 sizeof(array)
sizeof(array)还是sizeof(array[0]) //当用户需要元素大小时
6. 动态分配
6.1 分配
6.1.1 空间是否会不足?
6.1.2 分配者是否假定用户在另一个地方释放之?
(这样并不一定会错,但是应该纪录下来,包括这样做的理由。类的构造与析构函数要特别注 意.).
6.1.3 是否使用了malloc或者calloc, realloc,而不是C++推荐的new?
6.2 释放
6.2.1 数组是否使用了delete[]进行释放?
6.2.2 是否还有指针指向已经被释放的空间?
6.2.3 待释放空间是否已经被释放过(6.2.2遗留的问题)?
6.2.4 是否用delete释放了由malloc, calloc, realloc分配的空间?
6.2.5 是否用free释放了new分配的空间?
7. 指针
7.1 指针无效后,是否未赋值为NULL?
7.2 对指针做copy时,是否需要新分配空间?
8. 转换(cast)
8.1 NULL传递给函数作参数时是否被转换为正确的类型?(@@@@需要吗?)
8.2 代码是否依赖于隐式类型转换?
9. 计算
9.1 当测试赋值得结果时,是否按需要加上了括号?
例如: if( a = function()==0)
应为: if( (a = function()) == 0)
9.2 是否有些需要同步的数据并没有被更新?
10. 条件语句
10.1 是否使用==对一个浮点数进行判断?
如: if (someVar == 0.1),应该使用相减,然后>, >=, <, <=,
if (abs(someVar – 0.1) <= eps),eps很小,1e-6等。
10.2 是否用unsigned变量与0比较大小?
如:if (myUnsignedVar >= 0) //永远为true
10.3 是否用signed变量直接作为判断表达式?
如:if (mySignedVar) //不一定对,为负时可能期望它为false
如果这样,应该为: if (mySignedVar > 0),等等。
10.4 如果测试是用来检查错误, “错误情况”是否真的合理?
11. 流程控制
11.1 控制变量
11.1.1 下限是否是个exclusive的限制?
11.1.2 上限是否是个inclusive的限制?
例如:(0, 2]就是这样一种区间,上闭下开。但一般情况下推荐使用[0, 2),这样的上开下闭的区 区间。
11.2 分支
11.2.1 在switch时,是否有缺少break的case语句?
“从上面掉下”的用法只用在使事情变得简单而且清晰的地方。
11.2.2 switch是否缺少default分支?
即便不可能到达default分支,也要写上。有利于检查代码错误。
11.2.3 循环是否用一个bool型的变量作为结束标志goto出去?
一般应使用break.
12. 赋值
12.1 赋值操作符
12.1.1 是否在可用“a += b”的地方使用了 “a = a + b”?
12.1.2 赋值操作符或者拷贝构造函数参数是否为non-const的?
12.1.3 重载的赋值操作符是否做了自相等检测?
if (this == &right_hand_arg)
return *this;
12.2 使用赋值运算符
12.2.1 复制运算符能用初始化替代吗?
12.2.2 是否在表达式与变量之间有错配,如:需要element number,赋给的却是byte co
unt?
13. 参数传递
13.1 是否对非内部类型的参数使用了传值方式?
因该用reference或者const reference
14. 返回值
14.1 返回值是否赋给一个精度不够的变量?
注:指的是getchar返回int问题
int getchar(void);
char chr;
while ( (chr = getchar()) != EOF ) {
...
};
14.2 是否public成员函数返回一个指向成员变量(或者是外部变量)的non-const reference或者是 non-const pointer?
这样就会将受保护的变量暴露。
14.3 是否有函数返回一个引用,而实际上应该返回一个对象?
如:操作符+-*/的重载。
14.4 是否在应该返回const reference的地方返回了值?
15. 函数调用
15.1 参数个数不定的函数
15.1.1 fprintf的FILE参数丢失?
15.1.2 是否有多余的参数?
15.1.3 参数类型是否显式地匹配格式串中的参数?
指的是printf(”%d”, a_long_int); 其实应该用%ld;
15.2 通用函数
15.2.1 函数调用正确吗?是否调错了函数?(如:strchr与strrchr)
15.2.2 函数调用是否违反了该函数需要的先决条件?
16. 文件
16.1 临时文件名是否为唯一的?(惊奇的是:这个问题居然是常见的bug)
16.2 文件指针在打开之前是否未关闭前一个打开的文件?
17. 隐式类型转换带来的问题
class String {
public:
String( char *arg ); // copy constructor
// ...
};
void foo( const String& aString );
class Word {
public:
Word( char *arg ); // copy constructor
// ...
};
void foo( const Word& aWord );
void gorp()
{
foo("hello"); // This used to work!
// Now it breaks! What gives?
}
会报错,因为有两个函数可以与foo("hello")的调用可以匹配:
void foo( const String& );
void foo( const Word& );
1.1.2 数组维度是否等于元素总数?
char entry[TOTAL_ENTRIES];
应为
char entry[LAST_ENTRY + 1];
这个例子中第一种代码很容易造成“差一”错误。
1.2 常量
1.2.1 变量的值是否从来都不改动?
int monthes_in_year = 12;
应为
const unsigned months_in_year = 12;
1.2.2 是否有些常量以#define方式定义?
#define MAX_FILES 20
应为:
const unsigned MAX_FILES = 20;
1.2.3 常量是否只用于很少的几个(甚至一个)类?如果是,常量是否全局的?
const unsigned MAX_FOOS = 1000;
const unsigned MAX_FOO_BUFFERS = 40;
应为
class foo {
public:
enum { MAX_INSTANCES = 1000; }