Style
引言
代码风格是十分重要的,一个好的代码风格可以帮助读者更好地理解代码,也能帮助自己在写程序时及时排错。
Names
Use descriptive names for globals, short names for locals.
学习第一门程序语言时,我们总被教导道:要使用足够长的变量命名,这样读者才好理解这个变量的用途。其实这样是不对的。
全局变量的名字是需要足够长、足够具体的,因为他们可能出现在程序的各个地方,这样的名字可以帮助作者和读者回想起这个变量的用途。而局部变量简短些即可,比如要表示学生的人数,n
也许就够了,n_stu
也不错,但是number_of_stu
就没必要,会显得代码冗长。
Use active names for functions.
函数名应该是动态的。比如要打印数组,print_arr
就很好,而单独一个arr
就让人不知道该函数的用途。名字中的动词也需要足够直白,比如返回布尔值的函数,check_even
是模棱两可的,因为它并没有说明如果是偶数返回真还是假,而is_even
就让人清楚地知道如果是偶数,该函数返回真。
Expressions and Statements
Parenthesize to reslove ambiguity.
括号可以清晰的划分结构与优先级,即使不需要的时候我们也可以加上以避免不必要的错误和增加可读性。
比如if (x&MASK == BITS)
,显然其本意是想判断x
和掩码进行与运算后是否等于BITS
,但由于位操作符的优先级低于等于号,这段代码实际上等价于if (x & (MASK == BITS))
,这也就发生了意料之外的错误,因此这个表达式需要加上括号,即if ((x&MASK) == BITS)
。
又如这样一个表达式:
leap_year = y % 4 == 0 && y % 100 != || y % 400 == 0;
这当然是没错的,但第一眼看去,我们难以把握结构,若按如下修改会清晰得多:
leap_year = ((y%4 == 0) && (y%100 != 0) || (y%400 == 0));
注意到,我们移去了一些空格,使高优先级的操作符更紧凑,这样可以帮助读者更快地把握结构。
Be careful with side effects.
不了解序列点(sequence point)、副作用(side effect)和未定义行为(undefined behavior)的朋友可先参见:Undefined behavior and sequence points
在C和C++中,一个序列点前副作用的执行顺序是未定义的。如:
str[i++] = str[i++] = ' ';
在这里,
;
就是一个序列点,而我们知道,++
操作符是有副作用的,它不仅返回一个值,还会修改该变量的值。所以在这一语句中对同一个变量多次++
操作导致了未定义行为。因为虽然我们心里想的是i
增加两次,但编译器不知道到底i
是增加一次还是两次,也不知道是左边的i
是使用右边的i
递增后的值还是原来的值,这就导致了warning: multiple unsequenced modification to i
。再看一个我从书本Exercise 1-5改编的例子,以下代码片段有什么问题呢?
int read(int *ip) {
scanf("%d", ip);
return *ip;
}
void print(int a, int b) { printf("%d %d\n", a, b); }
int main() {
int a, b;
print(read(&a), read(&b));
return 0;
}
我们可以先看看当输入为
2 3
时,不同编译器下的输出结果:编译器 | 输出结果 |
---|---|
gcc | 3 2 |
clang | 2 3 |
scanf
函数不是分别运行的吗?为什么会产生这样的奇怪的结果呢?原因是函数调用计算完所有参数后且执行被调用函数前也是一个序列点,而我们这里的函数参数计算里含有I/O操作,I/O操作也有副作用,所以这里函数参数的计算顺序也是未定义的,因此如果先计算第一个参数,那么2
就会被写入参数a
中,3
就会被写入b
中,输出结果同clang;但如果先计算第二个参数,2
和3
就会被分别写入b
和a
中,输出结果同gcc。
Consistency and Idioms
在这一节中,基本都是熟知的准则,不再赘述。但下面这个代码示例告诉了我们一种优雅处理嵌套的if else
错误判断语句的方式。
我们很可能写出类似这样的代码:
if (argc == 3)
if ((fin = fopen(argv[1], "r")) != NULL)
if ((fout = fopen(argv[2], "w")) != NULL) {
while ((c = getc(fin)) != EOF)
putc(c,fout);
fclose(fin);
fclose(fout);
} else
printf("Can't open input file %s\n", argv[2]);
else
printf("Can't open input file %s\n", argv[1]);
else
printf("Usage: cp inputfile outputfile\n");
使用递进的if
判断,最终是前期准备无错误的处理代码,然后再用多个else
分别处理各种错误情况。这段代码并不优雅,并且有资源泄露的可能——如果fout
打开文件失败,那么fin
打开的文件就不会显式关闭,未显式关闭文件可能导致的问题可参见:Why do we need to close files?
那么如何改进呢?我们可以使用if, else if
来推进,先处理错误情况,最终才是正常情况下的代码。
if (argc != 3)
printf("Usage: cp inputfile outputfile\n");
else if ((fin = fopen(argv[1], "r")) != NULL)
printf("Can't open input file %s\n", argv[1]);
else if ((fout = fopen(argv[2], "w")) != NULL) {
printf("Can't open input file %s\n", argv[2]);
fclose(fin); /* release the resource fin occupied */
} else {
while ((c = getc(fin)) != EOF)
putc(c,fout);
fclose(fin);
fclose(fout);
}
Function Macros
Avoid function macros.
宏是进行文本的替换,因此如果函数宏定义中参数出现了多次,那么它就会被计算多次,这在大多数时候都不是我们想要的。如:
#define isupper(c) ((c) >= 'A' && (c) <= 'Z')
...
if (isupper(c = getchar()))
printf("%c is uppercase\n", c);
那么实际
if
的判断语句等价于if (((c = getchar()) >= 'A' && (c = getchar()) <= 'Z'))
这显然是错误的,假定我们输入一个字符然后回车,那么只要这个字符的ASCII码值大于等于A的码值就会有输出,因为逻辑与表达式的第二部分程序又读了一个字符,这个字符就是留在缓冲区的
\n
字符,其ASCII码值为0xa
,小于Z
的码值。很多人习惯使用宏来管理幻数/魔数(Magic Numbers),其实尽量不要用C预处理器来做这件事,让语言本身来处理是更好的。在C++中可以用const
来声明常量:
const int ARR_SIZE = 5;
C中当然也有
const
,值得注意的是,用const
修饰的值是无法作为静态数组
的边界的(因为这是运行时常量
),但enum
可以实现这一点:enum {ARR_SIZE = 5};
那这是不是意味着我们永远不能使用宏呢?并不是,我们应该让宏做函数做不了的事情。典型的例子就是仅由声明计算数组的长度:
#define NELEMS(array) (sizeof(array) / sizeof(array[0]))
在这里,
sizeof(array)
计算的就是数组本身的长度,而在函数中,数组参数退化为指针,sizeof(array)
就只能计算出指针的大小了。对数组和指针关系不太明确的朋友可参考:is an array name a pointer?Comments
Comment functions and global data.
给每个函数和全局变量写适当的注释是一个好习惯。在我接触的好的代码风格中,也有的还在文件开头写一个文件的注释,告诉读者这一个文件的主要用途以及注意事项等,个人认为这也值得学习。
Don’t comment bad code, rewrite it.
这一准则给出了一个有意思的判断,即当注释的字数多过于要注释的代码段时,这段代码可能就是bad code
。这时应该看看是否需要改善代码而不是坚持注释。
至此,第一章就结束了,希望我们都能形成一个好的代码风格。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!