《C缺陷与陷阱》读书总结

时间:2021-06-06 17:52:19


第一章 “词法陷阱”

1.词法分析中采用贪心算法

1、编译器处理的每个符号应该尽可能多的包含字符。
2、编译器以从左向右的顺序一个一个尽可能多的读入字符。
3、当即将读入的字符不可能和已读入的字符组成合法符号为止。

例如:

i=3;
result=++i+++i+++i;//根据贪心法分析,((++i)++)..报错error C2105: “++”需要左值 
printf("%d\n",result); 

i=3;
result=i+++++i;//根据贪心法分析,((i++)++)+i报错error C2105: “++”需要左值 
printf("%d\n",result);

i=3;
result=1+i++;//正确,根据贪心法分析,1+ i++
printf("%d\n",result);

2.注意数据类型的范围

和机器字长及编译器有关系:
所以,int,long int,short int的宽度都可能随编译器而异。但有几条铁定的原则(ANSI/ISO制订的):
1 sizeof(short int)<=sizeof(int)
2 sizeof(int)<=sizeof(long int)
3 short int至少应为16位(2字节)
4 long int至少应为32位。

unsigned 是无符号的意思。

例如:
16位编译器
char :1个字节
char*(即指针变量): 2个字节
short int : 2个字节
int: 2个字节
unsigned int : 2个字节
float: 4个字节
double: 8个字节
long: 4个字节
long long: 8个字节
unsigned long: 4个字节

32位编译器

char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节

int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 4个字节
long long: 8个字节
unsigned long: 4个字节

64位编译器
char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节

3.字符串不等于字符数组

printf("helllo world");

与

char hello [] = {'h','e','l','l','o','w','o','r','l','d'.'\0');
printf(hello);

相等

第二章 语法的陷阱

1.函数声明的理解

