工作原理

预处理器的行为是由预处理指令( 由 # 字符开头的一些命令)控制的,以 #define 和 # include 为例。

define 指令定义了一个宏——用来代表其他东西的一个名字,例如常量或常用的表达式。预处理器会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时,预处理器“扩展”宏,将宏替换为其定义值。

include 指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。如#include<stdio.h> 指示预处理器打开一个名字为 stdio.h 的文件,并将它的内容加到当前的程序中。(stdio.h包含了C语言标准输入/输出函数的原型。)

image.png

预处理指令

大多数预处理指令属于下面 3 种类型。

  • 宏定义。#define 指令定义一个宏,#undef 指令删除一个指令。
  • 文件包含。#include 指令导致一个指定文件的内容被包含到程序中。
  • 条件编译。#if、#ifdef、#ifndef、#elif、#else 和 #endif 指令可以根据预处理器可以测试的条件来确定是将一段文本块包含到程序中还是将其排除在程序外。

error、#line、#pragma 指令较少用到。

宏定义

简单宏

格式:#define 标识符 替换列表

宏的替换列表可以包含标识符、关键字、数值常量、字符常量、字符串常量、操作符和排列。当预处理器遇到一个宏定义时,会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在哪里出现,预处理器都会用替换列表代替它。

优点:

  • 程序更易读。
  • 程序更易于修改。
  • 可以帮助避免前后不一致或键盘输入错误。
  • 可以对 C 语法做小的修改。
  • 对类型重命名 如:#define BOOL int
  • 控制条件编译。

带参数的宏

格式:#define 标识符(x1, x2, … , xn) 替换列表

例子

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #define MAX(x, y) ((x) > (y) ? (x) : (y))
  4. #define IS_EVEN(n) ((n) % 2 == 0)
  5. #define TOUPPER(c) ('a' <= (c) && (c) <= 'z' ? (c) - 'a' + 'A' : (c))
  6. int main(void) {
  7. printf("max: %d\n", MAX(10, 12));
  8. printf("is_even: %d\n", IS_EVEN(20));
  9. printf("toupper: %c\n", TOUPPER('a'));
  10. system("pause");
  11. return 0;
  12. }

替换列表中的括号是有必要的,哪里需要加括号有两条规则需要遵守。

  • 如果替换列表中有运算符需要加。
  • 如果宏有参数,每个参数每次在替换列表中出现时都要放在圆括号里。

例子

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #define TWO_PI 2*3.14159 // 没加括号,正确形式 (2*3.14159)
  4. #define SCALE(x) (x*10) // 没加括号,正确形式 ((x)*10)
  5. int main(void) {
  6. float conversion_factor = 360/TWO_PI; // 除法在乘法之前执行,产生的结果并不是期望的结果。
  7. int j = SCALE(i+1); // 由于乘法的优先级比加法高,等价于 j = i * 10 我们希望是 j = (i + 1) * 10
  8. system("pause");
  9. return 0;
  10. }

特点

  • 程序可能会稍微快一点。
  • 宏更加通用。
  • 编译后的代码通常会更大。
  • 宏参数没有类型检查。
  • 无法用一个指针来指向一个宏。
  • 宏可能会不止一次地计它的参数。

#运算符

预算符将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中。可以#运算符所执行的操作理解为“字符串化(stringization)”。

例子

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #define PRINT_INT(n) printf(#n " = %d\n", n)
  4. int main(void) {
  5. int a = 10, b = 5;
  6. PRINT_INT(a/b); // printf("a/b = %d\n", a/b);
  7. system("pause");
  8. return 0;
  9. }

##预算符

运算符可以将两个(如标识符)“粘合”在一起,成为一个记号。如果一个参数是宏参数,“粘合”会在形式参数被相应的实际参数替换后发生。

例子

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #define MK_ID(n) i##n
  4. int main(void) {
  5. int MK_ID(1), MK_ID(2), MK_ID(3); // int i1, i2, i3;
  6. system("pause");
  7. return 0;
  8. }
  1. #include <stdio.h>
  2. #include <windows.h>
  3. #define GENERIC_MAX(type) type type##_max(type x, type y) { return x > y ? x : y; }
  4. int main(void) {
  5. GENERIC_MAX(float) // float float_max(float x, float y) { return x > y ? x : y }
  6. system("pause");
  7. return 0;
  8. }

其他

  • 宏的替换列表可以包含对其他宏的调用。

预处理器会不断重新检查替换列表,直到将所有的宏名字都替换掉为止。

  1. #define PI 3.14169
  2. #define TWO_PI (2*PI)
  • 预处理器只会替换万正的记号,而不会替换记号的片段。

预处理器会忽略嵌在标识符、字符常量、字符串字面量之中的宏名。

  1. #define SIZE 256
  2. int BUFFER_SIZE;
  3. if(BUFFER_SIZE > )
  4. {
  5. puts("Error: SIZE exceeded");
  6. }
  7. // 预处理后
  8. // int BUFFER_SIZE;
  9. // if(BUFFER_SIZE > 256)
  10. // {
  11. // puts("Error: SIZE exceeded");
  12. // }
  • 宏定义的作用范围通常到出现这个宏的文件末尾。

由于宏是有预处理器处理的,它们不遵从通常的作用域规则。定义在函数中的宏并不是仅在函数内起作用,而是
作用到文件末尾。

  • 不可以被定义两遍,除非新的定义与旧的定义是一样的。

小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。

  • 宏可以使用 #undef 指令“取消定义”,如: #undef N

条件编译

条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片断。

#if 指令和 #endif 指令

格式:

if 常量表达式

endif

  1. #define DEBUG 1
  2. #if DEBUG
  3. printf("Value of i: %d\n", i);
  4. printf("Value of j: %d\n", j);
  5. #endif

在预处理过程中,#if 指令会测试 DEBUG 的值。由于DEBUG的值不是 0,因此预处理器会将这两个 printf 函数调用保留在程序中(但 #if 和 #endif 行会消失)。如果我们将 DEBUG 的值改为 0 并重新编译程序,预处理器则会将这 4 行代码都删除。

defined 预算符

  1. #define DEBUG
  2. #if defined(DEBUG) // 或者 #defined DEBUG 括号不是必需
  3. printf("Value of i: %d\n", i);
  4. printf("Value of j: %d\n", j);
  5. #endif

仅当 DEBUG 被定义成宏时,#if 和 #endif 之间的代码会被保留在程序中。

#ifdef 指令和 #ifndef 指令

ifdef

格式:

ifdef 标识符

endif

语法糖

if defined(标识符)

endif

ifndef

格式:

ifndef 标识符

endif

语法糖

if !defined(标识符)

endif

#elif 指令和 #else 指令

elif 指令和 #else 指令可以与 #if 指令、#ifdef 指令和 #ifndef 指令结合使用,来测试一系列条件。

if 表达式1

当表达式非 0 时需要包含的代码

elif 表达式2

当表达式1 为 0 但表达式2 非 0 时需要包含的代码

else

其他情况下需要包含的代码

endif

上面使用了 #if 指令,但 #ifdef 指令或 #ifndef 指令也可以这样使用。在 #if 指令和 #endif 指令之间可以有任意多个 #elif 指令,但最多只有一个 #else 指令。

条件编译的用处

  • 编写在多台机器或多种操作系统之间的可移植的程序。

    1. #if defined(WIN32)
    2. ...
    3. #elif defined(MAC_OS)
    4. ...
    5. #elif defined(LINUX)
    6. ...
    7. #endif
  • 编写可以用不用的编译器编译的程序。

  • 为宏提供默认的定义。

    1. #ifndef BUFFER_SIZE
    2. #define BUFFER_SIZE 256
    3. #endif
  • 临时屏蔽包含注释的代码。

    1. #if 0
    2. // 包含注释的代码行
    3. #endif