文章目录
- C预处理器和C库
- 16.1翻译程序的第一步
- 16.2明示常量:#define
- 16.3在#define中使用参数
- 16.3.1用宏参数创建字符串:#运算符
- 16.3.2预处理器黏合剂:##运算符
- 16.3.3变参宏:...和__VA_ARGS__
- 16.4宏和函数的选择
- 16.5文件包含:#include
- 16.5.2使用头文件
- 16.6其他指令
- 16.6.1#undef指令
- 16.6.2从c预处理器角度看已定义
- 16.6.3条件编译
- 1.#ifdef、#else和#endif指令
- 2.#ifndef指令
- 3.#if和#elif指令
- 16.6.4预定义宏
- 16.6.5#line和#error
- 16.6.6#pragma
- 16.6.7泛型选择(C11)
- 16.7内联函数(C99)
- 16.8_Noreturn函数(C11)
C预处理器和C库
C预处理器在程序执行之前查看程序。根据程序中的预处理器指令,预处理器把符号缩写替换成其表示的内容。预处理器可以包含程序所需的其他文件,可以选择让编译器查看哪些代码。预处理器并不知道c。基本上它的工作是把一些文本转换成另外一些文本。
16.1翻译程序的第一步
在预处理之前,编译器必须对该程序进行一些翻译处理:
- 编译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字符序列。
- 编译器定位每个反斜杠后面跟着换行符(不是
\n
,而是通过按下enter键在源代码文件中换行所生成的字符)的实例,并删除它们。由于预处理表达式的长度必须是一个逻辑行,所以这一步为预处理器做好了准备工作。一个逻辑行可以是多个物理行。
printf("That's wond\
erful!\n");
// 转换成一个逻辑行:
printf("That's wonderful!\n");
- 编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项)。要注意的是,编译器将用一个空格字符替换每一条注释。而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。
- 程序已经准备好进入预处理阶段,预处理器查找一行中以
#
号开始的预处理指令。
16.2明示常量:#define
#define
以#
作为一行的开始。ANSI和后来的标准都允许#
前面有空格或制表符,而且还允许在#
和指令的其余部分之间有空格。但是旧版的c要求指令从一行最左边开始,而且#
和指令其余部分之间不能有空格。
指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。大量使用#define
指令来定义明示常量(也叫作符号常量)。
预处理器指令从#
开始运行,到后面的第1个换行符为止。也就是说,指令的长度仅限于一行。然而,在预处理开始前,编译器会把多行物理行处理为一行逻辑行。
#include <stdio.h>
#define TWO 2 /* 可以使用注释,每条注释都会被一个空格代替 */
#define OW "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde" /* 反斜杠把该定义延续到下一行,注意,第2行要与第1行左对齐,否则会输出多余的空格 */
#define FOUR TWO*TWO
#define PX printf("X is %d.\n", x)
#define FMT "X is %d.\n"
int main(void) {
// 变成了:int x = 2;
int x = TWO;
// 变成了:printf("X is %d.\n", x);
PX;
// 变成了:x = TWO*TWO;
x = FOUR;
// 变成了:printf("X is %d.\n", x);
printf(FMT, x);
// 变成了:printf("%s\n", "Consistency is the last refuge of the unimaginative. - Oscar Wilde");
printf("%s\n", OW);
// 不会变
printf("TWO: OW\n");
return 0;
}
由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理器不会进行实际的算术运算,这一过程在编译时进行。预处理器不做计算,不对表达式求值,它只进行替换。
16.3在#define中使用参数
在
#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 = 5;
int z;
printf("x = %d\n", x);
z = SQUARE(x);
printf("Evaluating SQUARE(x): ");
PR(z);
z = SQUARE(2);
printf("Evaluating SQUARE(2): ");
PR(z);
printf("Evaluating SQUARE(x+2): ");
// 此时,X*X替换成:x + 2*x + 2。可以将宏修改为:#define SQUARE(X) (X)*(X)以解决这样的问题。
// 但是,并未解决所有的问题,例如下面的代码行。
PR(SQUARE(x + 2));
// 此时,X*X替换成:100 / 2*2(即便是使用上面优化后的宏)。因此,要解决这两种情况,
// 要这样定义:#define SQUARE(X) ((X)*(X))。因此,必要时要使用足够多的圆括号来
// 确保运算和结合的正确顺序。
printf("Evaluating 100/SQUARE(2): ");
PR(100 / SQUARE(2));
printf("x is %d.\n", x);
printf("Evaluating SQUARE(++x): ");
// 但是,仍然无法避免这种情况,此时,X*X替换成:++x*++x(即便是使用上面优化后的宏),
// 不同的编译器会有不同的结果。例如,42或者49。
PR(SQUARE(++x));
printf("After incrementing, x is %x.\n", x);
return 0;
}
16.3.1用宏参数创建字符串:#运算符
C允许在字符串中包含宏参数。在类函数宏的替换体中,
#
号作为一个预处理运算符,可以把记号转换成字符串。
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.\n", ((x)*(x)))
int main(void) {
int y = 5;
// 用"y"替换#x
PSQR(y);
// 用"2 + 4"替换#x
PSQR(2 + 4);
return 0;
}
/*输出结果:
The square of y is 25.
The square of 2 + 4 is 36.
*/
16.3.2预处理器黏合剂:##运算符
##
运算符可用于类函数宏的替换部分。而且,##
还可用于对象宏的替换部分。##
运算符把两个记号组合成一个记号。
// 此时,XNAME(4)将展开为x4
#define XNAME(n) x ## n
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void) {
int XNAME(1) = 14; // 变成int x1 = 14;
int XNAME(2) = 20; // 变成int x2 = 20;
int x3 = 30;
PRINT_XN(1); // 变成printf("x1 = %d\n", x1);
PRINT_XN(2); // 变成printf("x2 = %d\n", x2);
PRINT_XN(3); // 变成printf("x3 = %d\n", x3);
return 0;
}
16.3.3变参宏:…和__VA_ARGS__
一些函数(如
printf()
)接受数量可变的参数。stdvar.h
头文件提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。
通常把宏参数列表中最后的参数写成省略号(...
)来实现这一功能。这样,预定义宏__VA_ARGS__
可用在替换部分中,表明省略号代表什么。
#define PR(...) printf(__VA_ARGS__)
// __VA_ARGS__展开为1个参数:"Howdy",展开后的代码是:printf("Howdy");
PR("Howdy");
// __VA_ARGS__展开为3个参数:"weight = %d, shipping = $%.2f\n"、wt、sp。
// 展开后的代码是:printf("weight = %d, shipping = $%.2f\n", wt, sp);
PR("weight = %d, shipping = $%.2f\n", wt, sp);
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void) {
double x = 48;
double y;
y = sqrt(x);
// 展开后的代码是:printf("Message 1: x = %g\n", x);
PR(1, "x = %g\n", x);
// 展开后的代码是:printf("Message 2: x = %.2f, y = %.4f\n", x, y);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}
16.4宏和函数的选择
使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。一些编译器规定宏只能定义成一行。不过,即使编译器没有这个限制,也应该这样做。
宏和函数的选择实际上是时间和空间的权衡。宏产生内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。
宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)。因此,只要能用int
或float
类型都可以用SQUARE(x)
宏。
C99提供了第3中可替换的方法,内联函数。
对于简单的函数,通常使用宏:
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0)
要记住以下几点:
- 宏名中不允许有空格,但是再替换字符串中可以有空格。ANSI C允许在参数列表中使用空格。
- 用圆括号把宏的参数和整个替换体括起来。这样能确保被括起来的部分能正确地展开:
forks = 2 * MAX(guests + 3, last);
- 用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛。但是,大写字母可以提醒程序员注意,宏可能产生的副作用。
- 如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。
16.5文件包含:#include
16.5.2使用头文件
头文件中最常用的形式:
- 明示常量。
- 宏函数。
- 函数声明。
- 结构模板定义。
- 类型定义。
另外,还可以使用头文件声明外部变量供其他文件共享。例如,如果已经开发了共享某个变量的一系列函数,该变量报告某种状况(如,错误情况),这种方法就很有效。这种情况下,可以在包含这些函数声明的源代码文件定义一个文件作用域的外部链接变量:
int status = 0; // 该变量具有文件作用域,在源代码文件。
然后,可以在与源代码文件相关联的头文件中进行引用式声明:
extern int status; // 在头文件中
这行代码会出现在包含了该头文件的文件中,这样使用该系列函数的文件都能使用这个变量。虽然源代码文件中包含该头文件后也包含了该声明,但是只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没问题。
需要包含头文件的另一种情况是,使用具有文件作用域、内部链接和const
限定符的变量或数组。const
防止值被意外修改,static
意味着每个包含该头文件的文件都获得一份副本。因此,不需要在一个文件中进行定义式声明,在其他文件中进行引用式声明。
16.6其他指令
16.6.1#undef指令
#undef
指令用于取消已定义的#define
指令。
#define LIMIT 400
// 移出上面的定义,现在可以把LIMIT重新定义为一个新值。
#undef LIMIT
即使原来没有定义,取消定义仍然有效。如果想使用一个名称,又不确定之前是否已经用过,为安全起见,可以用
#undef
指令取消该名字的定义。
16.6.2从c预处理器角度看已定义
当预处理器在预处理器指令中发现一个标识符时,它会把该标识符当作已定义的或未定义的。这里的已定义表示由预处理器定义。
已定义宏可以是对象宏,包括空宏或类函数宏:
#define LIMIT 1000 // LIMIT是已定义的
#define GOOD // GOOD是已定义的
#define A(X) ((-(X))*(X)) // A是已定义的
int q; // q不是宏,因此是未定义的。
#undef GOOD // GOOD取消定义,是未定义的。
#define
宏的作用域从它在文件中的声明处开始,直到用#undef
指令取消宏为止,或延伸至文件尾(以二者中先满足的条件作为宏作用域的结束)。另外还要注意,如果宏通过头文件引入,那么#define
在文件中的位置取决于#include
指令的位置。
16.6.3条件编译
1.#ifdef、#else和#endif指令
// 如果使用旧的编译器,必须左对齐所有的指令或至少左对齐#号:
#ifdef MAVIS
#include "horse.h" // 如果已经用#define定义了MAVIS,则执行下面的指令。
#define STABLES 5
#else
#include "cow.h" // 如果没有用#define定义MAVIS,则执行下面的指令。
// 必须存在
#endif
2.#ifndef指令
#ifndef
指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。
#ifndef SIZE
#define SIZE 100
#endif
#ifndef
指令通常用于防止多次包含一个文件。也就是说,应该这样设置头文件:
/* things.h,当预处理器第2次发现该文件被包含时,THINGS_H_是已定义的,预处理会跳过该文件的其他部分。 */
#ifndef THINGS_H_
#define THINGS_H_
/* 省略头文件中的其他内容 */
#endif
3.#if和#elif指令
#if
后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用c的关系运算符和逻辑运算符:
#if SYS == 1
#include "ibm.h"
#endif
可以按照if-else的形式使用
#elif
(早起的实现不支持#elif
):
#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else
#include "general.h"
#endif
较新的编译器提供另一种方法测试名称是否已定义,即用
#if defined (VAX)
代替#ifdef VAX
。
这里,defined
是一个预处理运算符,如果它的参数是用#define
定义过,则返回1;否则返回0。这种新方法的优点是,它可以和#elif
一起使用:
// 如果在VAX机上运行代码,则应该在文件前定义VAX:
#define VAX
#if defined (IBMPC)
#include "ibmpc.h"
#elif defined (VAX)
#include "vax.h"
#elif defined (MAC)
#include "mac.h"
#else
#include "general.h"
#endif
条件编译还有一个用途是让程序更容易移植。改变文件开头部分的几个关键的定义,即可根据不同的系统设置不同的值和包含不同的文件。
16.6.4预定义宏
C99标准提供一个名为
__func__
的预定义标识符,它展开为一个代表函数名的字符串(该函数包含该标识符)。那么,__func__
必须具有函数作用域,而从本质上看宏具有文件作用域。因此,__func__
是c语言的预定义标识符,而不是预定义宏。
16.6.5#line和#error
#line
指令重置__LINE__
和__FILE__
宏报告的行号和文件名:
#line 1000 // 把当前行号重置为1000
#line 10 "cool.c" // 把行号重置为10,把文件名重置为cool.c。
#error
指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断:
// 如果编译器只支持旧标准,则会编译失败。
#if __STDC__VERSION__ != 201112L
#error Not C11
#endif
16.6.6#pragma
#pragma
把编译器指令放入源代码中。
// 在开发C99时,标准被称为C9X,可以使用下面的编译指示让编译器支持C9X。一般而言,编译器都有自己的
// 编译指示集。例如,编译指示可能用于控制分配给自动变量的内存量,或者设置错误检查的严格程度,或者
// 启用非标准语言特性等。C99标准提供了3个标准编译指示,但是目前不在讨论范围中。
#pragma c9x on
C99还提供
_Pragma
预处理器运算符,该运算符把字符串转换成普通的编译指示:
_Pragma("nonstandardtreatmenttypeB on")
// 等价于:
#pragma nonstandardtreatmenttypeB on
// 由于该运算符不使用#符号,所以可以把它作为宏展开的一部分:
#define PRAGMA(X) _Pragma(#X)
#define LIMRG(X) PRAGMA(STDC CX_LIMITED_RANGE X)
// 然后,可以这样使用:
LIMRG( ON )
// 另一方面,下面的定义看上去没问题,但实际上无法正常运行。问题在于代码依赖字符串的串联功能,
// 而预处理过程完成之后才会串联字符串。
#define LIMRG(X) PRAGMA(STDC CX_LIMITED_RANGE #X)
_Pragma
运算符完成解字符串的工作,即把字符串中的转义序列转换成它所代表的字符:
_Pragma("use_bool \"true \"false")
// 变成了:
#pragma use_bool "true "false
16.6.7泛型选择(C11)
C11新增了泛型选择表达式,可根据表达式的类型(即表达式的类型是
int
、double
还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define
宏定义的一部分。
// 第1个项是一个表达式,后面的每个项都由一个类型、一个冒号和一个值组成。
// 第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。
_Generic(x, int: 0, float: 1, double: 2, default: 3)
// 宏必须定义为一条逻辑行,但是可以用`\`把一条逻辑行分隔成多条物理行。
#define MYTYPE(x) _Generic((X), \
int: "int",\
float: "float",\
double: "double",\
default: "other"\
)
对一个泛型选择表达式求值时,程序不会先对第一个项求值,它只确定类型。只有匹配标签的类型后才会对表达式求值。
16.7内联函数(C99)
通常,函数调用都有一定的开销。使用宏使代码内联,可以避免这样的开销,但是也可能不起作用。
标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。因此,最简单的方法是使用函数说明符inline
和存储类别说明static
。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型:
#include <stdio.h>
inline static void eatline() { // 内联函数定义/原型
while (getchar() != '\n') {
continue;
}
}
int main(void) {
// ...
eatline(); // 函数调用
// ...
}
由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。
编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。因此,如果程序有多个文件都要使用某个内联函数,最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。
一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。
与c++不同的是,c还允许混合使用内联函数定义和外部函数定义(具有外部链接的函数定义):
// file1.c
// ...
// inline static定义
inline static double square(double);
double square(double x) { return x * x; }
int main(void) {
// 使用square()的局部static定义。由于该定义也是inline定义,
// 所以编译器有可能优化代码,也许会内联该函数。
double q = square(1.3);
// ...
}
// file2.c
// ...
// 普通的函数定义(因此具有外部链接)
double square(double x) { return (int) (x*x); }
void spam(double v) {
// 使用该文件中square()函数的定义,该定义具有外部链接,其他文件也可见。
double kv = square(v);
// ...
}
// file3.c
// ...
// inline定义,省略了static。
inline double square(double x) { return (int) (x * x + 0.5); }
void masp(double w) {
// 编译器既可以使用该文件中square()函数的内联定义,也可以使用file2.c文件
// 中的外部链接定义。如果像file3.c那样,省略file1.c文件inline定义中的static,
// 那么该inline定义被视为可替换的外部定义。
double kw = square(w);
// ...
}
注意GCC在C99之前就使用一些不同的规则实现了内联函数,所以GCC可以根据当前编译器的标记来解释
inline
。
16.8_Noreturn函数(C11)
函数说明符
_Noreturn
,表明调用完成后函数不返回主调函数。exit()
函数是_Noreturn
函数的一个示例。注意,void
类型的函数在执行完毕后返回主调函数,只是它不提供返回值。
_Noreturn
的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码。