C语言以C关键字、表达式、语句,以及这些元素的使用规则为基础。ANSI C标准不仅描述了C语言,还描述了C预处理器的工作机制。在编译之前,先由预处理器检查程序,根据程序中使用的预处理器指令,使用符号缩略语所代表的内容替换程序中的缩略语。
预处理之前的翻译
在程序预处理之前,编译器会先做几次翻译处理。编译器先把源代码中出现的字符映射到源字符集。该过程处理多字节字符和使C外观更加国际化的三元字符的扩展。(这是因为有的键盘不能提供C使用的所有符号,所以C是哟娜个一组三字符序列为一些符号提供可选的表示方法,比如使用 ??=
表示 #
,C99提供了一种二元字符来代替三元字符)。C源码中出现这类三元字符时要先进行替换。
然后编译器会查找反斜杠()后紧跟换行符的实例,并删除这些实例,即把使用反斜杠换行的两个物理行合并。接下来编译器将文本划分成预处理的语言符号序列和空白字符及注释序列,这里,编译器用一个空格字符代替每一个注释。之后进入预处理阶段。
#define
预处理器指令从#define
开始,到期后一个换行符为止,也就是说指令的长度限于一行代码,但是在前一节中说过,翻译过程会删除反斜杠和换行符的组合,所以代码中,预处理器指令可以扩展到几个物理行,由它们组成单个逻辑行。
每个 #define
行由三个部分组成,第一部分为 #define 自身,第二部分即所选择的缩略语,这些缩略语被称为 宏。宏的名字中不允许出现空格,而且需要遵守C变量命名规则。第三部分就是剩下的其他部分,称为替换列表,预处理器发现了宏的实例后(双引号中的不是宏),总会用实体代替该宏。这个过程称为宏展开。
你可以用标准注释方法(/* ... */
)在 #define
中进行注释,因为这些注释会被空格替换。
预处理阶段不进行计算,只是按照指令进行文字替换操作。实际的计算过程发生在编译阶段。
对于大多数数字常量应该使用符号常量,如果是用于计算式的常量,使用符号名会更加清楚,如果数字代表数组的大小,那么使用符号名后更容易改变数组的大小和循环的界限。常用的如 EOF 等系统代码是的程序更加易于移植。
语言符号
系统把宏的主体当作语言符号(token)类型字符串,而不是字符型字符串。C预处理器中的语言符号是宏定义主体中的单独的词(word)。用空白符把这些词分开。**
你有可能在不同的文件中定义了同名的常量,这成为重定义常量,不同的编译器对此采用不同的策略,在新定义不同于旧定义时,有的编译器认为这是错误,ANSI采用这种方式:即只允许新定义与旧定义完全相同。相同的定义即宏主体由相同顺序的语言符号:
// 这两个宏定义相同 |
#define 使用参数
通过使用函数,我们可以创建外形和作用都和函数相似的类函数宏。宏的参数也使用圆括号括起来,使用参数的宏定义格式如下:
#define SQ(X) X*X |
其使用看起来就像函数调用,但我们知道,它的行为和函数调用完全不一样。应该说宏与函数除了看起来很像,其它是完全不一样的!宏处理器是单纯的替换,如果用函数去理解宏,会发现很多不能理解的地方,最典型的一个问题可以参考cfaq上的一个讨论,http://c-faq.com/cpp/safemacros.html
#define SQ(x) x * x |
可以调试下r1-r4的值,就可以看出问题了,记住:预处理器只是简单的替换,不要用函数的思路去思考它。
尤其是不要在任何的宏参数中使用一元运算符(++, –),这会导致未定义的行为,你不知道编译器会怎么处理为定义行为。
前面提到过双引号中出现的宏名称不会被预处理器替换,如果要在字符串中包含宏的参数,可以在参数字符前加一个 #
作为预处理运算符,会将语言符号转换为字符串,这个过程称为字符串化(stringizing)。
#define PSQ(x) printf("the square of " #x " is %d.\n", ((x) * (x))) |
第一行的 #x
就会把x替换为宏参量。这里利用了字符串自动拼接(空格分隔的字符串会自动拼接 “…” “…”)。
##运算符:粘合剂
##
可以作为类函数宏和类对象宏的替换部分,它可以把两个语言符号组合成单个的语言符号,比如: #define XNAME(n) x ## n
这个宏,使用如 XNAME(4)
则会被替换为 x4
。考虑预处理阶段只是简单的源码文本替换,没有语法分析,所以可以用这种宏生成变量名:
#define XNAME x ## n |
是不是很有意思。
可变宏,可变参数
像printf这样的可变参数函数一样,宏定义也可以使用可变的参数,在宏定义中参数列表的最后一个参数为省略号,这样预定义宏 __VA_ARGS__
就可以用在被替换部分中,以表明省略号代表的内容。如下面的宏定义:
#define PR(X, ...) printf("Message" ##x ":" __VA_ARGS__) |
在进行宏展开的时候,VA_ARGS 会自动展开为n个参数,也是简单的替换,所以字符串部分会自动拼接。
注意可变参数(…)只能在宏参数列表的最后,这和可变参数函数是一样的。
宏,还是函数
类函数宏和函数看起来很像,可以用类函数宏来做一些函数的工作。但是使用宏很容易产生一些奇怪的现象,并且一般宏比函数更难理解。
宏与函数之间的选择其实是时间与空间的权衡,宏替换产生内联的代码,如果使用了100次宏,宏主体在代码中会出现100次,而函数则不会,但是使用宏生成的内联代码省去了函数调用的控制转移,可以节省很多时间。
宏的另一个有点是没有变量类型检查,可以对不同类型的变量使用同一个宏。
选择使用宏时,要注意一下几个问题:
- 宏的名字不能有空格
- 用圆括号括住每一个宏参数,并括住宏的主体定义,这在之前的例子中说明过
- 用大写字母作为宏的名字
常用的几个宏:
#define MAX(x, y) ((x)>(y) > (x):(y)) |
文件包含 #include
#include
是我们最早接触的预处理指令,表示将指定的文件包含到当前的文件中。被包含文件中的文本将替换源代码中的 #include
指令。
我们知道 #include 有两种形式,一种尖括号,一种双引号,尖括号告诉预处理在标准系统目录中寻找指定的文件,而双引号告诉预处理器现在当前目录(或指定的目录)寻找文件,没有则继续在标准目录寻找文件。一般标准库中的头文件使用尖括号,当前目录或者指定的目录使用双引号。 不过对于当前目录在Unix下依赖于编译器的实现,可能是当前文件所在目录,也可能是工作目录,或者工程文件所在目录。一般来说是源文件所在目录。
当然包含大型头文件并不一定会显著增加程序的大小,很多情况下,头文件中的内容是编译器产生最终代码所需要的信息,而不是加到最终代码里的具体语句。
标准头文件的常见内容如下:
- 明显常量,如EOF,NULL,BUFSIZE等
- 宏函数,例如
getchar()
经常被定义为getc(stdin)
, getc() 通常被定义为复杂的宏,而头文件 ctypes.h 通函包含ctype 函数的宏定义 - 函数声明。在 ANSI C中,声明采用函数原型形式
- 结构模板定义。如stdio.h中存放的FILE结构
- 类型定义。比如size_t 和 time_t等类型定义在头文件中
还可以使用头文件来声明多个文件共享的外部变量。定义在头文件中的 static 修饰的变量或数组(文件作用域)会在所有引入头文件的源文件中创建一个副本,该副本也是文件作用域的,它们不会相互影响。
其他预处理指令
C语言程序的跨平台编译通常借用 #define
定义的一些宏来确定所编译的平台,选择相应的C代码和库包进行编译,通过改变这些定义的宏的值,可以将代码从一个系统移植到另一个系统。#undef
指令取消之前的 #define 定义, #if
#ifdef
#else
#elif
#endif
这些指令可用于选择什么情况下编译哪些代码。 #line
指令用于重置行和文件信息。 #error
指令用于给出错误消息, #pragma
指令用于向编译器发出指令。
#undef
用于取消一个定义,即时开始没有定义这个宏,取消它也是合法的,如果想使用某个宏名字又不确定它是否已经被定义过,为了安全起见,应该取消该名字的定义。当然有一些总是认为已经被定义的宏,并且它们不能被 undef 取消定义,我们可以把这些宏看作全局定义的宏,比如 DATA, FILE 等,这类宏大多前后都是双下划线。
条件编译
可以使用宏定义告诉编译器,根据编译时的条件接收或者忽略代码块,实现条件编译。
#ifdef LINUX |
类似于这样的方式实现对不同的平台引入不同的头文件,编译不同的代码。(宏定义的缩进排版在ANSI 标准支持,就的编译器可能不支持缩进,要求 # 号顶格)。因为预处理在编译动作之前,所以可以在代码的任意位置使用上面风格的条件宏。
#ifndef
还常用来防止多次包含同一文件。什么情况下会多次包含同一个文件呢,常见的是在许多包含文件本身包含了其他文件,因此,可能显式地包含其他文件已经包含的文件。因为头文件中有些语句在一个文件中只能出现一次(强类型符号,如结构类型的声明),标准C头文件使用 #ifndef 技术避免多次包含,通常做法如下:用文件名大写化作为标识符,用下划线代替点号,使用下划线做前缀和后缀。在头文件的开头先用 #ifndef
判断这个标识符有没有定义过,如果定义过表示已经在其他地方包含了这个文件了。
比如 stdio.h 在开头就判断 #ifndef _STDIO_H
,如果没有定义,则在后续代码定义该标识符。当然在我们的头文件中,不推荐使用下划线,因为C标准保留使用下划线作为前缀,自己的头文件可以用下划线做后缀,或者不用下划线。
#if 和 #elif
这是预处理中唯一牵涉到运算的指令。 #if 后常跟常量整数表达式,如果表达式为非零值,则表达式为真。表达式中可以使用C的关系运算符和逻辑运算符。
#if SYS == 1 |
另一个常用的是 #if defined(VAX)
,defined是一个预处理器运算符。
预定义宏
前面讲过有一些预定义了全局宏,这里整理如下:
宏 | 意义 |
---|---|
DATE | 进行预处理的日期(“Mmm dd yyyy”)形式的字符串文字 |
FILE | 代表当前源代码文件名中的字符串文字 |
LINE | 代表当前源代码中的行号和整数常量 |
STDC | 设置为1时表示实现遵循 C 标准 |
STDC_HOSTED | 为本机环境时设置为1,否则为0 |
STDC_VERSION | 为C99时设置为 199901L |
TIME | 源文件编译时间,格式为”hh:mm:ss” |
C99 标准提供一个名为 func 的预定义标识符, func 标识符展开为一个代表函数名的字符串(当前函数的名称),该标识具有函数作用域,而宏本身具有文件作用域。 注意 func 是C语言的预定义标识符,而不是预定义宏。
重置行号和文件,以及#error
可以用 #line指令重置 LINE 和 FILE 宏报告的行号和文件名,其用法如下:
#line 1000 // 设置当前行号为 1000 |
#error
指令用于使预处理器发出一条错误消息,该消息包含指令中的文本,可能的话,编译过程应该中断,其用法如下:
#if __STDC_VERSION__ != 19901L |
另一个指令 #pragma
用于指定编译器的某些参数设置。 比如使用 #pragma C9x on
启用对C9X(C99)的支持。其他的感觉一般用不到,在这里不细看了。
内联函数
C99中提供了内联函数的支持,“把函数变为内联函数将建议编译器尽可能快速地调用该函数,上述建议的效果由实现来定义”。由于依赖于具体的编译器行为,所以使函数作为内联函数可能会简化函数调用机制,也可能不起作用。
创建内联函数的方法是在函数声明中使用 inline
说明符,通常,首次使用内联函数前在文件中对该函数进行定义,该定义也可以作为函数原型。
内联函数不会在调试器中显示。
编译器优化内联函数时,必须知道函数定义的内容(而不只是声明),这意味着内联函数的定义和对该函数的调用必须在同一个文件。一次内联函数通常具有内部链接,在多文件程序中,每个调用内联函数的文件都要对该函数进行定义(而不是声明)。可以通过在头文件中定义该内联函数实现。虽然一般不再头文件中放置可执行代码,但内联函数是一个例外。因为内联函数是内部链接的,所以在头文件中定义实现并不会产生什么问题。
C允许混合使用内联函数定义和外部函数定义,即一个文件中定义的内联函数也可以有一个外部函数定义。C++不允许这样的行为,如:
inline double sq(double); |
C编译器在编译这个文件使用内联定义,而在其他文件使用 sq 时使用外部函数定义。实际使用不推荐这么做,但是应该知道可以这么做。
完