【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 所指向的函数返回值为浮点类型。

一旦我们知道如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到:只需要把声明中的变量名和声明末尾的分号去掉,再用括号整体括起来

比如:

1float (*h)(); 2 3(float (*)())p;

假定变量 fp 是一个函数指针,那么如何调用 fp 所指向的函数呢?调用方法如下:

(*fp)(); 

*fp 就是该指针所指向的函数。ANSI C 标准允许将上式简写为:

fp(); 

但是要记住这是一种简写方法。

注意:(*fp)()*fp()的含义完全不同,不要省略 *fp 两侧的分号。

现在我们声明一个返回值为 void 类型的函数指针:

void (*fp)(); 

如果我们现在要调用存储位置为 0 的子例程,我们是否可以这样写:

(*0)(); 

上式并不能生效,因为运算符 * 需要一个函数指针作为操作数。我们需要对 0 进行类型转换:

(* (void (*)())0 )(); 

我们可以使用 typedef来使表述更加清晰:

1typedef void (*funcptr)(); 2(*(funcptr)0)();

1. 运算符优先级问题

1if(FLAG & flags != 0){ 2 ... 3}

FLAG 是一个已经定义的常量,FLAG 是一个整数,该数的二进制表示中只有某一位是 1,其余的位都为 0 ,也就是 2 的某次幂。为了判断整数 flags 的某一位是否也是 1,并且将结果与 0 作比较,我们写出了上面 if 的判断表达式。

但是!=的优先级高于&,上面的式子被解释为:

1if(FLAG & (flags != 0)){ 2 ... 3}

这显然不是我们想要的。

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; 

下面我们说几个比较常见的运算符的用法:

  • a.b.c的含义是(a.b).c而不是a.(b.c)

  • 函数指针要写成:(*p)(),如果写成了*p(),编译器会解释为:*(p())

  • *p++会解释为:*(p++)而不是(*p)++

  • 记住两点:

    • 任何一个逻辑运算符的优先级低于任何一个关系运算符。
    • 移位运算符的优先级比算数运算符要低,但是高于关系运算符。
  • 赋值运算符结合方式从右到左,因此:

    a = b = 0; 
    

    等价于:

    1b = 0; 2a = b;
  • 关于涉及赋值运算时优先级的混淆:

    复制一个文件到另一个文件中:

    1while(c = getc(in) != EOF) 2 putc(c, out);

    但是上式被解释为:

    1while(c = (getc(in) != EOF)) 2 putc(c, out);

    关系运算符的结果只有 0 或 1 两种可能。最后得到的文件副本中只包含了一组二进制为 1 的字节流。

2. 注意作为语句结束标志的分号

考虑下面的例子:

1if(x[i] > big); 2 big = x[i];

这与:

1if(x[i] > big) 2 big = x[i];

大不相同。

前面的例子相当于:

1if(x[i] > big) {} 2 big = x[i];

无论 x[i] 是否大于 big,赋值都会被执行。

如果不是多写了分号,而是遗漏了分号,一样会招致麻烦:

1if( n < 3) 2 return 3logrec.date = x[0]; 4logrec.time = x[1]; 5logrec.code = x[2];

遗漏了 return 后的分号,这段程序仍然会顺利通过编译而不会报错,它等价于:

1if( n < 3) 2 return logrec.date = x[0]; 3logrec.time = x[1]; 4logrec.code = x[2];

还有一种情形,也是有分号与没有分号实际效果相差极为不同。那就是当一个声明的结尾紧跟一个函数定义时,如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型。考虑下例:

1struct logrec{ 2 int date; 3 int time; 4 int code; 5} 6main(){ 7 8}

上面代码段的实际效果是声明函数 main 返回值是结构 logrec 类型。

如果分号没有被省略,函数 main 的返回值类型会缺省定义为 int 类型。

3. switch 语句

1switch(color){ 2 case 1: printf("red"); 3 break; 4 case 2: printf("blue"); 5 break; 6 case 3: printf("yellow"); 7 break; 8}

如果稍作改动:

1switch(color){ 2 case 1: printf("red"); 3 case 2: printf("blue"); 4 case 3: printf("yellow"); 5}

假定 color 的值为 2,那么将会输出:

blueyellow 

因为程序的控制流程在执行了第二个 printf 函数的调用后,会自然地顺序执行下去。第三个 printf 函数也会被调用。

switch 的这种特性,即使它的弱点,也是它的优势所在。

对于两个操作数的加减运算,我们可以将操作数变号来取代减法:

1case SUBTRACT: 2 opnd2 = -opnd2; 3case ADD: 4 ...

在这里,我们是有意省略 break 语句。

4. 函数调用

C 语言要求:在函数调用时,即使函数不带参数,也应该包含参数列表。如果,f 是一个函数:

f(); 

是一个函数调用语句,而:

f; 

却是一个什么也不作的语句,f 表示函数的地址。

5. 悬挂 else 引发的问题

这个相信大家学习 C 的时候老师都会讲,不懂可以去参考相关。

这里说一点,写 if 语句时,不要省略括号是一种可以学习的习惯。

参考资料《C 缺陷与陷阱》


以上就是本次的内容,感谢观看。

如果文章有错误欢迎指正和补充,感谢!

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

C# Aspose.Cells导出xlsx格式Excel,打开文件报“Excel 已完成文件级验证和修复。此工作簿的某些部分可能已被修复或丢弃”

报错信息:最近打开下载的Excel,会报如下错误。(xls格式不受影响)!(https://oscimg.oschina.net/oscnet/2b6f0c8d7f97368d095d9f0c96bcb36d410.png)!(https://oscimg.oschina.net/oscnet/fe1a8000d00cec3c

Linux查看GPU信息和使用情况

1、Linux查看显卡信息:lspci|grepivga2、使用nvidiaGPU可以:lspci|grepinvidia!(https://oscimg.oschina.net/oscnet/36e7c7382fa9fe49068e7e5f8825bc67a17.png)前边的序号"00:0f.0"是显卡的代

KVM调整cpu和内存

一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid