c陷阱与缺陷

第一章     词法陷阱

1.这一章没有太多“干货”,唯一比较有趣的就是 1.3 语法分析中的“贪心法” 所讲内容。这个“贪心”就是编译器会读入字符,如果能新读入的字符和之前所读入字符能组成符号,则编译器会继续读入下一个字符,直到读入的字符不能和之前的字符组成符号。 
比如,

/* a---b和(a--)-b等价 */
/* a+++++b和((a++)++)+b等价 */

2.用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。   

   用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,该数组双引号之间的字符以及一个额外的二进制值为零的字符‘\0’初始化。

   例如 “yes”  ‘yes’  

    前者含义是“依次包含‘y’、‘e’、's'以及空字符'\0'的4个连续内存单元的首地址”,后者并没有明确的定义,但大多数c编译器理解为“一个整数值,由'y','e','s'所代表的整数值按照特定编译器实现中定义的方式组合得到”




第二章     语法“陷阱”


1.理解函数声明


  • 两部分组成:类型 + 一组类似表达式的声明符(declarator),后者的求值应返回一个给定类型的结果
  • float f, f; 等同于 float ((f)), (g);       理解:由1可知
  •      3.typedef void (*funcptr)(); (*(funcptr)0)(); 等同于 (*(void(*)())0)(); 显示调用开机子例程
  •      4.一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需把声明中的变量名和声明末尾的分号去掉
  •         再将剩余的部分用括号封装起来 例如:
  •         float (*h)()      ----->       (float (*)())
  •      5.*fp()=*(fp())      ANSI C把它作为*((*fp))()的简写形式

    



这个小节,作者给出一个有趣的函数用

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

当我第一次看到这个函数调用的时候,直接就懵了,完全不知道它要干啥。其实这个函数就是为了调用在地址0处的返回值为void类型的函数指针的函数。我知道这个中文解释也特别绕,下面我就一步步的分析这个语句。

第一,返回值为void类型的函数指针

void (*pfun)()

这个就是上面那个语句中的

void(*)()

void(*)()0

便是将0这个地址转换成void (*)()类型。如果这个不理解,这个语句该懂吧

(int *)0          //理解:类型不过是告诉编译器分配内存的大小,所以这个的意思是告诉编译器从零开始给我分配(int *)类型大小的内存空间,并且可以存放int类型变量的                           地址

对,这个例子就是将0这个地址转化成int类型。读和写这个地址都是按照32bit或者16bi进行操作(由操作系统是32bit还是16bit决定)。

第二,通过指针访问函数
一般而言,我们使用func()来调用函数,如果是使用函数指针pfun的话,我们应该这样使用

(*pfun)()

而不是

*pfun()

因为()的优先级高于*,如果是后者的话,该语句就等价于

*(pfun()) == *((*pfun)())

这并不是我们想要的结果。说了这么多,只要我们结合一和二就很容易理解这个语句是做什么的了。说实话,他这个用法也比较奇葩,因为他不是用函数的间接地址(函数名)而是用直接地址(这个例子中是0)来调用函数,因此理解起来比较费力。对于函数指针本身,我将在之后的文章中详细讲解如何使用。

2.运算符的优先级问题

  • 任何一个逻辑运算符的优先级低于任何一个关系运算符(有关系的人排在前面)
  • 移位运算符的优先级比算术运算符要低,但比关系运算符要高
  • 关系运算符的优先级并不相同,!===要低于其他的
  • 任何两个逻辑运算符都有不同的优先级, 按位运算符比顺序运算符要高
  • 结合性为自右向左的有:单目运算;三目运算;assignments
       

      注意的例子:*p++会被编译器解释成*(p++),即取指针p所指向的对象,然后将p递增1;而不是(*p)++即取指针p所指向的对象,然后将该对                                象递增1

3.函数调用要包括函数列表

       即使函数不带参数,但调用时也得包括函数列表, f()是一个调用语句,而f仅计算函数f的地址却不调用该函数


第三章    语义陷阱

1.指针和数组

1这节给出了C中数组两个特别需要注意的地方: 
第一,C语言只有一维数组,其元素可以为任何数据类型。

第二,对于一个数组,我们只能做两件事,确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕他们乍看上去是对数组下标进行运算的,实际上都是通过指针进行的。t

