《C陷阱与缺陷》——第二章(语法陷阱)

x33g5p2x  于2021-11-21 转载在 其他  
字(3.0k)|赞(0)|评价(0)|浏览(227)

语法问题不是语法错误,因此编译器不会将以下问题进行报错,但是这些问题又会对程序员预料的结果与实际结果又会有出入,会造成极难发现的bug。在写代码时应该时刻小心。

索引
《C陷阱与缺陷》——第一章(词法陷阱)

1.理解函数声明

在《C陷阱与缺陷》中,介绍到了这样一个例子:(*(void(*)())0)();,这样的一个函数声明如何去理解呢?

解释与分析:首先我们一定要学会如何观察函数指针的类型,例如:void( * pf)(int,int),pf是该函数指针的名字,去掉名字,就是该函数指针的类型。因此去掉名字剩下的就是void( * )(int,int),它的意思指该指针存放的是返回类型为void,而形参中两个参数均为int类型。由此我们可以得出上面的例子中void( * )()是函数指针,存放的是返回值为void型,无形参的函数的地址。

而该函数指针在0前又有一个括号,指的是将0(为int类型)强制转换为函数指针类型,在最前面有一颗*,在指针中*代表的是解引用操作,即将指针存放的地址中的内容“拿”出来,并且如果要调用函数指针时,例如上述的函数指针,可以(*pf)(2,3)来调用函数,当然这只是便于理解,完全可以写为pf(2,3),它们的运行结果是一致的。

因此书中举的例子就是将常数0强制转换为返回类型为void型,无形参的函数指针类型,并且调用这个函数指针,不设置传入的实参。

我相信对上面的例子理解地非常透彻,对void(*signal(int,void(*)(int)))(int);也是迎刃而解了。

2.运算符优先级问题

《C陷阱与缺陷》中举了这样的例子:原意是:假设FLAG一个整数,并且FLAG二进制中只有某一位是1,其余都为0。判断整型变量flags在该位上是否也为1,则判断部分写为:if(flags&FLAG !=0) ... ,但是!=运算符的优先级高于&,则实际上被解释为if(flags&(FLAG !=0)) ...,结果完全不同,无论flags在该位上是否有1,判断结果都为真,除非flags整体值为0。

运算符优先级高低表如下:

注意:

  • 优先级最高者其实不是真正意义上的运算符,包括:数组下标[ ],函数调用操作符(),各结构成员选择操作符.或->。它们都是从左至右结合。因此a.b.c含义是(a.b).c,而不是a.(b.c)。
  • 单目运算符的优先级仅次于上述几种运算符,因此在所有真正的运算符中,它们的优先级是最高的。
  • 优先级比单目运算符要低的,接下来就是双目运算符。双目运算符中,算术运算符的优先级最高,移位运算符其次,关系运算符再其次,接着是逻辑运算符,赋值运算符,最后是三目运算符。

因为运算符==与运算符!=的优先级要低于其他关系运算符的优先级。因此当我们比较a和b的大小顺序与c和d的大小顺序是否相同时,可以直接这样表示:a<b==c<d, 当然我们也可以用括号来控制它们的运算优先级(a<b)==(c<d),但是如果括号过多代码的可读性就会降低。

举个相较复杂的例子:if((t=BTYPE(p1->aty)==START)||t==UNIONTY){,这行代码的本意是将BTYPE的返回值赋值给t,再判断t与START或UNIONTY是否相等。但结果却大相径庭。因为==运算符比=运算符的优先级高,因此是先判断BTYPE与START的值是否相等,如果相等则结果为1,不等则结果为0。将0或1再赋值给t,接着再去与UNIONTY进行比较。

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

我相信各位同学在学分支、循环等语句的过程中难免会打多一个分号,那么打多一个分号与没有分号有什么本质区别呢?

例如这个例子if(x[i]>big);big=x[i];我们的本意是如果x[i]大于big,则能够进入到if语句中,但此时结果却不同并且编译器下不会报错。在if语句后面不小心加了个分号,实际上相当于if(x[i]>big){}big=x[i];,即big=x[i];这条语句不包含在if语句内,结果当然完全不同。

当一个函数结束时需要return;语句进行返回,返回值可以选择返回也可以不返回。但是,return后面一定要加分号,比如下面这个例子:

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

原意是该函数不返回任何值直接结束调用。以上代码中return后面缺少分号,则返回的是logrec.date=x[0];即等同于以下代码:

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

这个代码实际上是一个错误的代码,但是编译器不会根据程序员自身的出发点来判断错误,编译器只会进行语法纠错,因此造成该错误可能是个潜伏很深、极难发现的程序bug。

还有另一种情形:

struct logrec{
        int date;
        int time;
        int code;
}
main()
{
 ....
}

当一个声明的结尾紧跟一个函数定义时,如果函数声明结尾的分号被忽略,编译器可能会把声明的类型视作函数的返回值类型。
以上的代码认为main函数返回值类型为结构体类型,如果分号没有被省略,则函数main的返回值类型会缺省定义为int类型。

4.switch语句

switch语句中的break非常重要,但是常常被我们忽略,输出的结果跟我们自己的预想截然不同。例如:

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

以上的程序打印结果为:redyellowblue ,因此我们可以知道当执行到第一个case中如果没有break,程序将会自然而然地执行下去,直到没有case语句。当然我们也要注意default语句,如果条件中没有case内的情况,则要执行default语句,而default语句中没有break,并且在switch中的句首,它仍然会按照顺序进行下去,直到switch结束为止。

5.函数调用

函数调用中必须包括"()"此运算符,如果函数调用时不带参数,也必须带有括号,如果没有括号,则计算的是该函数的地址,并不能调用该函数。例如:正规写法:f(); 错误写法:f;这条语句什么都不会执行。

6.“悬挂”else引发的问题

在刚学C语言时,我相信各位都被这个问题”坑“过,因为当时我们都是凭借感觉来判断哪个if与哪个else是结合的。例如以下代码:

if(x==0)
       if(y==0)error;
else{
       z=x+y;
       f(&z);
    }

如果我们凭借感觉来判断,会认为第一个if是与else结合的,其实不然。我们再将以上代码调整位置。

if(x==0)
       if(y==0)error;
       else
     {
       z=x+y;
       f(&z);
     }

此时就非常明确了,是第二个if与else结合的。在C语言中认为:else始终与同一对括号内最近的未匹配的if结合。即与它相邻最近的if语句结合。

原创不易,如果这篇文章对你有帮助,麻烦点个赞支持下谢谢!

相关文章