《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匹配的错误。