我读的这本书是Andrew Koneig 著作的。读完了之后,对我对c语言的理解增加了很多,总结于此,以备我以后或者其他读者学习。
第一章:词法陷阱
1、编译程序编译C程序的时候使用的是贪心法,也就是说编译程序讲程序分解成符号的方法是,从左到右一个字符一个字符的读入,如果字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串师父可能是一个符号的组成部分,如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。
例如:a---b的意义与表达式a-- -b的含义相同,而与a- --b的含义不同。
2、为了使程序不会出现一些意外的错误,等号的两边最好都有空格,该加括号的时候也一定要加括号。
第二章:语法陷阱
1、理解函数声明
float *g( ),(*h)();
这两个声明表示*g( )与(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g()),g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,可以得出h是一个函数指针,h指向的函数的返回值为浮点类型。
一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个装起来即可。例如float (*h)();表示h是一个指向返回值为浮点类型的函数指针,因此,(float (*)())表示一个指向返回值为浮点类型的函数的指针的类型转化符。
考虑signal库函数,在包括函数的C编译器实现中,signal函数接受两个参数,一个是代表需要“被捕获”的特定signal的整数值,另一个是指向用户提供的函数的指针。
signal函数是如何声明的呢?
信号处理函数可以定义如下:
void sigfunc(int n){ /*特定信号处理部分*/}
函数sigfunc的参数是一个代表特定信号的整数值。现在假定我们希望声明一个指向sigfunc函数的指针变量,不妨命名为sfp。因为sfp指向sigfunc函数,则*sfp就代表了sigfunc函数,因此*sfp可以被调用。又假定sig是一个整数,则(*sfp)(sig)的值得为void类型,因此我们可以如下声明sfp:
void (*sfp)(int);
因为sfp的返回类型与signal的返回类型一样,所以上式也就声明了signal函数,我们可以如下声明signal函数:
void (*signal(something))(int);
此处的something代表了signal函数的参数类型。上面声明可以这样理解:传递适当的参数以调用signal函数,对signal函数返回值解除引用,然后传递一个整型参数调用解除引用后所得函数,最后返回值为void类型。因此,signal函数的返回值是一个指向返回值为void类型的函数的指针。
那么,signal函数的参数又是如何呢?signal函数接受两个参数:一个整型的信号编号,以及一个指向用户定义的信号处理函数的指针。我们此前已经定义了指向用户定义的信号处理函数的指针sfp:
void (*sfp)(int);
sfp 的类型可以通过将上面的声明中的sfp去掉而得到,即void(*)(int)。此外,signal函数的返回值是一个指向调用前用户定义信号处理函数的指针忙着个指针的类型一sfp指针类型一致,因此我们可以如下声明signal函数:
void (*signal(int,void(*)(int)))(int);
我们使用typedef简化上面的函数声明:
typedef void (*HANDLER)(int);
HANDLER signal(int ,HANDLER);
2、运算符优先级的问题
初等运算符((),[],-> ,.) > 单目运算符 > 算数运算符 > 关系运算符 > 逻辑运算符 > 条件运算符 > 赋值运算符 > 逗号运算符
最好的方法是在不知道优先级的情况下加括号。
三、语义陷阱
1、边界计算与不对称边界
用第一个入届点(值处于数组下标范围以内的点,包括边界点)和第一个出界点(指不在数组下标范围以内的店,不含边界点)来表示一个数值范围。
比如说数组有10个元素,就是从0(包含0)到10(不包含10)
for(i =0 ;i < 10 ;++i)
而不是写成
for(i =0 ;i < =9 ;++i)
这样写是会有好处的。
四 、连接
1、为了避免可能因为连接或者别的原因造成的影响活错误,在需要外部变量的时候最好是在.c文件中定义,宅.h文件中声明,然后在需要变量的文件中include .h 文件。
在.h中用extern 修饰变量。
五、库函数
1、返回整数的getchar函数
getchar函数在一般情况下返回的是标准输入文件中的下一个字符,当没有输入时返回EOF(在一个在头文件stdio.h中被定义的值,不同于任何一个字符)。
特别的是,char类型的变量可能容不下EOF的值,所以在使用getchar函数的时候,之前的变量最好是用int声明。如:
int c;
while((c=getchar())!=EOF){}
2、更新顺序文件
为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后直接紧跟一个输出操作,反之亦然。如果同时进行输入和输出操作,必须在其中插入fseek函数的调用。
3、缓冲输出
程序输入有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式,前者往往造成较高的系统负担。因此,c语言实现通常都允许程序员进行实际的写操作之前控制产生的数据量。
这种控制能力一般通过库函数setbuf实现的。如果buf是一个大小适当的字符数组,那么
setbuf(stdout,buf);
语句将通知输入输出库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满或者程序员直接调用fflush,buf缓冲区中的内容才实际写入到stdout中。缓冲去的大小由系统头文件<stdio.h>中的BUFSIZ定义。
六、预处理器
1、不能忽视宏定义中的空格
#define f (X) (x-1) 定义的答案可能有两种:f(x)或者代表((x)-1);或者f 代表(x)((x)-1);
所以不能忽视定义中的空格#define f(X) (x-1)
2、宏不是函数
宏定义不是函数,宏定义是定义一个代表符号,在调用的地方就是会展开的,所以可能会造成一些没有预料的错误,所以在宏定义的时候最好把定义的式子用括号括起来。
还有一点注意的是在在使用宏的时候,尽量不要用与X++(X--,++X.--X)类似的式子。
3、宏不是类型定义
考虑如下代码:
#define T1 char *;
typedef char * T2;
从上面的定义来看,T1和T2从概念上完全符同,都是指向结构char的指针。但是当我们试图使用它们来声明多个变量时,问题就来了:
T1 a,b;
T2 a,b;
第一个声明被扩展为
char * a,b;
这个语句中的a被定义为一个指向char的指针,而b却被定义为char。第二个声明就是对的。