理解C程序,除了要理解程序的符号,还必须理解这些符号是如何组成声明、表达式、语句和程序。
2.1理解函数声明
如何理解C语句 ((void()())0)();的意义是计算机启动时,硬件将调用首地址为 0 位置的子例程?
构造此类表达式的简单规则是:按照使用的方式来声明,同样也可以按照使用的方式来理解。
任何 C 变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对其求值应该返回一个声明中给定类型的结果。
比如,最简单的单个变量的声明符:
float f, g;
//对其求值表达式 f 和 g 的类型为浮点数类型(float)
比如,对函数的声明:
float ff (); //表达式ff()的求值结果是一个浮点数,也就是说,ff 是一个返回值为浮点类型的函数。
比如,对指针类型的声明:
float *pf; //含义为 *pf 是一个浮点数,也就是说 pf 是一个指向浮点数的指针。
将上述形式组合起来,就像就像在表达式中进行的组合,比如:
float *g(),(*h)(); //g是一个函数,该函数的返回值类型为一个指向浮点数的指针,h是一个函数指针,h 所指向的函数的返回值为浮点类型,则 h 为该函数,调用该函数的形式为 (h)()。
知道了如何声明一个给定类型的变量,那么该类型的类型转换符为:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。
比如声明:float (h)(); //h 表示一个指向返回值为浮点类型的函数的指针
则对应的:(float()()) // 整个表达式表示“指向返回值为浮点类型的函数的指针”的类型转换符。
现在解释开头语句 ((void()())0)(); 的含义:原来的语句想要实现的功能是硬件调用首地址为 0 的子例程,则把 0 作为地址,可使用 (0)();函数的调用形式,但是 0 作为常值,不能作为地址或指针,故需要先对 0 进行 类型的强制转化,将 0 转化为指向函数的指针,即 (void()())0 ,将此表达式代换上式中的 0 ,即为开始讲述的函数语句调用形式。
比如现有声明 void (*sfp)(int); 和 void *signal(something)(int); 将其组合后。可以有如下函数调用: void(*signal(something))(int); ,故最终的的理解为:传递适当的参数以调用signal 函数,对signal 函数的返回值(为函数指针类型)解除引用(dereference),然后传递一个整型参数调用解除引用后所得的函数,最后返回值为void类型。故,signal函数的返回值是一个指向返回值为void类型的函数的指针。
2.2运算符的优先级问题
文章图片
注意运算符的结合性:除了单目运算符,条件运算符和赋值运算符的结合性是自右至左外,其他的所有的运算符的结合性都是自左向右。
优先级最高的并不是真正意义上的运算符,包括:数组下表,函数调用操作符,各结构成员选择操作符。
单目运算的优先级仅次于前述的运算符是所有的真正意义上的运算符中优先级最高的。由于函数调用的优先级高于单目运算符的优先级,故若 p 是一个函数指针,则要调用 p 所指向的函数,必须写为 (p)(),如果写成 p(),编译器理解为(p())。类型转换也是单目运算符,和其他的单目运算符的优先级一样。由于单目运算符的结合性是自右向左,故p++ 的含义是将 p自加之后,则进行解除引用的操作。若是想要把 p 所指的对象进行自加 1 的操作,则必须使用(*p)++ 的形式。
优先级比单目运算符要低的,接下来是双目运算符,在双目运算符中,算数运算符的优先级最高,移位运算符次之,关系运算符再次之,接着是逻辑运算符,赋值运算符。
重要的记住两点:任何的一个逻辑运算符的优先级低于任何一个关系运算符;移位运算符的优先级比算数运算符低,但是比关系运算符高。
在 6 个关系运算符中,== 和 != 的优先级比其他的关系运算符的优先级低。
逻辑运算符中,任何两个运算符的优先级都不相同。首先,所有的按位逻辑运算符的优先级高于顺序逻辑运算符。每个“与”运算符要比相应的“或”运算符优先级高,而按位异或运算符(^)的优先级介于按位与运算和按位或运算之间。
赋值运算符包括了加有前置符号的赋值运算符。
逗号运算符的优先级最低。
2.3注意作为语句结束标志的分号
不小心在 C 程序中多写了一个分号,可能不会造成什么不良后果,这个分号只是被视作一个不会产生任何实际效果的空语句,但若是把分号直接加到了 if 或者 while 之后,那么就造成了 if 和 while 之后的语句成为了一条单独的语句,不再和条件判断部分有任何的联系。比如 if(x[i]>big);
big=x[i]];
,造成的实际的结果为 if(x[i]>big) { };
big=x[i]];
同样,如果遗漏一个分号,也会招致麻烦,例如:
if(n<3)
return
logrec.data=https://www.it610.com/article/x[0];
logrec.data=https://www.it610.com/article/x[1];
logrec.data=https://www.it610.com/article/x[2];
此处return后面遗漏了一个分号, 实际效果为:
if(n<3)
return logrec.data=https://www.it610.com/article/x[0];
logrec.data=https://www.it610.com/article/x[1];
logrec.data=https://www.it610.com/article/x[2];
此时若 n<3 则直接返回了一个数值,若函数的返回类型声明为void,则会造成类型不一致,编译器警告,若函数没有声明返回值类型,则默认为整型,此时编译器不会检测出错误。若n>=3,则会跳过第一个赋值语句,造成一个潜伏更深的,极难发现的错误。
当一个声明的结尾紧跟函数定义时,如果声明的结尾的分号被省略,编译器可能将声明的类型作为函数的返回值类型。
2.4 switch语句
C语言中的把 case 标号当做真正意义上的标号,因此程序的控制流程会径直通过 case 标号,而不会受到任何影响。比如:
switch(color){
case 1:printf(“red”);break;
case 2:printf(“green”);break;
case 3:printf(“blue”);break;
}
每个case后都有break来跳出剩余的选项,即如果color为2,则只会输出green。若省去各个case后的break,即
switch(color){
case 1:printf(“red”);
case 2:printf(“green”);
case 3:printf(“blue”);
}
此时若进入了某个case,会顺序的向下执行,直达遇到break或者整个{ }到达结尾,即若color为1 。输出为greenblue。
C的这个特性及时优点(有意的省略break,实现一些特殊的控制结构)也是缺点(会大意的丢失break)。
2.5 函数调用
C语言中,在函数调用时即使函数不带参数,也应该包括参数列表。故,若 f 是一个函数,则 f(); 是一个函数调用语句,而 f;是一个什么也不做的语句,更准确的说,这个语句计算函数 f 的地址,但并不调用该函数。
【C陷阱与缺陷——第二章(语法陷阱)】2.6 “悬挂”else 引发的问题
C 语言中要,else 始终与同一对括号内的最近的未匹配的 if 结合。