跳转至

预处理器

本文为《C 与 指针读书笔记》,感兴趣的读者可以去看原书。

预定义符号

由预处理器 定义的符号。它们的值或者是字符串常量,或者是十进制数字常量。__FILE__和__LINE__在确认调试输出的来源方面很有用处。__DATE__和__TIME__常常用于在被编译的程序中加入版本信息。__STDC__用于那些在ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译

define

你已经见过 define指令的一些简单用法,就是为数值命名一个符号。

#define name  stuff

有了这条指令以后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff。

替换文本并不仅限于数值字面值常量。使用#define指令,你可以把任何 文本替换到程序中。

如果定义中的stuff非常长,它可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠。

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏 (macro)或定义宏(defined macro)。下面是宏的声明方式:

define name(parameter-list

)  stuff

其中,parameter-list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

当宏被调用时,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。当参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。

#define SQUARE(x)  x * x
SQUARE( 5 )

define替换

在程序中扩展 define定义符号和宏时,需要涉及几个步骤。

1.在调用宏时,首先对参数进行检查,看看是否包含了任何由 define定义的符号。如果是,它们首先被替换。

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代。

3.最后,再次对结果文本进行扫描,看看它是否包含了任何由define定义的符号。如果是,就重复上述处理过程。

这样,宏参数和define定义可以包含其他define定义的符号。但是,宏不可以出现递归。

宏与函数

宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大(或较小)的一个,为什么不用函数来完成这个任务呢?有两个原因。首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。

但是,更为重要的是,函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用。反之,上面这个宏可以用于整型、长整型、单浮点型、双浮点数以及其他任何可以用>操作符比较值大小的类型。换句话说,宏是与类型无关的。

和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏可能会大幅度增加程序的长度。

带副作用的宏参数

当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果。副作用 就是在表达式求值时出现的永久性效果。

命令行定义

许多C编译器提供了一种能力,允许你在命令行中定义符号,用于启动编译过程。当我们根据同一个源文件编译一个程序的不同版本时,这个特性是很有用的。

条件编译

在编译一个程序时,如果我们可以选择某条语句或某组语句进行翻译或者被忽略,常常会显得很方便。只用于调试程序的语句就是一个明显的例子。它们不应该出现在程序的产品版本中,但是你可能并不想把这些语句从源代码中物理删除,因为如果需要一些维护性修改时,你可能需要重新调试这个程序,还需要这些语句。

条件编译(conditional compilation)就是用于实现这个目的。使用条件编译,你可以选择代码的一部分是被正常编译还是完全忽略。用于支持条件编译的基本结构是if指令和与其匹配的endif指令。

文件包含

你已经看到过,include指令使另一个文件的内容被编译,就像它实际出现于 include指令出现的位置一样。这种替换执行的方式很简单:预处理器删除这条指令,并用包含文件的内容取而代之。这样,一个头文件如果被包含到10个源文件中,它实际上被编译了10次。

当头文件被包含时,位于头文件内的所有内容都要被编译。这个事实意味着每个头文件只应该包含一组函数或数据的声明。和把一个程序需要的所有声明都放入一个巨大的头文件相比,使用几个头文件,每个头文件包含用于某个特定函数或模块的声明的做法更好一些。

编译器支持两种不同类型的include文件包含:函数库文件和本地文件。

函数库头文件包含使用下面的语法。

#include <filename>

本地文件包含使用另一种语法:

#include "filename"

总结

编译一个C程序的第1个步骤就是对它进行预处理。预处理器共支持5个符号

define指令把一个符号名与一个任意的字符序列联系在一起。例如,这些字符可能是一个字面值常量、表达式或者程序语句。这个序列到该行的末尾结束。如果该序列较长,可以把它分开数行,但在最后一行之外的每一行末尾加一个反斜杠。宏就是一个被定义的序列,它的参数值将被替换。当一个宏被调用时,它的每个参数都被一个具体的值替换。为了防止可能出现于表达式中的与宏有关的错误,在宏完整定义的两边应该加上括号。同样,在宏定义中每个参数的两边也要加上括号。define指令可以用于“重写”C语言,使它看上去像是其他语言。

argument结构由预处理器转换为字符串常量“argument”。##操作符用于把它两边的文本粘贴成同一个标识符。

有些任务既可以用宏也可以用函数实现。但是,宏与类型无关,这是一个优点。宏的执行速度快于函数,因为它不存在函数调用/返回的开销。但是,使用宏通常会增加程序的长度,但函数却不会。同样,具有副作用的参数可能在宏的使用过程中产生不可预料的结果,而函数参数的行为更容易预测。由于这些区别,使用一种命名约定,让程序员很容易地判断一个标识符是函数还是宏是非常重要的。

在许多编译器中,符号可以从命令行定义。#undef指令将导致一个名字的原来定义被忽略。

使用条件编译,你可以从一组单一的源文件创建程序的不同版本。#if指令根据编译时测试的结果,包含或忽略一个序列的代码。当同时使用#elif#else指令时,你可以从几个序列的代码中选择其中之一进行编译。除了测试常量表达式之外,这些指令还可以测试某个符号是否已被定义。#ifdef#ifndef指令也可以执行这个任务。

#include指令用于实现文件包含。它具有两种形式。如果文件名位于一对尖括号中,编译器将在由编译器定义的标准位置查找这个文件。这种形式通常用于包含函数库头文件时。另一种形式,文件名出现在一对双引号内。不同的编译器可以用不同的方式处理这种形式。但是,如果用于处理本地头文件的任何特殊处理方法无法找到这个头文件,那么编译器接下来就使用标准查找过程来寻找它。这种形式通常用于包含你自己编写的头文件。文件包含可以嵌套,但很少需要进行超过一层或两层的文件包含嵌套。嵌套的包含文件将会增加多次包含同一个文件的危险,而且使我们更难以确定某个特定的源文件依赖的究竟是哪个头文件。

#error指令在编译时产生一条错误信息,信息中包含的是你所选择的文本。#line指令允许你告诉编译器下一行输入的行号,如果它加上了可选内容,它还将告诉编译器输入源文件的名字。因编译器而异的#progma指令允许编译器提供不标准的处理过程,比如向一个函数插入内联的汇编代码。

编程练习

练习 1

写一个函数,返回一个值,提示运行这个函数的计算机的类型。这个函数将由一个能够运行于许多不同计算机的程序使用。

#define CPU_UNKNOWN 0

// 假设这些是可能被定义的宏的示例
#define CPU_X86 1
#define CPU_X64 2
#define CPU_ARM 3
#define CPU_MIPS 4

int cpu_type(void) {
    #if defined(CPU_X86)
        return CPU_X86;
    #elif defined(CPU_X64)
        return CPU_X64;
    #elif defined(CPU_ARM)
        return CPU_ARM;
    #elif defined(CPU_MIPS)
        return CPU_MIPS;
    #else
        return CPU_UNKNOWN;
    #endif
}

最后更新: November 17, 2024