2.除了a被用作运算符sizeof的参数的情形,在其他所有的情形中数组名都代表指向数组a中下标为0的元素的指针

2.作为参数的数组声明

       1.C语言中,我们无法将一个数组作为函数参数直接传递,如果我们使用数组名作为参数,那么数组名就会立刻被转换为指向该数组第一 个元素的指针

       2.C语言会自动地将作为参数的数组声明转换为相应的指针声明

3.空指针并非空字符串

  • 编译器保证由0转换而来的指针不等于任何有效的指针,即NULL
  • 当常数0被转化为指针使用时,其绝不能被解除引用(dereference),即if (p == (char *) 0)是合法的,但if (strcmp(p, (char *)0) == 0)...非法
  • p是空指针,则printf(p)printf("%s", p)的行为均为未定义

4.不对称边界
  • ”off-by-one error” 差一错误
  • 左闭右开, for (i = 0; i < 10; i++), 而不写成for (i = 0; i <= 9 ; i++)
  • 入界点(可用序列中的第一个元素为0),出界点(不可用序列中的第一个元素)为10
  • --n >= 0至少要与等效的n-- >0一样快或更快,第一个结果先将n减1,再将结果与0比较;第二个表达式则先保存n,从n中减1,然后比较保存值与0的大小()
  • 坚持“不对称原则”
  • 数组中实际不存在的“溢界”元素的地址位于数组所占内存之后,这个地址可以用于赋值和比较,但如果引用该元素,则非法

5.求值顺序

C语言中只规定了四个运算符有明确规定的求值顺序,它们分别是&&, ||, ?:和,。所以=左右两边是没有规定求值顺序的。这节给出一个例子:

	i = 0;
	while( i < n )
  	  y[ i ] = x[ i++ ];

由于没有说明到底是先算左边还是先算右边,所以可能左边用y[ i+1 ]前的结果接收了右边x[ i++ ]后的结果。当然,也可能左边用y[ i+1 ]的结果接收右边x[ i++ ]后的结果。这和编译器有关,我们应该避免这种写法。

6.整数溢出

这节讲了如何避免有符号数的溢出问题,比如两个有符号非负数a和b,如何判断相加是否溢出?文中给了两个方法,我准备在日后写篇如何防止溢出的文章详细讨论更多情况。

  • 无符号运算没有溢出一说
  • 如果算数运算中一个操作数是有符号整数,另一个无符号整数,则均会转换为无符号整数
  • 如果两个都是有符号整数,则溢出结果未定义
  • 正确检测溢出的方法if ((unsigned)a +(unsigned)b < INT_MAX) 或 if (a < INT_MAX - b), INT_MAX在<limits.h>

/* 方法0 错误方法 */
if( a + b < 0 )

/* 方法1 */
if( ( unsigned )a + ( unsigned )b > INT_MAX )

/* 方法2 */
if( a > INT_MAX - b )

为什么方法0不正确?因为对于有些系统,对于有符号数的溢出,它并不会在状态寄存器中标记“负”,而是会标记“溢出”。这样a+b其实就没有小于0,因此这种判断方式不正确(至少某些情况不正确)。

7.为函数main提供返回值

在某些情形下函数main的返回值却并非无关紧要,大多数c语言实现都通过函数main的返回值来告知操作系统该函数的执行时成功还是失败,典型的处理方案是,返回值为0代表程序执行成功,返回值非0则表示程序执行失败

第四章  连接

1.连接器

1.c语言中的一个重要思想就是分别编译,即若干个源程序可以在不同的时候单独进行编译,然后在恰当的时候整合到一起。

2.连接器通常把目标模块看成是由一组外部对象组成的。每个外部对象代表这机器内存中的某个部分,并通过一个外部名称来识别,因此,程序中的每个函数和每个外部变量,如果没有声明static,就都是一个外部变量。

3.连接器的工作:连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看石佛已有同名的外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命名冲突。

2.声明与定义

1.int a         如果其位置出现在所有的函数体之外,那么它就被称为外部对象a的定义。这个语句说明了a是一个外部整型变量,同时为a分配了存储空间。

2.extern  a   这并不是对a的定义,这个语句仍然说明了a是一个外部整型变量,但是因为它包括了extern关键字,就显示的说明了a的存储空间是在程序的其他地方分配的。从连接器的角度看,上述声明是一个对外部变量a的引用,而不是对a的定义。


