16.1 翻译程序的第一步
对程序作与处理前,编译器会对它进行几次翻译处理:
- 首先,编译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三元字符。
- 其次,编译器查找反斜线后紧跟换行符的实例并删除这些实例。
- 接下来,编译器将文本划分成预处理的语言符号序列、空白字符、注释序列。
16.2 明显常量:#define
每个 #define 行(逻辑行)由三部分组成:
- 第一部分:#define 指令自身。
- 第二部分:宏(macro)。宏的名字遵循 C 变量命名规则。
- 第三部分:替换列表或主体。
预处理器在程序中发现宏后,会用替换文本代替该宏。例外情况是双引号中的宏。从宏变成最终的替换文本的过程称为宏展开。
预处理器不进行计算,只按照指令进行文字替换操作。
16.2.1 语言符号
从技术方面看,系统把宏的主体当作语言符号(token)类型字符串,而不是字符型字符串。C 预处理器中的语言符号是宏定义主体中单独的词(word),词与词之间用空白字符分开。
从语言符号字符串的观点看,空格只是分隔主体中语言符号的符号;从字符型字符串的观点看,空格也是主体的一部分。
16.2.2 重定义常量
ANSI C 标准只允许新定义与就定义完全相同,即主体具有相同顺序的语言符号。例如,下面两个定义相同:
#define SIX 2 * 3
#define SIX 2 * 3
16.3 在 #define 中使用参数
通过使用参数,可以创建外形和作用都与函数相似的类函数宏(function-like macro)。宏的参数用圆括号括起来。如:
#define SQUARE(X) X*X //类函数宏定义
num = SQUARE(2); //使用
SQUARE 为宏标识符,SQUARE(X) 中的 X 为类函数宏的参数,X*X 为替换列表。预处理器在程序中发现宏后,会用替换文本代替该宏(只进行文本替换,而不进行计算)。
16.3.1 利用宏参数创建字符串:# 运算符
# 运算符可以把语言符号转化为字符串,该过程称为字符串化。
#define SQUARE(X) printf("The squuare of "#x" is %d.\n", ((X)*(X))) //定义
PSQR(5); //调用。调用时用"5"代替了#X
The squuare of 5 is 25. //输出结果
16.3.2 预处理器的粘合剂:## 运算符
## 运算符可以把两个语言符号组和成一个语言符号。
#define XNAME(X) n ## X //定义
XNAME(5); //调用
n5 //输出结果
16.3.3 可变宏:... 和 __VA_ARGS__
实现思想:在宏定义中参数列表的最后一个参数为省略号(...)。这样预定义宏 __VA_ARGS__ 就可以被利用在替换部分中。
#define PR(X,...) printf("Message "#X": "__VA_ARGS__) //定义
PR(5,"MrGold.\n"); //调用
Message 5: MrGold. //输出结果
16.4 宏,还是函数
宏与函数间的选择实际上是时间与空间的权衡。
- 编译器限制宏只能定义成一行。宏产生内联代码,即在程序中产生语句。若使用宏20次,则将20行代码插入程序中。
- 使用函数20次,程序中只有一份函数语句的拷贝,节省空间。但程序的控制必须转移到函数中并返回调用程序,花费的时间比内联代码多。
对于宏的几点注意:
- 宏的名字遵循 C 变量命名规则,不能含有空格。替换列表中可以含有空格。
- 用圆括号括住每个参数,并括住宏的整体定义。
- 用大写字母表示宏函数名。
16.5 文件包含:#include
预处理器发现 #include 指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含的文件中的文本将替换源代码文件中的 #include 指令,相当于被包含文件中的全部内容键入到源文件中的这个特定位置。 #include 指令有两种形式:
#include <stdio.h> //文件名放在尖括号中,优先搜索系统目录
#include "mystuff.h" //文件名放在双引号中,优先搜索本地目录
16.5.1 头文件:.h 文件
头文件包含置于程序头部的信息。头文件经常包含预处理语句。头文件可由系统提供,也可以自己创建。
16.5.2 使用头文件
头文件内容最常见的形式包括:
- 明显常量:如 stdio.h 文件定义 EOF、NULL。
- 宏函数:如 ctype.h 文件包含 ctype 函数的宏定义。
- 函数声明:如 string.h 文件包含字符串函数系列的函数声明。
- 结构模板定义:如 stdio.h 文件包含 FILE 结构的声明。
- 函数定义:如 stdio.h 文件用 #define 使 FILE 代表指向 FILE 结构的指针。
16.8 C 库
16.8.1 访问 C 库
首先,通常可以在多个不同位置找到库函数。其次,不同的系统使用不同的方法搜索这些函数。
- 自动访问
- 文件包含,使用 #include 指令
- 库包含
16.9 数学库
数学库包含许多有用的数学函数。头文件 math.h 提供这些函数的函数声明或原型。
16.10 通用工具库
通用工具库包含各种函数,其中包括随机数产生函数、搜索和排序函数、转换函数和内存管理函数。头文件 stdlib.h 提供这些函数的函数声明或原型。
16.11 诊断库
诊断库是设计用于辅助调试程序的小型库,由头文件 assert.h 支持。它由宏 assert() 构成,assert() 宏的作用为:表示出程序中某个条件应为真的关键位置,并在条件为假时用 assert() 语句终止该程序。
16.12 string.h 库中的 memcpy() 和 memmove()
memcpy() 和 memmove() 函数为赋值数组提供了便利工具。下面是两个函数的原型:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);
这两个函数均从 s2 指向的位置复制 n 字节数据到 s1 指向的位置,且均返回 s1 的值。两者间的差别由关键字 restrict 造成:
- memcpy() 假定两个内存区域之间没有重叠。复制过程类似于先将所有字节复制到一个临时缓冲区,再复制到目的地。
- memmove() 不做这个假定。
16.13 可变参数:stdarg.h
头文件 stdarg.h 为函数提供了“可以接受可变个数的参数”的能力。但必须按照如下步骤进行:
- 1.在函数原型中使用省略号。(省略号必须是最后参量)
- 2.在函数定义中创建一个 va_list 类型的变量。
- 3.用宏将该变量初始化为一个参数列表。
- 4.用宏访问这个参数列表。
- 5.用宏完成清理工作。
一个实例:
double sum(int lim, ...) //最右边的参量(省略号前)用parmN表示,传递给该参量的实际参数值是省略号部分代表的参数个数。
va_list ap; //创建一个va_list类型的变量ap,用于存放参数。
va_start(ap, lim); //使用宏va_start()初始化为参数列表。宏va_start()接受两个参数:
//第一个参数为va_list类型的变量(此处为ap),第二个参数为参量parmN(此处为lim)。
va_arg(ap,double); //使用宏va_arg()访问参数列表中的内容。宏va_start()接受两个参数:
//第一个参数为va_list类型的变量(此处为ap),第二个参数为一个类型名
va_end(ap); //使用宏va_end()完成清理工作。va_end()接受一个va_list类型的变量(ap)。
C99 添加了宏 va_copy() 来保存 va_list 变量的副本。宏 va_copy() 的两个参数均为 va_list 类型的变量,它将第二个参数复制到第一个参数中。例如:va_copy(apcopy, ap); //apcopy 是 ap 的一个副本。