从较低的层面考察, 程序是由符号(token)序列所组成的,将程序分解成符号的过程,称为“词法分析”(第一章)。组成程序的这些符号,又可以看成是语句和声明的序列,就好像一本书可以看出是由单词进一步结合而成的句子所组成的集合,符合或者单词如何组成更大的单元的语法细节(第二章)最终决定了语义(第三章)。
C程序通常是由若干个部分组成,它们分别进行编译,最后再整合起来。这个过程称为“连接”,是程序和其支持环境之间关系的一部分(第四章)。
程序的支持环境包括某组库函数(library routine),库函数对任何一个有用的程序都非常重要(第五章)。
第六章会讨论由于C预处理器的介入,实际运行的程序并不是最初编写的程序。虽然不同预处理器的实现存在或多或少的差异,但是大部分特性是各种预处理器都支持的。
第七章讨论可移植性问题,也就是为什么在一个实现平台上能够运行的程序却无法在另一个平台上运行。当牵涉到可移植性时,哪怕是非常简单的类似的算术运算这样的事情,其困难程度也常常会出人意料。
第八章提供了有关预防性程序设计的一些建议。
1. “=” 不同于 “==”
2. “&” 和 “|” 不同于 “&&” 和 “||”
3. 词法分析中的 “贪心法”
C语言的某些符号只有一个字符长,称为单字符符号,如/ 、* 、 =;也有一些符号为多字符符号,如/* 、 ==。编译器读入字符时必须明确:是将其作为两个分别的符号对待,还是合起来作为一个符号对待。解决方案:每一个符号应该包含尽可能多的字符。
4. 整型常量
如果整型常量的第一个字符是数字 0 ,那么该常量将被视作八进制数。
5. 字符与字符串
单引号 和 双引号 含义迥异。
规则:按照使用的方式来声明
//*g(): g是一个函数,该函数的返回值类型为指向浮点数的指针
//(*h)():h是一个指针函数,h所指向函数的返回值为浮点类型
float *g(), (*h)();
一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。如:
//h是一个指向返回值为浮点类型的函数的指针
float (*h)();
//表示一个“指向返回值为浮点类型的函数的指针”的类型转换符
(float (*)())
//eg.调用首地址为0位置的子例程:分析表达式 (*(void(*)())0)();
//1. 假定变量fp是一个函数指针,那么调用fp所指向的函数方法如下:
(*fp)();
//因为fp是一个函数指针,那么*fp就是该指针指向的函数,所以(*fp)()就是该函数的调用方式
//2. 如果C编译器能够理解我们大脑中对于类型的认识,那么我们可以这样写:
(*0)();
//上式并不能生效,因为运算符*必须要有一个指针来做操作数。
//而且这个指针还应该是一个函数指针,这样经运算符*作用后的结果才能作为函数被调用。
//因此,在上式中,必须对0作类型转换,转换后的类型可以大致描述为:
//“指向返回值为void类型的函数的指针”
//如果fp是一个指向返回值为void类型的函数的指针,那么(*fp)()的值为void,fp的声明如下:
void (*fp)();
因此,我们可以用下式来完成调用存储位置为0的子例程:
void (*fp)();
(*fp)();
//但是,我们一旦知道如何声明一个变量,也就自然知道如何对一个常数进行类型转换,
//将其转型为该变量的类型:只需要在变量声明中将变量名去掉即可
//因此,将常数0转型为“指向返回值为void的函数的指针”类型,可以这样写:
(void(*)())0
//因此,我们可以用(void(*)())0 来替换fp,从而得到
(*(void(*)())0)();
//使用typedef能够使表述更加清晰
typedef void (*funcptr)();
(*(funcptr)0)();
//eg. signal函数声明
//首先,让我们从用户定义的信号处理函数开始考虑,该函数可以定义如下:
void sigfunc(int n){
//特定信号处理部分
}
//函数sigfunc的参数是一个代表特定信号的整数值,此处我们暂时忽略它
//上面假设的函数体定义了sigfunc函数,因而sigfunc函数的声明可以如下:
void sigfunc(int );
//现在假定我们希望声明一个指向sigfunc函数的指针变量,不妨命名sfp。
//因为sfp指向sigfunc函数,则*sfp可以被调用。
//又假定sig是一个整数,则(*sfp)(sig)的值为void类型,因此我们可以如下声明sfp:
void (*sfp)(int);
//因为signal函数的返回值类型与sfp的返回类型一样,上式也就声明了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);
C语言要求:在函数调用时即使函数不带参数,也应该包括参数列表
//错误范例
if(x == 0)
if(y == 0) error();
else{
z = x+y;
}
C语言:else 始终与同一对括号内最近的未匹配的if结合
//上述代码实际为:
if(x == 0)
if(y == 0)
error();
else{
z = x+y;
}
//正确示范
if(x == 0){
if(y == 0) error();
}else{
z = x+y;
}
C语言中会自动地将作为参数的数组声明转换为相应的指针声明
//eg.以下两种函数声明写法等效
int strlen(char s[]);
int strlen(char* s);
//eg.以下两种写法有着天壤之别
extern char* hello;
extern char hello[[];
//eg.以下两种写法完全等价
int main(int argc, char* argv[]);
int main(int argc, char** argv);
“举隅法”(synecdoche)是一种文学修辞上的手段,以部分代表整体,或以整体代表部分。
C语言中常见的“陷阱”:混淆指针与指针所指向的数据。
char *p, *q;
p = "xyz"; //p的值是一个指向'x' 'y' 'z' '\0' 4个字符组成的数组的起始元素的指针
q = p; //p和q现在是两个指向内存中同一地址的指针(这个赋值语句并没有同时复制内存中的字符)
//C语言中,常数0这个值经常用一个符号来代替
#define NULL 0
/*
由0转换而来的指针不等于任何有效的指针
当常数0被转换为指针使用时,这个指针绝对不能被解引用(dereference)
也就是说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容
*/
if(p == (char *) 0)//合法
if(strcmp(p, (char *) 0) == 0)//非法
不对称边界:用第一个入界点(有效范围内)和第一个出界点(有效范围外)来表示数值范围。
/*
函数bufwrite有两个参数:
第一个参数是一个指针,指向将要写入缓冲区的第一个字符;
第二个参数是一个整数,代表将要写入缓冲区的字符数。
*/
void bufwrite(char* p, int n){
while(--n >= 0){
if(bufptr == &buffer[N]) //等效于if(bufptr > &buffer[N-1])
flushbuffer(); //把缓冲区中的内容写出,并重置指针bufptr,使其指向缓冲区起始位置
*bufptr++ = *p++;
}
}
//上面的程序每次迭代都需要两个检查,且一次只能转移一个字符到缓冲区
//一次移动k个字符的方法如下:
void memcpy(char* dest, const char* src, int k){
while(--k >= 0){
*dest++ = *src++;
}
}
//改进bufwrite
void bufwrite(char* p, int n){
while(n > 0){
int k, rem;
if(bufptr == &buffer[N])
flushbuffer();
rem = N - (bufptr - buffer);
k = n > rem ? rem : n;
memcpy(bufptr, p, k);
bufptr += k;
p += k;
n -= k;
}
}
//eg. 按一定顺序生成一些整数,并将这些整数按列输出
//(程序的输出可能包括若干页的整数,每页包括NCOLS列,每列又包括NROWS个元素,每个元素就是一个待输出的整数)
... ...
一个C程序可能是由多个分别编译的部分组成,这些不同部分通过一个连接器的程序合并成一个整体。
典型的连接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。
连接器的输入:一组目标模块和库文件
连接器的输出:一个载入模块
int a; //声明语句,同时分配存储空间
int a = 7;//为 a 分配内存的同时说明了在该内存中应存储的值
extern int a;//说明 a 是一个外部整型变量,但 a 的存储空间是在程序的其他地方分配的
static int a;//static修饰符将a的作用域限制在当前源文件内,可以有效减少命名冲突问题
int main(){
char c;
while((c = getchar()) != EOF){
putchar(c);
}
return 0;
}
/*
上述程序的bug:
变量 c 为char类型,getchar()范围值为int类型。
这意味着,c无法容下所有可能的字符,特别是,可能无法容下EOF。
*/
//许多系统中的标准输入、输出库都允许程序打开一个文件,同时进行写入和读出的操作
FILE *fp;
fp = fopen(file, "r+");
struct record rec;
...
while(fread((char*)&rec, sizeof(rec), 1, fp) == 1){
//对rec执行某些操作
if(/*rec必须被重新写入*/){
fseek(fp, -(long)sizeof(rec), 1);
fwrite((char*)&rec, sizeof(rec), 1, fp);
fseek(fp, 0L, 1);//fseek切换文件状态
}
}