3.命名冲突与static修饰符

全局变量在不同文件中不能多次定义,我们定义了一次以后,在其他文件中使用extern修饰符进行访问。为了避免在不同文件中定义同名的全局变量,我们应该使用static修饰符。static修饰的变量和函数的作用域仅限于其所在的。


4.形参、实参与返回值                                                                                                                                                                                                                         1.如果一个函数在被定义或声明之前被调用,那么它的返回类型就默认为整型。

2.为避免错误,在函数调用前应该先声明或者定义       

3任何C函数都含有一个形参列表,该变量在函数调用时被初始化

        4.检查外部类型

     1.char filename[] = "/etc/passwd";extern char* filename;,前者中filename的类型为“字符数组”,后者的类型为“字符指针”,这两个对filename的声明使用存储空间的方式不同(图不一样)

2.应修改为char filename[] = "/etc/passwd";extern char filename[]; 或者char* filename = "/etc/passwd";(文件一), extern char* filename;(文件二)
                                                                                                                                                                                                                                                               5.头文件

我们可以通过把extern修饰的变量放入头文件,只要include这个头文件的文件都可以访问这个全局变量。

五 、库函数

1.预处理用得好事半功倍,用得不好bug满天。在这章,作者给出了一些比较常见的错误使用,比如用宏错误定义函数或者函数参数,用宏错误定 义数据类型。

/* 多了空格 */
#define f (x) ((x) - )

/* 优先级考虑不周到,如果x = a - b结果不对*/
#define abs(x) x>=0?x:-x

/* 正确使用应该全部添加括号,包括最外面也要添加括号,这是为了避免一些比较特殊情况,比如 abs(a) + 1 */
#define abs(x) (((x)>=0)?(x):-(x))

/* 错误的在数据类型上使用宏定义 */
#define T1 struct foo *
T1 a, b;   理解:展开为struct foo *a,b; 此时a是结构体指针,而b并不是。

/* 正确的方法 */
typedef struct foo * T2
T2 c, d;

除了上面这些易错点,在使用宏定义的时候,尤其需要注意++以及--的情况。当遇到++/--的时候,宏定义出错的概率会高很多。

                                                                                                                                                                                                                                                             

附录

printf

  • %之后的称为格式码(指明了格式转换的类型)
  • 修饰符, %和格式码之间 %3.1g(宽度修饰符、精度修饰符等)
  • 标志, %和域宽修饰符之间,如%-14s(左对齐), %+d
  • #对数值的输出格式进行微调,0%o%#o,针对数值0,其分别打印00和0;%#x%#X打印出的16进制数前加上0x或0X;#用在浮点数中则其要求小数点必须打印出来(即使小数点后没有数字),如果用于%g或%G格式项,打印出的数值尾缀的0 将不会被去掉
  • printf允许间接指定域宽,只需用*替换域宽修饰符或精度修饰符或两者,printf("%*.*s", 12, 5, str);
  • printf("%*%\n", n) 打印出n-1个空白字符,后面再跟一个%符号
  • 新增格式码:%p 打印出该指针所指向的地址; %n 指出已经打印的字符数,这个数被存储在对应参数所指向的整数中(一个整型指针),如下int n; printf("hello\n%n", &n)


 道理:

“思考”是一切错误之源;我可以轻易地举出事实来证明这一点:犯了错的人总是会说,“哦,可是我原以为。。。”只要大键琴的各种部件还没有粘合到一起,你就应该反复思考直到真正理解,这种“思考”是无妨的。你应该在不用粘合剂的情况下把所有的部件拼装起来(称之为演习或排练),研究它们是如何结合的,并与装配图仔细对照。
在你吧某些部件粘合起来之后,还应该在检查一遍,我听过很多次这种不幸的故事:“昨晚我做了什么什么,可是今天早上我再看就.....”
 亲爱的制作者,如果你昨晚就好好看了的话,那么你可能已经把不合适的部件拆下来重新装好了,很多制作者是利用业余时间来动手DIY一个大键琴,所以经常忍不住要赶到深夜,但是,根据我接听求助电话的经验,大多数错误都出在制作者在上床睡觉之前做的最后一件工作,所以,在你准备最后做一点什么之前,还是早点休息吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值