不合格程序
- 错误的浮点数比较用法。
- 指针退化仍使用 sizeof。
- 数组越界操作。
==
写成=
或!=
写成=
。- 宏函数没有足够的括号来预防边界(表达式参数)。
小屁孩程序(需要为其擦屁股,不可用代码)
- 声明变量没有赋初值。
- 函数参数使用前没有判断特殊情况。
- 编写函数在需要返回不同的错误情况时没有返回错误(或者全部使用 -1 表示)。
- 函数调用没有错误返回的判断。
- 理解不完全的错误返回判断(想当然地自己定义错误消息) - 不懂得使用
perror
。 - 重复使用的字符串数组没有
memset
。 - 文件描述符泄漏。
- 使用了内含
malloc
(calloc
) 的 API,没有调用相应 API 释放。 - 循环语句边界条件错误。
- switch-case 没有 break;确定不用 break 的位置需加
/* FALL THROUGH */
注释。 fclose(野指针);
、fclose(NULL);
;free(野指针);
、free(NULL);
发生的情况没有考虑(释放资源后没有对变量“重置”)。- 想当然地使用自我命名地缩写 - 无通用缩写单词使用全单词,或者保留发音字符后长度需大于原本 50%。
- 不适宜的代码风格。
- 有限定范围值的参数,没有对其做判断。
代码风格,错误预防
- 在 if 语句成立情况下必定会执行
break
,continue
,exit
,return
,goto
的代码中勿用if ... else ...
(无需else
)。 - 变量在使用的位置声明与赋值,勿全部集中在函数起始部分。
- 长上下文变量命名使用有含义的名称 - 可将可用于该变量注释内容用下划线连接后作为变量名。
- 函数名尽可能提供足够多的信息。
- 不应该惧怕编写函数,而在函数内直接添加 bug fix 代码,直接添加 feature 代码。
- 有必要就、尽可能地使用
enum
定义函数返回。 - 尽可能做显式条件判断(不要使用
if (!<condition>)
这种写法)。 - 多使用
struct
组织变量,为struct
添加void (*to_string)(struct name_s *me, char dst[])
函数,尽可能为struct
编写构造函数。 - 写足够多的代码以形成确定的个人代码风格 - 公司内尽最大可能使用约定风格。
- 必要的注释。
- 不要滥用短路逻辑;
- 不要在判断条件中做
fp = fopen(file, "r")
打开文件并做相关判断。 - 不要在判断条件中做
malloc
并做相关判断。
- 不要在判断条件中做
- 尽可能使用 const, static, extern 限定条件。
- 弱类型语言 - 不要隐式变量类型转换;尽可能显式类型转换,对于无法做到显式的位置要做注释。
bug 实例收集 - N/A
for 循环 - N/A
声明变量没有赋初值
声明变量没有赋初值有多糟糕?下面是一个典型的代码:
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
typedef enum {FALSE=0, TRUE} Boolean;
void print_date();
void get_date(char *);
Boolean chr2bool(char c) {
if ((c - '0') == 0)
return FALSE;
else if ((c - '0') == 1)
return TRUE;
else {
printf("ValueError: can't convert '%c' to Boolean", c);
exit(1);
}
}
int main()
{
while (TRUE) {
usleep(500000);
print_date();
}
return 0;
}
void print_date() {
char data[256];
get_date(data);
printf("data: %s\n", data);
return;
}
void get_date(char *data) {
Boolean flag = FALSE;
char enter = '\0';
printf("print date?(0 or 1, 'q' to quit)>");
scanf("%c", &enter);
enter == 'q' ? exit(0) : (flag = chr2bool(enter));
if (flag == FALSE) // simulate call ioctl, but no clients -- not write :data:.
return;
FILE *fp = NULL;
fp = popen("date +\"%Y-%m-%dT%H:%M:%S:%N\"", "r");
if (fp == NULL) {
perror("popen: ");
exit(errno);
}
char tmp[256] = {'\0'};
fgets(tmp, 256, fp);
memcpy(data, tmp, sizeof(char) * 256);
pclose(fp);
return;
}
保存为 memset.c 文件,运行方式如:
$ gcc memset.c && echo "1110001000101000q" | ./a.out
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:04:290045662
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:04:793379343
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:05:297036989
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:05:297036989
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:05:297036989
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:05:297036989
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:07:301353222
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:07:301353222
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:07:301353222
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:07:301353222
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:09:305473044
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:09:305473044
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:10:309085615
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:10:309085615
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:10:309085615
print date?(0 or 1, 'q' to quit)>data: 2019-09-12T16:12:10:309085615
print date?(0 or 1, 'q' to quit)>
$
如果你读懂了上面的代码,就会发现代码中的 data
变量在通过 get_date(data)
的时候,如果输入是 0
,那么 get_date
这个函数不会去修改 data 变量的内容。
我们知道没有初始化的变量是随机值,内存中是什么样它就是什么样。
甚至某种程度上可以不正确地认为它是空值(
0
)。
但是在上面这个例子中完全行不通,因为程序重复调用 print_date
函数(而不是执行一个程序调用一次 print_date
函数),所以看上去在 print_date 函数内部,data
每次都有“重新”声明,定义。但是实际上在程序运行的时候,它会重复地使用同一块内存地址,使用地还是上一次调用 print_date
时留下来的值(是的,在特定情况下,函数内没有附初值的变量行为上等同于加上了 static
关键字)。
具体在实际软件中的表现这里就不展开了,上面运行程序的输出,那样的结果就已经足够骇人听闻了!因为那明显不是正确的结果,这种情况一是不容易发生,它一般是在特定条件下发生,但是越是这种在特定条件下发生的 bug,越是令人头疼;二是在代码很大,嵌套很深的情况下,可能不是那么容易联想到原因是这样子的,而找出问题来就很麻烦。所以变量要赋初始值这一点一定要养成一个习惯!
变量没有赋初值的问题不仅仅是上面的那样情况,很有其它的表现,比如声明了指针,但是偏偏在某些特殊情况下没有用到这个指针变量,那么比如:
- 文件指针:
FILE *fp; ...codes... if (fp != NULL) fclose(fp);
- malloc 内存指针:
void *buf; ...codes... if (some-condition) { buf = malloc(SIZE); ...codes... } if (buf != NULL) free(buf);
文件指针如果是野指针(声明之后没有赋值,马上就是野指针),调用 fclose
关闭这个野指针,马上进程就会异常退出!
调用 free
野指针也一样。所以声明变量马上赋初值百利无一害!