《C陷阱和缺陷》读书笔记-其一:词法和语法陷阱

第一章:词法陷阱

1、= 与 ==

赋值运算符"=",表示将等号右侧的值赋值给左侧的变量,注意点在于等号左侧必须为变量,等号右侧可以为变量、常量或者表达式;
比较运算符"==",判断双等号左右两侧的值是否相等,如果相等则该比较表达式返回true,否则返回false;其中两侧可以为变量、常量或者表达式;

(1)在if条件或者循环条件语句中,经常会用到比较运算符“==”,判断两个值是否相等,并分别进行操作;如果,此时将比较运算符误写成赋值运算符,则可能导致出现if条件恒为真或者进入死循环;如下程序段:

int start = 0;
int end = 100;
// 预期判断start与end是否相等,相等则退出,实际上,当end为非零值时if条件恒真
if (start = end) {
	break;
}

建议:在比较运算符使用时,将常量写在左侧,利用赋值运算符的左侧必须为变量的原则进行校验,此时若将“==”误写为“=”时,编译器会报错;

(2)如果在赋值过程中,将“=”误写成“==”,会更令人头疼,编译器很难发现这个错误,且人工排查也难,只能在遇到变量赋值异常的时候,进一步确认变量是否真正赋值成功;

int i = 0;
int j = 5;
if (i != j) {
	// 预期当i与j不等时,将j的值赋值给i,但实际上比较运算符操作后,i和j的值没有任何变化
	i == j;
}

(3)比较运算符和位运算符,也需要注意避免混用:& 和 &&; | 和 ||

2、 词法分析中贪心法

当编译器一次读入多个字符时,划分原则:每一个符号应该包含尽可能多得字符。
即,从左到右一个一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个符号,判断ok则再继续读,直到读入的字符组成的字符串已经不再可能组成一个有意义的符号,也被称为“贪心法”。
a---b会被解释成(a--) - b,而不是a - (--b)
y = x/*p /* p指向除数*/会被解释成y = x,注释为/*p /*p指向除数*/

3、整形常量

(1)如果一个整形常量的第一个字符时数字0,那么该常量将被视作八进制数。需要注意在数字对其时,避免在数字第一个字符添加0。

4、字符与字符串

(1)单引号引起的一个字符实际上代表一个整数,其值为该字符在编译器采用的字符集中的序列值,如在ASCII字符集中,'a'的含义与十进制97严格一致;
(2)双引号引起的字符串,表示一个指向无名数组的起始字符的指针,且为常量指针,即字符串中内容不能改变,该数组被双引号之间的字符以及衣蛾额外的二进制值为0的字符'\0'初始化。
(3)简而言之,单引号表示的字符实际上是一个整数,双引号表示的常量字符串实际上是一个指针。

第二章 语法陷阱

1、函数声明

(1)任何变量的声明都由两部分组成:类型以及一组类似表达式的声明符。
第一步,基本上没啥问题:

int a;         // 声明一个int型变量a  
float ff();    // 声明一个返回值为浮点类型的函数,且函数参数为空    
float *p;      // 声明一个指向浮点数的指针  

第二步,还可以理解:
注意:()的优先级高于*

float *g();    // *g()实际上是*(g()),表示声明一个函数,函数的返回值类型为指向浮点数的指针
float (*h)();  // 表示声明一个指针,该指针是一个函数指针,返回值为浮点型,函数参数为空

(2)将声明转换成该类型的类型转换符:只需要将声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个封起来即可。如:

float (*h)();   // 声明一个指向返回值为浮点类型的函数指针
(float (*)());   // 表示一个“指向返回值为浮点类型的函数指针”的类型转换符

如何调用函数指针:如上,h为一个函数指针,h则是该指针指向的函数,(h)()就是调用该函数的方式,ANSI C标准允许将其简写为h()。

(3)(*(void (*)())0)();
计算机启动,调用首地址为0的位置,如上,void ()() 是一个类型转化符,将0转换为一个指向返回值为void类型且入参为空的函数指针,
在利用
运算可以得到对应的函数,(*0)(),即可进行调用。

(4)void (*signal(int, void(*)(int)))(int);
信号处理函数,如上上,void (*h)()表示声明一个指向返回值为void类型且参数为空的函数指针h;
则 void (*signal)(int)表示声明一个指向返回值void类型且参数为int型的函数指针signal;
如果有,void (*signal())(),可以看做signal()调用后,返回一个函数指针,该指针指向的函数类型为void,且入参为int;
至此,如果signal如果添加两个入参:一个整形和一个函数指针类型,就变成了标题中形式;
使用typedef简化函数声明如下:

typedef viod (*HANDLER)(int);  
HANDLER sinnal(int, HANDLER);

2、运算符的优先级

(1)最高优先级:() [] -> .
(2)其次是单目运算符:! -- ++ - (type) * & sizeof
(3)再次是双目运算符,三目运算符,最后是逗号
(4)任何一个逻辑运算符的优先级低于任何一个关系运算符;
(5)移位运算符的优先级低于算符运算符,高于关系运算符;
(6)赋值运算符的优先级低于比较运算符;

3、语句结束标志——分号

(1)多加分号:在if语句或者while语句的条件判断后多加一个分号,表明当前条件体或者循环体为空:

if (x[i] > big); 
{
	// 获取x[i]的最大值
	big = x[i];
}

利用一下缩进可以解决:

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

while (i > big) {
	i++;
}

(2)少加分号:当声明结尾的分号丢失时,编译器可能会将声明的类型作为函数的返回值类型:

struct logrec {
	int date;
	int time;
    int code;
} 

main()
{
	// 此时编译器可能会认为main函数的返回值类型为struct logrec
}

4、switch语句

C语言的switch-case结构中,case是真正意义上的标号,程序的控制流程会直通case标号,类似于goto语句;

如果需要在执行完当前case后立即退出switch,需要在case部分添加break,如果有意遗漏,需要添加注释说明;

利用case的这种直通特性,有时候可以构造处精巧的程序。

5、函数调用

在函数调用时,即使函数不带参数,也需要包括参数列表,如f()。

6、悬挂的else

else始终与同一对括号内最近的未匹配的if结合;使用完备的大括号,可以有效避免if-else匹配的错误。

posted @ 2021-06-20 00:58  Pangolin2  阅读(39)  评论(0编辑  收藏  举报