第十六章 C预处理器和C库
16.1 翻译程序的第一步
对程序做预处理前,编译器会对它进行几次翻译处理。
编译器首先把源代码中出现的字符映射到源字符集。
第二,编译器查找反斜线后紧跟换行符的实例并删除这些实例。也就是将两个物理行
printf("That's wond\
erful!\n");
转换成一个逻辑行:printf("That's wonderful!\n");
接下来,编译器将文本划分成预处理的语言符号序列和空白字符及注释序列。编译器用一个空格字符代替每一个注释。
最后程序进入预处理阶段。预处理器寻找可能存在的预处理指定。
这些指令由一行开始出的#符号标识。
16.2 明显常量:#define
预处理指定用#符号作为行的开头。
指令可出现在源文件的任何地方。指令定义的作用域从定义出现的位置开始直到文件的结尾。
每个#define行由三部分组成。
第一部分为指令#define自身。
第二部分为所选择的缩略语,这些缩略语被称为宏。像本例中的这些宏用来代表值,它们被成为类对象宏。宏的名字中不允许有空格,而且必须遵循C变量命名规则。
第三部分称为替换列表或主体。
预处理器在程序中发现了宏的实例后,总会用实体代替该宏。从宏变成最终的替换文本的过程成为宏展开。
使用#define的时候可以使用多个物理行,一行结尾加反斜线符号以使该行扩展至下一行,注意第二行要左对齐,否开头空格也会作为主体的一部分。
一般而言,预处理器发现程序中的宏后,会用它的等价替换文本代替宏,如果字串中还包括红,则继续替换。
例外情况是双引号中的宏printf("TWO:OW");将输出TWO:OW而不是主体。
16.2.1 语言符号
从技术方面看,系统把宏的主体当作语言符号类型字符串,而不是字符型字符串。
#define FOUR 2*2有一个语言符号:2*2。
#define SIX 2 * 3有三个语言符号:2、*和3。当主体解释为字符型字符串时,预处理器用2 * 3替换SIX,也就是额外的空格也当作替换文本的一部分。但是当主体解释为语言符号类型时,预处理用由单个空格分隔的三个语言符号,即2 * 3来代替SIX。
用字符型字符串的观点看,空格也是主体的一部分;而用语言符号字符串的观点看,空格只是分隔主体中语言符号的符号。
C编译器处理语言符号的方式比预处理器的处理方式更加复杂。
16.2.2 重定义常量
假设您把LIMIT定义为20,后来在该文件中又把LIMIT定义为25.这个过程被称为重定义常量。
ANSI标准只允许新定义与旧定义完全相同。这意味着主体具有相同顺序的语言符号。
下面两个定义相同:
#define SIX 2 * 3
#define SIX 2 * 3
两者都有三个相同的语言符号,而且额外的空格不是主体的一部分,下面的定义则被认为是不同的:
#define SIX 2*3
上式只有一个语言符号,因此与前面两个定义不相同。
可以使用#undef指令重新定义宏。
16.3 在#define使用参数
通过使用参数,可以创建外形和作用都与函数相似的类函数宏。宏的参数也用圆括号括起来。
例如#define SQUARE(X) X*X
在程序中可以使用z=SQUARE(2);
一个例子程序:
#include<stdio.h>
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d.\n",X)
int main(void)
{
int x=4;
int z;
printf("x=%d\n",x);
z=SQUARE(x);
PR(z);
PR(SQUARE(x+2));
PR(100/SQUARE(2));
printf("x=%d\n",x);
PR(SQUARE(++x));
return 0;
}
这和我们期待的结果不一样,原因在于预处理器不进行计算,而只进行字符串替换。早出现x的地方,预处理器都用字符串x+2进行替换,因此x*x变成x+2*x+2
想改变结果,只需要多加几个圆括号 例如#define SQUARE(X) (X)*(X)
16.3.1 利用宏参数创建字符串:#运算符
引号中的字符串中的宏被看作普通文本,而不是被看作一个可被替换的语言符号。
假设您确实希望在字符串中包含宏参数,可以使用#。如果x是一个宏参量,那么#x可以把参数名转化为相应的字符串。这个过程称为字符串化。
#define PR(X) printf("The square of " #X " is %d.\n",(X)*(X))
int y=5;PR(y);
它的替换结果为printf("The square of " “y” " is %d.\n",(y)*(y));
接着,字符串连接功能将这三个相邻的字符串转换为一个字符串:
输出The square of y is 25.
16.3.2 预处理器的粘合剂:##运算符
和#运算符一样,##运算符可以用于类函数宏的替换部分。另外##还可用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。
例如 #define XNAME(n) x ## n
通过这个XNAME(4)的宏调用会展开成下列形式:x4
16.3.3 可变宏:...和_ _VA_ARGS_ _
实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个句号)。这样预定义_ _VA_ARGS_ _就可以被用在替换部分中,以表明省略号代表什么。
例如#define PR(...) printf(_ _VA_ARGS_ _)
PR("Hello");就展开一个参数“Hello”;
#define PR(X,...) printf("Message " #X ": "_ _VA_ARGS_ _)
double x=48;PR(1,"x=%g\n",x);
输出如下 Message 1: x=48
16.4 宏,还是函数
宏与函数间的选择实际上是时间与空间的权衡。
宏产生内联代码,也就是在程序中产生语句。如果使用宏20次则插入20行代码。如果使用函数20次,那么程序中只有一份函数语句的拷贝,因此节省了空间。
但是函数设计到程序控制的转移,因此花费时间更多。
宏的一个优点是它不检查其中的变量类型,但是不注意的话会产生奇怪的现象。
16.5 文件包含:#include
预处理器发现#include指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中,就行您把被包含文件中的全部内容键入到源文件的这个特定位置一样。
习惯上使用后缀.h表示头文件,这类文件包含置于程序头部的信息。
包含大型头文件并不一定显著增加程序的大小。很多情况下,头文件中的内容是编译器产生最终代码所需的信息,而不是加到最终代码里的具体语句。
浏览任何一个标准头文件都会使您对头文件中信息的类型有一个清晰的概念。
另外,许多使用头文件来声明多个文件共享的外部变量。
16.6 其它指令
16.6.1 #undef指令
#undef指令取消定义一个给定的#define,即使没有定义,也可以取消。
例如#define LIMIT 400 #undef LIMIT
16.6.2 已定义:C预处理器的观点
如果C中的标识符是该文件签名的#define指令创建的宏名,并且没有用#undef指令关闭该标识符,则标识符是已定义的。
如果标识符不是宏,而是一个具有文件作用域的C变量,那么预处理器把标识符当作未定义的。例如 int q;//q未定义
16.6.3 条件编译
一、#ifdef、#else和#endif指令
#ifdef MAVIS
statement1;
#else
statement2;
#endif
如果预处理定义了标识符MAVIS则执行statement1,否则执行statement2。
二、#ifndef指令
#ifdef的反义指令
#ifndef SIZE
#define SIZE 100
#endif
三、#if和#elif指令
#if指令更像常规的C中的if;#if后跟常量整数表达式。
许多新的方法提高另一种方法来判断一个名字是否已经定义。
不需要用 #ifdef VAX
而是采用 #if defined(VAX)
16.7 内联函数
调用函数需要一定时间开销。使用类函数宏的一个原因是减少执行时间。
C99还提供另一方发:内联函数。
创建内联函数的方法是在函数声明中使用函数说明符inline。
例如 inline void eatline()
内联函数应该比较短小,对于很长的函数,调用函数的时间少于执行函数主体的时间;此时,使用内联函数不会节省多少时间。
内联函数的定义和对该函数的调用必须在同一文件中。
C只允许对函数进行惟一的一次定义,但是对内联函数却放松了这个限制。
C允许混合使用内联函数定义和外部函数定义。
16.8 C库
16.8.1 访问C库
一、自动访问
在许多系统上,您只需编译程序,一些常见的库函数自动可用。
记住,应该声明所使用的函数的类型,通常包含适当的头文件即可。
二、文件包含
如果函数定义为宏,可使用#include指令来包含拥有该定义的文件。
三、库包含
库选项告诉系统到哪儿寻找函数代码
16.9 通用工具库
16.9.1 exit()和atexit()
atexit()函数使用函数指针作为参数。由atexit()注册的函数的类型应该为不接受任何参数的void函数。
调用exit()函数时,按先进后出的顺序执行这些函数。
exit()将做一些自身清理工作,刷新所有输出流,关闭打开流等。
16.10 诊断库
由头文件assert.h支持的诊断库是设计用于辅助调试程序的小型库。
它有宏assert()构成,该宏接受整数表达式作为参数。如果为假,向标准错误流写一条错误消息并调用abort()函数以终止程序。
例如assert(z>=2)
可以使用#define NEDBUG 来禁用文件中所有的assert()语句。
16.11 string.h库中的memcpy()和memmove()
两个函数原型为
void *memcoy(void *restrict s1,const void *restrict s2,size_t n);
void *memmove(void *s1,const void * s2,size_t n);
这两个函数均从s2指向的位置复制n字节数据到s1指向的位置,切均返回s1的值。
两者间的差别在于第一个函数不允许重叠区域。
第三个参数可以为 num*sizeof(int)