C陷阱与缺陷
读书笔记一
1. 词法“陷阱”
1.1 =不同于= =
C语言中用字符更少“=”来表示频繁发生和使用的赋值操作。而用“= =”表示使用相对较少的比较操作。由于两者相近,可能会因为失误将“= =”误输入为“=”。或反之的误输入。造成程序的不正常,增加程序员工作。
解决方法:在做比较时将常量和不可赋值的标识符放在“= =”的左方而将变量放在右侧。这样在出现误将比较写成赋值操作时编译器会提示出错。产生必要的提示信息。(林锐—高质量C/C++)
1.2 “&和|”不同于“&&和||”
“&和|”表示按位与和或,而“&&和||”表示逻辑与和或两者是不同类型的操作,需要注意。
1.3 词法分析的“贪心算法”—标识符优先级和结合问题。
编译器在对程序做处理时必然要经历词法分析阶段,在这阶段编译器将拆分语句和标识符使其为下一步做好准备。这时需要注意在程序中是否有容易产生歧义的标识符的组合。
解决办法:尽量用括号将自己需要表达的优先级进行封装。明确结合步骤。
1.4 整型常量:
C语言在区分进制时八进制是以0开头的,十六进制以0x开头。此外在许多编译器中把8,9也会用作八进制中(标准c中是禁止的)。需要注意类似0128并不代表128而是1*82+2*81+8*80
1.5 字符和字符串:
首先是”和“”的问题,单引号在程序中表示字符即8位整数数值。双引号表示一个指向无名数组起始字符的指针,该数组被双引号之间的字符和以及额外的一个二进制值为0的字符’/0’初始化。举例如下:
char a[]={’a’};
char b[]=”a”;
数组a的内容并不等同于数组b的内容。a的长度是一个字符长度内容是字符a,而数组b是两个字符长度,内容是字符a和字符/0。
另举例如下
printf(“this is a string/n”);
与
char string[]={‘t’,’h’,’i’,’s’,’ ’,’i’,’s’,’ ’,’a’,’ ’,’s’,’t’,’r’,’i’,’n’,’g’,’/n’0};
printf(string);
是等效。所以在用数组存储字符串时注意预留最后的结束符/0的空间。
另:当出现’abc’这是根据编译器的不同做出不同的处理的。所以要注意双引号与单引号的区别。
(*(void(*)()0)();这是一个显示的函数调用。如何去分析类似的使用和声明的问题。
任何C变量的声明都由两部分组成:类型以及一类类似表达式的声明符。
最简单的声明符就是单个变量:
float f,g;其中float是类型,f和g是声明符。
其中声明符是在允许范围内可以任意使用括号,并注意优先级结合问题。
既可以用如下方式:
float ((f));表示 ((f))的类型为float,同样f也为float。
同样声明一个函数指针如下:
float (*h)();
h是函数指针。
将开始的(*(void(*)()0)();分解分析如下:首先这是个函数调用,即类似(*h)();
“*(void(*)()0)”等同于*h,是个函数的指针。而“(void(*)()0)”相当于h,“void(*)()”可以认为是类型,即“指向返回值为void的函数指针”用其对0做类型转换,最终(*(void(*)()0)()调用的是从0这个地址开始的函数。
2.2 运算符优先级
C语言有15个优先级,具体如下:
() [] -> . left to right
! ~ ++ -- +(正号) -(负号) *(指针取值符) (type) sizeof right to left
* / % left to right
+ - left to right
<< >> left to right
< <= > >= left to right
== != left to right
& left to right
^ left to right
| left to right
&& left to right
|| left to right
?: right to left
= += -= *= /= %= &= ^= |= <<= >>= right to left
, left to right
15个优先级全部记住需要点时间和熟悉,但是因为优先级的问题造成的程序问题也是不容小看的。例如:
r = high<<4 +low;
程序员本意想将high(值小于16)和low(值小于16),这两个数按高低位组成新的数据r的,但是在上式中由于+的优先级高于<<,就变成了将high左移4+low位然后赋值给r。
所以需要熟悉运算符优先级,找到网上的记忆口诀如下:
记忆口诀:
括号成员第一; //括号运算符[]() 成员运算符. ->
全体单目第二; //所有的单目运算符比如++ -- +(正) -(负) 指针运算*&
乘除余三,加减四; //这个"余"是指取余运算即%
移位五,关系六; //移位运算符:<< >> ,关系:> < >= <= 等
等于(与)不等排第七; //即== !=
位与异或和位或; //这几个都是位运算: 位与(&)异或(^)位或(|)
"三分天下"八九十;
逻辑或跟与; //逻辑运算符:|| 和 &&
十二和十一; //注意顺序:优先级(||) 底于 优先级(&&)
条件高于赋值, //三目运算符优先级排到 13 位只比赋值运算符和","高
逗号运算级最低! //逗号运算符优先级最低
另外:还有一个简单的解决办法,那就是添加括号。
2.3 句尾的分号问题。
当使用if()语句和return和声明结构体时注意句尾分号的问题。例如:
if(i==0)
return
x= y[20];
本意当i等于零时,结束,但因为少了分号,则变为返回x= y[20];
的值了。
2.4 swith
c语言中将switch中的case当作真正的标号来看待,即case作为程序的入口,从此处开始执行,并且顺执行下去。举例:
switch (num)
{
case 1:
printf(”case 1/n”);
case 2:
printf(”case 2/n”);
case 3:
printf(”case 3/n”);
case 4:
printf(”case 4/n”);
case 5:
printf(”case 5/n”);
}
当num=2时,屏幕输出为:
case 2
case 3
case 4
case 5
当需要对每个分支进行控制时需要使用break;语句。
2.5 函数调用
C语言中规定函数调用即使没有参数也要包括函数列表。仅使用函数仅是计算函数地址而不调用函数。
2.6 “悬挂”else的问题—if与else匹配问题。
if与else的匹配问题是很基础的问题。但是确是很容易出现问题的地方。解决方法只有细心和良好的书写格式。
3. 语义陷阱
3.1 指针和数组。
在C语言中数组和指针俩跟着紧密联系。关于这两者需要注意两点。
1. C语言中只有一维数组,而且数组的大小必须在编译期就做为一个常数确定下来。(C99新标准中支持可变长数组,gcc编译器实现了可变长数组但与标准有出入)但是数组的成员可以是任意类型,当然也可以是另一个数组,这样就可以构成多维数组。
2. 对于数组,我们仅能确定该数组的大下,以及指向该数组的下标为0的元素指针。即使乍看上去是以数组下标完成,而实际是以指针操作完成。
PS:同时注意C语言对数组的越界操作时默认允许的,在一些编译器的实现上可能会给出告警,但是并影响编译。所以要注意这点。
根据这点我们可以定义出指向数组的指针 int (* ap)[20]
ap是一个指向拥有20个整型变量的数组,即ap+1将移动20个整型变量的距离。
3.2非数组指针。
C专家编程
1. 安静的按转变(编译的类型转换)
char ,short int或int型位段,无论有符号或是无符号变型,枚举等。可以使用int或unsigned int型时,如果int能表示源类型的所有值,则转换为int型,否则转换为unsigned int型。这就是所谓的整型升级。
类型的转换可以简单表述为,当执行算术运算时,如果类型不同就会发生类型转换。数据类型一般朝着个浮点精度更高,长度更长的类型转换。整型如果signed不会丢失信息就转化为signed,否则则转化为unsigned型。
举例:
int array[] ={12,56,36,25,1,23,89}
#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))
main()
{
int d=-1,x;
/* ……..*/
x= array[d+1];
/* ……………………*/
}
上例中if(d<=TOTAL_ELEMENTS-2)比较时,发生了类型转换,TOTAL_ELEMENTS为unsigned型则,d也要转化为unsigned,则d在转化后是一个非常大的正数。条件一直成立。一个类型转化引起的bug。
解决方法:
- 尽量减少使用无符号类型数进行计算。以免增加不必要的复杂性,尤其避免因为不存在负值而用其来表示年龄等。
- 只用在位段和二进制掩码时,才可以用无符号数,应该在表达式中使用枪支类型转化,使操作数均为有符号数或者无符号数,这样就不必由编译器来选择结果的类型。
2.NULL与NUL。
NULL表示指针指向为空,NUL表示字符串的结束标志符。