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。
19 预处理器
预处理阶段对所有的宏定义进行替换
我们最好在宏定义中把每个参数都用括号括起来。同样,整个结果表达式也应该用括号括起来,以防止当宏用于一个更大一些的表达式中可能出现得问题。