【C 陷阱与缺陷】(二)语法陷阱
码字不易,对你有帮助 点赞/转发/关注 支持一下作者
微信搜公众号:不会编程的程序圆
看更多干货,获取第一时间更新
零
0. 理解函数声明
请思考下面语句的含义:
(*(void(*)())0)()
前面我们说过 C 语言的声明包含两个部分:类型和类似表达式的声明符。
最简单的声明符就是单个变量:
float f, g;
由于声明符和表达式的相似,我们可以在声明符中任意使用括号:
float ((f));
这个声明的含义是:当对 f 求值时,((f))
的类型为 float 类型,可以推知 f
也是浮点类型。
同样的,我们可以声明函数:
float ff();
这个声明的含义是:表达式 ff()
求值结果是 float 类型,也就是返回 float 类型的函数。
类似的:
float *pf;
这个声明的含义是:*pf
是一个 float 类型的数,也就是说 pf 是指向 float 类型的指针。
以上的声明可以结合起来:
float *g(), (*h)();
*g()
和(*h)()
是浮点表达式。因为()
(和[]
)的优先级高于*
。*g()
也就是*(g())
:g 是一个函数,该函数返回一个指向浮点数的指针。同理,可以得到 h 是一个函数指针,h 所指向的函数返回值为浮点类型。
一旦我们知道如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到:只需要把声明中的变量名和声明末尾的分号去掉,再用括号整体括起来。
比如:
float (*h)();
(float (*)())p;
假定变量 fp 是一个函数指针,那么如何调用 fp 所指向的函数呢?调用方法如下:
(*fp)();
*fp 就是该指针所指向的函数。ANSI C 标准允许将上式简写为:
fp();
但是要记住这是一种简写方法。
注意:(*fp)()
和*fp()
的含义完全不同,不要省略 *fp 两侧的分号。
现在我们声明一个返回值为 void 类型的函数指针:
void (*fp)();
如果我们现在要调用存储位置为 0 的子例程,我们是否可以这样写:
(*0)();
上式并不能生效,因为运算符 * 需要一个函数指针作为操作数。我们需要对 0 进行类型转换:
(* (void (*)())0 )();
我们可以使用 typedef
来使表述更加清晰:
typedef void (*funcptr)();
(*(funcptr)0)();
1. 运算符优先级问题
if(FLAG & flags != 0){
...
}
FLAG 是一个已经定义的常量,FLAG 是一个整数,该数的二进制表示中只有某一位是 1,其余的位都为 0 ,也就是 2 的某次幂。为了判断整数 flags 的某一位是否也是 1,并且将结果与 0 作比较,我们写出了上面 if 的判断表达式。
但是!=
的优先级高于&
,上面的式子被解释为:
if(FLAG & (flags != 0)){
...
}
这显然不是我们想要的。
high 和 low 是两个 0 ~ 15 的数,r 是一个八位整数,且 r 的低 4 位与 low 一致,高 4 位与 high 一致,很自然想到:
r = high<<4 + low;
但是,加法的优先级高于移位运算,本例相当于:
r = high<<(4 + low);
对于这种情况,有两种更正方法:
r = (high<<4) + low;
或利用移位运算的优先级高于逻辑运算:
r = high<<4 | low;
image
下面我们说几个比较常见的运算符的用法:
-
a.b.c
的含义是(a.b).c
而不是a.(b.c)
-
函数指针要写成:
(*p)()
,如果写成了*p()
,编译器会解释为:*(p())
-
*p++
会解释为:*(p++)
而不是(*p)++
-
记住两点:
- 任何一个逻辑运算符的优先级低于任何一个关系运算符。
- 移位运算符的优先级比算数运算符要低,但是高于关系运算符。
-
赋值运算符结合方式从右到左,因此:
a = b = 0;
等价于:
b = 0; a = b;
-
关于涉及赋值运算时优先级的混淆:
复制一个文件到另一个文件中:
while(c = getc(in) != EOF) putc(c, out);
但是上式被解释为:
while(c = (getc(in) != EOF)) putc(c, out);
关系运算符的结果只有 0 或 1 两种可能。最后得到的文件副本中只包含了一组二进制为 1 的字节流。
2. 注意作为语句结束标志的分号
考虑下面的例子:
if(x[i] > big);
big = x[i];
这与:
if(x[i] > big)
big = x[i];
大不相同。
前面的例子相当于:
if(x[i] > big) {}
big = x[i];
无论 x[i] 是否大于 big,赋值都会被执行。
如果不是多写了分号,而是遗漏了分号,一样会招致麻烦:
if( n < 3)
return
logrec.date = x[0];
logrec.time = x[1];
logrec.code = x[2];
遗漏了 return 后的分号,这段程序仍然会顺利通过编译而不会报错,它等价于:
if( n < 3)
return logrec.date = x[0];
logrec.time = x[1];
logrec.code = x[2];
还有一种情形,也是有分号与没有分号实际效果相差极为不同。那就是当一个声明的结尾紧跟一个函数定义时,如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型。考虑下例:
struct logrec{
int date;
int time;
int code;
}
main(){
}
上面代码段的实际效果是声明函数 main 返回值是结构 logrec 类型。
如果分号没有被省略,函数 main 的返回值类型会缺省定义为 int 类型。
3. switch 语句
switch(color){
case 1: printf("red");
break;
case 2: printf("blue");
break;
case 3: printf("yellow");
break;
}
如果稍作改动:
switch(color){
case 1: printf("red");
case 2: printf("blue");
case 3: printf("yellow");
}
假定 color 的值为 2,那么将会输出:
blueyellow
因为程序的控制流程在执行了第二个 printf 函数的调用后,会自然地顺序执行下去。第三个 printf 函数也会被调用。
switch 的这种特性,即使它的弱点,也是它的优势所在。
对于两个操作数的加减运算,我们可以将操作数变号来取代减法:
case SUBTRACT:
opnd2 = -opnd2;
case ADD:
...
在这里,我们是有意省略 break 语句。
4. 函数调用
C 语言要求:在函数调用时,即使函数不带参数,也应该包含参数列表。如果,f 是一个函数:
f();
是一个函数调用语句,而:
f;
却是一个什么也不作的语句,f 表示函数的地址。
5. 悬挂 else 引发的问题
这个相信大家学习 C 的时候老师都会讲,在我的 【C 必知必会】系列教程中也有详细讲解,不懂可以去参考相关。
这里说一点,写 if 语句时,不要省略括号是一种可以学习的习惯。
参考资料:《C 缺陷与陷阱》
以上就是本次的内容,感谢观看。
如果文章有错误欢迎指正和补充,感谢!
最后,如果你还有什么问题或者想知道到的,可以在评论区告诉我呦,我在后面的文章可以加上。
最后,关注我,看更多干货!
我是程序圆,我们下次再见。