例如:(*((void (* ) ( ) )0)();

((void()())0)();如何理解呢…请看下文…

    (*(void(*)())0)();

这样的表达式会令C程序员心惊胆战。但是,并不需要这样,因为他们可以在一个简单的规则的帮助下很容易地构造它:以你使用的方式声明它。

每个C变量声明都具有两个部分:一个类型和一组具有特定格式的期望用来对该类型求值的表达式。最简单的表达式就是一个变量:

float f, g;

说明表达式f和g——在求值的时候——具有类型float。由于待求值的时表达式,因此可以*地使用圆括号:

float ((f));

这表示((f))求值为float并且因此,通过推断,f也是一个float。

同样的逻辑用在函数和指针类型。例如:

float ff();

表示表达式ff()是一个float,因此ff是一个返回一个float的函数。类似地,

float *pf;

表示*pf是一个float并且因此pf是一个指向一个float的指针。

这些形式的组合声明对表达式是一样的。因此,

float *g(), (*h)();

表示g()和(*h)()都是float表达式。由于()比绑定得更紧密,g()和(g())表示同样的东西:g是一个返回指float指针的函数,而h是一个指向返回float的函数的指针。

当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。因此,由于

float *g();

声明g是一个返回float指针的函数,所以(float *())就是它的模型。

有了这些知识的武装,我们现在可以准备解决((void()())0)()了。 我们可以将它分为两个部分进行分析。首先,假设我们有一个变量fp,它包含了一个函数指针,并且我们希望调用fp所指向的函数。可以这样写:

(*fp)();

如果fp是一个指向函数的指针,则fp就是函数本身,因此(*fp)()是调用它的一种方法。(*fp)中的括号是必须的,否则这个表达式将会被分析为(fp())。我们现在要找一个适当的表达式来替换fp。

这个问题就是我们的第二步分析。如果C可以读入并理解类型,我们可以写:

(*0)();

但这样并不行,因为运算符要求必须有一个指针作为他的操作数。另外,这个操作数必须是一个指向函数的指针,以保证的结果可以被调用。因此,我们需要将0转换为一个可以描述“指向一个返回void的函数的指针”的类型。

如果fp是一个指向返回void的函数的指针,则(*fp)()是一个void值,并且它的声明将会是这样的:

void (*fp)();

因此,我们需要写:

void (*fp)();
(*fp)();

来声明一个哑变量。一旦我们知道了如何声明该变量,我们也就知道了如何将一个常数转换为该类型:只要从变量的声明中去掉名字即可。因此,我们像下面这样将0转换为一个“指向返回void的函数的指针”:

(void(*)())0

接下来,我们用(void(*)())0来替换fp:

(*(void(*)())0)();

结尾处的分号用于将这个表达式转换为一个语句。

在这里,我们就解决了这个问题时没有使用typedef声明。通过使用它,我们可以更清晰地解决这个问题:

typedef void (*funcptr)();
(*(funcptr)0)();

2.运算符的优先级

优先级

运算符

名称或含义

使用形式

结合方向

说明

1

[]

数组下标

数组名[常量表达式]

左到右

()

圆括号

(表达式)/函数名(形参表)

.

成员选择(对象)

对象.成员名

->

成员选择(指针)

对象指针->成员名

2

-

负号运算符

-表达式

右到左

单目运算符

(类型)

强制类型转换

(数据类型)表达式

++

自增运算符

++变量名/变量名++

单目运算符

自减运算符

–变量名/变量名–

单目运算符

*

取值运算符

*指针变量

单目运算符

&

取地址运算符

&变量名

单目运算符

!

逻辑非运算符

!表达式

单目运算符

~

按位取反运算符

~表达式

单目运算符

sizeof

长度运算符

sizeof(表达式)

3

/

表达式/表达式

左到右

双目运算符

*

表达式*表达式

双目运算符

%

余数(取模)

整型表达式/整型表达式

双目运算符

4

+

表达式+表达式

左到右

双目运算符

-

表达式-表达式

双目运算符

5

<<

左移

变量<<表达式

左到右

双目运算符

>>

右移

变量>>表达式

双目运算符

6

>

大于

表达式>表达式

左到右

双目运算符

>=

大于等于

表达式>=表达式

双目运算符

<

小于

表达式<表达式

双目运算符

<=

小于等于

表达式<=表达式

双目运算符

7

==

等于

表达式==表达式

左到右

双目运算符

!=

不等于

表达式!= 表达式

双目运算符

8

&

按位与

表达式&表达式

左到右

双目运算符

9

^

按位异或

表达式^表达式

左到右

双目运算符

10

|

按位或

表达式|表达式

左到右

双目运算符

11

&&

逻辑与

表达式&&表达式

左到右

双目运算符

12

||

逻辑或

表达式||表达式

左到右

双目运算符

13

?:

条件运算符

表达式1? 表达式2: 表达式3

右到左

三目运算符

14

=

赋值运算符

变量=表达式

右到左

/=

除后赋值

变量/=表达式

*=

乘后赋值

变量*=表达式

%=

取模后赋值

变量%=表达式

+=

加后赋值

变量+=表达式

-=

减后赋值

变量-=表达式

<<=

左移后赋值

变量<<=表达式

>>=

右移后赋值

变量>>=表达式

&=

按位与后赋值

变量&=表达式

^=

按位异或后赋值

变量^=表达式

|=

按位或后赋值

变量|=表达式

15

,

逗号运算符

表达式,表达式,…

左到右

从左向右顺序运算

3.”else“的注意事项

这个问题虽然已经为人熟知,而且也并非C语言所独有,但即使是有多年经验的C程序员也常常在此失误过。
考虑下面的程序片段:

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

这段代码中编程者的本意是应该有两种主要情况,x等于0以及x不等于0。对于x等于0的情形,除非y也等于0(此时调用函数error ),否则程序不作任何处理;对于x不等于0的情形,程序首先将x与y之和赋值给z,然后以z的地址为参数来调用函数f。

然而,这段代码实际上所做的却与编程者的意图相去甚远。原因在于C语言中有这样的规则,else始终与同一对括号内最近的未匹配的if结合。如果我们按照上面这段程序实际上被执行的逻辑来调整代码缩进,大致是这个样子:

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

也就是说,如果x不等于0,程序将不会做任何处理。如果要得到原来的例子中由代码缩进体现的编程者本意的结果,应该这样写:

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

现在,else与第一个if结合,即使它离第二个if更近也是如此,因为此时第二个if已经被括号“封装”起来了。
有的程序设计语言在if语句中使用收尾定界符来显式地说明。例如,在Algol 68语言中,前面提到的例子可以这样写:

if x = 0 
then      if    y = 0 
then  error
fi
else      z := x + y;
f(z)
fi

像上面这样强制使用收尾定界符完全避免了“悬挂”else的问题,付出的代价则是程序稍稍变长了一点。有些C程序员通过使用宏定义也能达到类似的效果:
#define IF {if(
#define THEN ) {
#define ELSE } else {
#define FI }}
这样,上例中的C程序就可以写成:

IF x == 0
THEN   IF  y == 0
THEN  error();
FI
ELSE    z = x + y;
f(&z);
FI

如果一个C程序员过去不是长期浸淫于Algol 68语言,他会发现上面这段代码难于卒读。这样一种解决方案所带来的问题可能比它所解决的问题还要更糟糕。

第三章 语义陷阱

1.指针和数组

1.指针不等同于数组
2.C语中只有一维数组,二维数组只是一维数组的数组。
3.任何一个一数组下标进行的运算都等同于一个对应的指针运算,因此我们完全可以依据指针的行为定义数组下表的行为。

例如:

int calendar[12][30];

可以理解为calendar 由12个元素组成的数组,每个元素又是由30个元素组成的数组。

calendar[3][4];

完全而已理解为以下形式:

(*(calendar+3)+4);

数组和指针并不完全相通

例如:
在一个文件中包含一个定义:

char filename[] = "/etc/passed";

而在另一个文件中声明:

exter char *filename;

2.非数组的指针

在C语言种,字符串常量代表着一个包括字符串所有字符以及一个空字符(‘\0’)的内存区域地址。因为C语言要求字符串常量以空字符作为结束标志,对于其他字符串,C程序员通常也沿用了找个惯例。

假定我们有两个字符串s和t,我们希望这连个字符串连接成单个字符串r。要做到这一点,我们可以借助常用的库函数strcpy和strcat.下面的方法似乎一目了然,可是却不能满足我们的目标:

char *r;
strcpy(r,s);
strcat(r,t);

之所以不行的原因是在与不能确定r指向何处。我们还应看到,不仅要让r指向一个地址,而且r所指向的的地指出还应该有内存空间可供容纳字符串,这个内存空间应该是以某种方式分配了的。

char *r,*malloc();
r = malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);

