C陷阱与缺陷读书笔记

1 符号

术语“符号”(token)指的是程序的一个基本组成单元,其作用相当于一个句子中的单词。编译器中负责将程序分解为一个一个符号的部分,一般称为“词法分析器”。 

2 赋值符号

一般而言,赋值运算相对于比较运算出现得更频繁,因此字符数较少的符号=就被赋予了更常用的含义------ 赋值操作。 

3 词法分析中的贪心法

表达式  a---b  应该怎样理解呢?

词法分析中的贪心法:每一个符号应该包含尽可能多的字符。“如果(编译器的)输入流截止至某个字符之前都已经被分解为一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串。”明白这一规则,上面的表达式就不难理解了。相当于

a -- - b 

4 八进制

如果一个整形常量的第一个字符是数字0,那么该常量将被视作八进制数。 

5 字符与字符串

用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制为零的字符’\0’初始化。 

6 嵌套注释

#if 0

/*

*/

//

#endif 

7 理解函数声明

( * ( void ( * ) ( ) 0 ) ( );    ---- 硬件将调用首地址为0位置的子例程

看到这样的表达式,也许每个程序员的内心会都“不寒而栗”。

构造表达式其实只有一条简单的规则:按照使用的方式来声明。

如何理解这句话呢?

float f, g;

这个声明的含义是:当对其求值时,表达式f和g的类型为浮点数类型。

float ff();

这个声明的含义是:表达式ff()求值结果是一个浮点数,也就是说,ff是一个返回值为浮点类型的函数。

float *pf;

这个声明的含义是*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针。

 

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

声明了这样一个函数g:返回值为指向浮点数的指针,参数列表为空。

声明了h是指向这样一个函数---返回值float,参数列表为空的指针。

float fun()

{

}

h = fun;    //赋值

h();       //调用函数fun – 这是一种简写形式

(*h)();     //调用函数fun – 这是标准的形式

一但我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需要把声明中的变量名和声明末尾的分号去掉,在将剩余的部分用一个括号整个“封装”起来即可。例如,因为下面的声明:

float (*h)();

表示h是一个指向返回值为浮点类型的函数的指针,因此,

(float (*)() )         //注意有个星号*

表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。

上例中可以如下使用这个类型转换符:

void *p = fun;

((float(*)())p)();  //调用fun

( * ( void ( * ) ( ) 0 ) ( );    ---- 硬件将调用首地址为0位置的子例程

这个表达式就表示调用首地址为0位置的子例程,子例程的原型是:返回值为void的函数,其参数列表为空。

有了typedef后这个问题就清晰多了

typedef void (*funcptr)();

(*(funcptr)0)();  //等价于 ( * ( void ( * ) ( ) 0 ) ( ); 

8 运算符的优先级

拿不准的情况下最好用括号。 

9 指针与数组

C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而数组元素可以是任意类型(所以可以定义多维数组)。

对于一个数组,我们只能做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。以数组下标进行的运算实际上是通过指针进行的。

int calendar[12][31];

如果calendar不是用于sizeof的操作数,二是用于其他的场合,那么calendar总是被转换成一个指向calendar数组的起始元素的指针。 

指针的类型很重要,加一减一操作是根据其类型定义的。void类型的指针就不能进行加一减一操作,因为不知道它所指向的元素的大小。 

int a[5][3] = { 1, 2, 3,

                     4, 5, 6,

                     6, 7, 8,

                     9, 10, 11,

                     12, 13, 14};

a 是指向有五个元素的一维数组的地址,其中每个元素又包含三个int型整数。

printf(“%d\n”, **(a+3)) 打印输出9

sizeof(a[1]) = 12

sizeof(*(a+1)) = 12;

*(a+1) 指向数组第二个元素的地址,而这个元素是一个包含3个int型整数的一维数组。 

int (*ap)[31];  //指向数组的指针

int *p[10];    //指针数组 

10 作为参数的数组声明

int strlen (char s[]) 与 int strlen (char *s) 等价

需要注意的是如果一个指针参数并不实际代表一个数组,即使从技术上而言是正确的,采用数组形式的记法经常会起到误导作用。

main (int argc, char *argv[]) 与 main (int argc, char **argv) 等价,前一种写法强调的重点在于argv是一个指向某数组的起始元素的指针,该数组的元素为字符指针类型。因为这两种写法是等价的,所以我们可以任选一种最能清楚反映自己意图的写法。 

11 边界计算与不对称边界

int i, a[10];

for (i=0; i<=10; i++)

       a[i] = 0;

如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么这段程序将陷入死循环。因为a[10] = 0 就相当于i=0,这样当i递增到10时又回到0,循环就这样继续下去。 

有的语言数组下标是从1开始的,C语言的数组下标是从0开始的。这种数组的上界(即第一个“出界点”)恰是数组元素的个数!

避免“栏杆错误”(涉及边界计算时常出的错误)的两个通用原则:

1 特例外推       考虑最简单情况下的实例,然后将结果外推。

2 仔细计算边界

编写这样一个函数:函数buffwrite有两个参数,第一个参数是一个指针,指向将要写入缓冲区的第一个字符;第二个参数就是一个整数,代表将要写入缓冲器的字符数。假定我们可以调用函数flushbuffer来把缓冲区中的内容写出,而且函数flushbuffer会重置指针bufptr,使其指向缓冲区的起始位置。

在下面的两个例子中,我们要小心“栏杆错误”。

version 1

void bufwrite (char *p, int n)

{

       while (--n >= 0)

       {

              if (bufptr == &buffer[N])

                     flushbuffer();

              *bufptr++ = *p++;

       }

}

优化的version 2 

12 main返回值

典型的处理方案是,返回值为0代表程序执行成功,返回值非0则表示程序执行失败。如果一个程序的main函数并不返回任何值,那么有可能看上去执行失败。所以稳健的做法就是始终显示返回正确的值。 

13 连接器

连接器:不理解C语言,然而它能够理解机器语言和内存布局。

典型的连接器把由编译器或汇编器生成的若干目标模块,整合成一个被称为可载入模块或可执行文件的实体,该实体能被操作系统直接执行。

 

编译器:把C源程序“翻译”成连接器能够理解的形式。

简单的编译流程图: 

 

14 声明与定义

有时候声明也是定义

一个声明就是一个定义,除非声明:引入名称 定义:引入实体。

extern int a

extern 显示地说明了a的存储空间是在程序的其它地方分配的。

每个外部对象都必须在某个地方进行定义。

static int a

static修饰符是一个能够减少命名冲突的有用工具。上面的定义中,a的作用域限制在单个文件中。

保证同一个对象只有一种类型(定义与声明统一)、并保证只在一个地方定义。 

15 文件访问

C中要同时对打开的文件进行输入和输出操作,必须在其中插入fseek函数的调用。 

16 缓冲输出与内存分配

设置输出缓冲区

setbuf(stdout, buf)

调用fflush刷新缓冲区

设置缓冲区后,数据会先缓冲在缓冲区知道缓冲区满才输出。

缓冲区的大小由系统头文件<stdio.h>中的BUFSIZ定义。 

17 使用errno检测错误

应在确认出错后,再检查 errno。而不应该直接检查errno判断是否出错。 

18 signal函数

让signal处理函数尽可能的简单。比如不要在signal处理函数中使用malloc。 

一、词法陷阱:

1)=不同于==;
2)&和|不同于&&和||;
3)词法分析中的“贪心法”,例如a---b 会被解析成a-- -b而不是a- --b;
4)整形常量陷阱:046不同于46,原因是046会被解析成八进制,而不是十进制;
二、语法陷阱:
1)运算符的优先级问题,解决办法:加括号;
2)语句结束时的分号,例如:if(x[i] > big); big=x[i];;
3)switch语句的break问题; 
4)else匹配问题,else始终与同一对括号内最近的未匹配的if结合;
三、语义陷阱:
1)指针与数组有时候不是完全等价的;
2)能使用数组的边界指针来进行比较等操作,但不能涉及其具体内容的调用。
   例如:char num[5]; char * p; 可以p=&num[5];但不可以strcpy(p,&num[5]);
