前言
本文主要参考Andrew Koenig 的《C Traps and Pitfalls》,重新编排整理,归纳总结了C语言编程中常见的问题点,并提出了一些简单的意见和建议,以期读者阅读本文后,能够在编程实践中做出预防性的设计,减少因语言理解不到位而产生的Bug。让我们站在巨人的肩膀上前进,而不是陷在巨人的脚印里挣扎,愿与诸君共勉。
目录
前言
一、相似符号
二、贪心法
三、八进制
四、字符与字符串
五、优先级与结合性
参考资料
一、相似符号
在C语言中有几对长得很相似符号,如:= 和 ==、| 和 ||、& 和 &&,其潜在的问题在于,程序员很容易无意中将这些符号误写成对方的模样,而且大多数情况是无法通过编译报错发现的。
代码1.1 误写判等符
int x = 2;
int y = 3;
bool func(int x, int y)
{
bool res = false;
if (x = y) {
res = true;
}
return res;
}
可以看出上边这段代码的原意是要在参数相等的时候返回真,但由于将 == 误写了 =,实际就成了判断y是不是0。现在的编译器一般可以识别出判断表达式中 = 和 == 的区别,并通过告警信息显示出来,但如果考虑兼容编译器版本,则还是应改做一些规定和处理:
- 判断表达式中不要进行赋值操作
- 变量和常量判等时,常量放在符号左边
- 两个变量判等时,生命周期长的变量放在符号右边
扩展的讲,最好不要在判断表达式中进行带有“副作用”的操作,比如自增、自减甚至是运算赋值,这会增加程序理解的复杂度,同时很容易引入问题。
常量放在 == 左边可以避免误写为 = 的情况,此时编译器一定会报错。不同于 > 和 < 判断,判等符的左、右值是什么,并不会影响程序的阅读。
生命周期长的变量放在 == 右边,是考虑在误写时降低影响面,便于定位问题;但有些情形下,也可能会掩盖问题的发现,因此还要结合实际业务场景,区别对待。
| 和 ||、& 和 && 与 = 和 == 的问题大致相同。要想避免误写,除了仔细仔细再仔细外,可以做的一点是:
- 用 or 替换 ||
- 用 and 替换 &&
如同用 uint 替换 unsigned int 一样,借鉴其他语言,使用文本替换,降低误写的概率。
另外,/ 和 // 在形式上似乎和上述几种情形相似,但实际上却有一个明显的差别,即误写时很容易在编译阶段发现,因而不再作进一步讨论。
二、贪心法
C编译器读取多字符符号(//、; return res; }
代码2.2展示了一种准二义性的语句,其原意是要求商。但由于编译器识别到第一个 / 后,会根据贪心法继续向后读取,一直到最后一个 / 才结束,对编译器而言就变成了返回x的值。针对这种情况:
- 运算符号左右需要各空一个空格
- 使用括号明确运算优先级
在运算符左右各空一个空格,除了使代码美观、易读之外,还具有分割字符的作用,如将代码2.2改为代码2.3的形式,语义就正确了。
代码2.3
int func(int x, int *p)
{
if (*p == null) {
return -1;
}
int res = x / *p ;
return res;
}
使用括号显式的标明运算符的优先级,而不过度依赖运算符自身的结合性和优先级,可以减少错误,并且易于阅读,将代码2.2之改为代码2.4的形式,语义也能正确。
代码2.4
int func(int x, int *p)
{
if (*p == null) {
return -1;
}
int res = x/(*p) ;
return res;
}
针对代码2.2这个具体的例子,使用带有主题颜色区分功能的代码编辑器也能明显的发现问题,当然也还可以有另外一些规则,如:
- 注释只能加在语句上边或右边
三、八进制
在C语言中整型常量有八进制、十进制和十六进制三种表示方法,为了上下文对齐会无意中将十进制的数改成八进制形式,如:
代码3.1
int array[] = {
123,
125,
024,
};
其中024是八进制数,对应十进制的值是20,这种情况下一般可以采用空格对齐。
四、字符与字符串
在C语言中,用单引号引起来的一个字符实际代表一个整数,而用双引号引起来的字符串,代表的是一个指向无名数组起始字符的指针。两者混用,会引发一些显式的错误,如:
代码4.1
char *buf = '3';
此时编译器会报错,因为 '3' 是一个整数;而 "3" 就是正确的,它代表的是一个包含结束符的字符串常量。
五、优先级与结合性
附上C语言运算符优先级与结合性明细表,以备查览。
表5.1 运算符优先级与结合性
| 运算符 | 名称或含义 | 优先级 | 结合性 |
| [] | 数组下标 | 1 | 从左到右 |
| () | 圆括号 | ||
| . | 成员运算符 | ||
| -> | 成员运算符 | ||
| - | 负号 | 2 | 从右到左 |
| ~ | 按位取反 | ||
| ++ | 自增 | ||
| -- | 自减 | ||
| * | 取值 | ||
| & | 取地址 | ||
| ! | 逻辑非 | ||
| (类型) | 强制类型转换 | ||
| sizeof | 获取字节大小 | ||
| / | 除 | 3 | 从左到右 |
| * | 乘 | ||
| % | 取余 | ||
| + | 加 | 4 | 从左到右 |
| - | 减 | ||
| << | 左移 | 5 | 从左到右 |
| >> | 右移 | ||
| > | 大于 | 6 | 从左到右 |
| >= | 大于等于 | ||
| < | 小于 | ||
| <= | 小于等于 | ||
| == | 等于 | 7 | 从左到右 |
| != | 不等于 | ||
| & | 按位与 | 8 | 从左到右 |
| ^ | 按位异或 | 9 | 从左到右 |
| | | 按位或 | 10 | 从左到右 |
| && | 逻辑与 | 11 | 从左到右 |
| || | 逻辑或 | 12 | 从左到右 |
| ?: | 条件运算符 | 13 | 从右到左 |
| = | 赋值 | 14 | 从右到左 |
| /= | 除后赋值 | ||
| *= | 乘后赋值 | ||
| %= | 取余后赋值 | ||
| += | 加后赋值 | ||
| -= | 减后赋值 | ||
| <<= | 左移后赋值 | ||
| >>= | 右移后赋值 | ||
| &= | 按位与后赋值 | ||
| ^= | 按位异或后赋值 | ||
| |= | 按位或后赋值 | ||
| , | 逗号运算符 | 15 | 从左到右 |