这个例子还是错的,原因归纳起来有3个:

第一: malloc函数会通过返回一个空指针作为“内存分配失败”的信号。
第二:给r分配内存后应及时释放 ,这一点必须记牢。程序显式的给r分配了内存,就必须显式的释放内存。
第三: 最重要的原因,就是前面的例程在调用malloc函数时并未分配足够的内存。库函数strlen范围参数中字符串所包括的字符数目并未把作为结束标志的空字符计算在内,因此,如果strlen(s)的值是 n,那么字符串实际需要n+1个空间,所以我们必须位r多分配一个字符的空间。做到了这些,并且注意检查malloc是否调用成功,我们就能得到正确的结果:

char *r,*malloc();
r=malloc(strlen(s)+strlen(t)+1);
if(!r)
{
    complain();
    exit(1);
}

strcpy(r,s);
strcat(r,t);


free(r);

3.作为参数的数组声明

如果数组作为一个函数的参数,那么传入的是数组的首地址。C语言会自动的将作为参数的数组声明转换为指针声明。也就是说:

char hello [] ="hello ";

声明了hello 是一个字符数组,如果将该数组作为参数传递给一个函数,

printf("%s\n",hello);

实际上与将该数组第一个元素的地址作为参数传递给函数的作用完全等效,即:

printf("%s\n",&hello);

因此将数组作为函数参数声明毫无意义。所以,C语言中会自动地将作为参数的数组声明转为相应的指针声明。

4.避免“举偶法”

即:指针间相互传递的是地址,一个指针改变内容,另一个指针指向的内容也会改变。

char *p,*q;
p = "xyz";
p=q;
p[1] =a;
printf("%c\n",q[1]);

此时将会输出 a.

尽管在某些上下问的环境,数组和指针非常类似,但他们毕竟不同,在第一个声明中:filename是一个字符数组的名称。尽管在一个语句中引用filename的值将指向该数组的起始元素指针,但是filename的类型是“字符数组”,而不是字符指针。在第二个声明中,filename 被确定为一个指针。这两个对filename的声明使用存储空间是不同的;他们无法以一个合理的方式共存。

后面的暂时还不太懂,想写到这了,完~