3)指针复制问题,复制指针并不会同时复制指针所指向的数据,而只会让两个指针指向内存中的同一地址;
4)栏杆错误:修一个100米的护栏,护栏的栏杆之间相距10米,问需要多少根栏杆?答案是11根,而不是10根;
5)整数溢出问题,比如说两个正整数相加,结果却是负数,或者一个负数取绝对值的时候也有可能会出问题,因为在有符号整型变量中负数的表示范围比正数大;
6)为main函数提供返回值,如果没有说明main的返回类型并且没有返回值的时候,默认会返回一个整型随机数,这有可能会让系统觉得该函数运行失败;
四、预处理问题:
1)宏定义中的空格问题,#define f (x) (x+1)的含义是f代表(x) (x+1)而不是f(x)代表(x+1)
2)宏不是函数,宏定义中所有的变量都要用括号括起来,防止引起与优先级有关的问题。
   例如:#define abs(x) x>0?x:-x
abs(a-b)会被展开成a-b>0?a-b:-a-b
3)宏的参数中如果使用自增或自减函数有可能会出问题
   例如:#define max(a,b) ((a)>(b)?(a):(b))
int a=6; max(a++,5);会被展开成a++>5?a++:5;
4)宏不是语句,如果在宏中使用到了if可能会产生陷阱
   例如:#define assert(e) if(!e) assert_error(_FILE_,_LINE_)
if(x>0 && y>0)
assert(x>y);
else
assert(y>x);
5)宏定义指针型变量的时候,有可能会出现陷阱
   例如: #define int_ptr int*
int_ptr a,b;//这时候,a的类型是int*,而b的类型却是int,所以此时应该用typedef来解决变量重命名问题
五、更新书序文件
为了保持与过去不能同时进行读写操作的程序的向下兼容性,一个输入操作不能随后紧跟一个输出操作,反之亦然。即fread和fwrite之间必须要插入fseek函数的调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值