第一章 词法陷阱
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)
的行为均为未定义
- ”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)