11、预处理指令

厨子大约 5 分钟C语言基础程序程序厨

预处理指令是C语言(以及C++)中用于在编译之前对源代码进行处理的指令。这些指令以#字符开头,并且不产生任何机器代码,而是对源代码进行文本替换、条件编译或包含其他文件等操作。

宏定义与宏替换

宏定义

宏定义是使用#define预处理指令来创建一个符号常量或宏函数。符号常量是一个简单的文本替换,而宏函数则可以接受参数,并在预处理阶段进行文本替换。

  • 符号常量#define PI 3.14159

这行代码定义了一个名为PI的符号常量,它在代码中的每次出现都会被替换为3.14159。

  • 宏函数#define SQUARE(x) ((x) * (x))

这行代码定义了一个名为SQUARE的宏函数,它接受一个参数x,并返回x的平方。注意,宏函数中的参数在替换时不会进行类型检查或边界检查,它只是单纯的替换。

宏替换

宏替换是在预处理阶段进行的,编译器在编译之前会将源代码中的宏名称替换为其定义的值或代码片段。

示例代码

#include <stdio.h>

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

int main() {
    double radius = 5.0;
    double area = PI * SQUARE(radius); // 宏替换为:area = 3.14159 * ((5.0) * (5.0))

    printf("Area of circle: %.2f\n", area);
    return 0;
}

提问:#define SQUARE(x) ((x) * (x)),为什么这里是((x) * (x)),而不是(x) * (x)

文件包含与条件编译

文件包含

文件包含是使用#include预处理指令来将一个文件的内容包含到另一个文件中。通常用于包含标准库头文件或自定义头文件。

  • 标准库头文件#include <stdio.h>

这行代码包含了标准输入输出库的头文件,使得程序可以使用printf()等函数。

  • 自定义头文件#include "myheader.h"

这行代码包含了名为myheader.h的自定义头文件,该头文件可能包含了一些函数声明、宏定义等。

条件编译

条件编译是使用#if#elif#else#endif等预处理指令来根据条件选择性地编译代码片段。通常用于跨平台编程或根据不同的编译选项来包含或排除代码。

示例代码

#include <stdio.h>

// 假设我们有一个条件编译的宏
#define DEBUG_MODE

int main() {
    // 条件编译示例
#ifdef DEBUG_MODE
    printf("Debug mode is enabled.\n");
#else
    printf("Debug mode is disabled.\n");
#endif

    // 无论DEBUG_MODE是否定义,下面的代码都会被编译
    printf("Program is running.\n");

    return 0;
}

在这个例子中,如果定义了DEBUG_MODE宏,则程序会输出"Debug mode is enabled.",否则输出"Debug mode is disabled."。

高级用法

编译指令

编译指令通常指的是用于控制编译过程的预处理指令,如#pragma。这些指令的具体作用取决于编译器,并且不是C语言标准的一部分。例如,某些编译器可能使用#pragma来优化代码、控制警告和错误信息的输出等。

预定义宏

ANSI C 标准定义了一些预定义宏,这些宏在编译时自动可用,无需显式定义。常见的预定义宏包括 __DATE____TIME____FILE____LINE__ 等。

示例:

printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Date: %s\n", __DATE__);
printf("Time: %s\n", __TIME__);

这些代码将输出当前文件名、行号、编译日期和时间,以及是否以 ANSI C 标准编译的信息。

宏延续运算符(\)

当宏定义过长,一行无法容纳时,可以使用宏延续运算符(\)将宏定义拆分到多行。

示例:

#define LONG_MACRO(a, b) \
    do { \
        int temp = (a) + (b); \
        printf("Temp: %d\n", temp); \
    } while (0)

这个宏定义了一个多行的代码块,使用 do { ... } while (0) 结构确保宏在使用时能够作为一个单独的语句。

字符串常量化运算符(#)

字符串常量化运算符(#)用于将宏参数转换为字符串常量。

示例:

#define STRINGIFY(x) #x
printf("%s\n", STRINGIFY(Hello, World!));

输出将是 "Hello, World!",其中 Hello, World! 被转换为了字符串常量。

标记粘贴运算符(##)

标记粘贴运算符(##)用于将两个宏参数合并为一个标记。这在创建具有动态名称的变量或函数时非常有用。

示例:

#define CONCAT(a, b) a##b
int CONCAT(my, Var) = 10;
printf("%d\n", myVar);

这里,CONCAT(my, Var) 被替换为 myVar,并且 myVar 被初始化为 10。输出将是 10。

defined() 运算符

defined() 运算符用于检查某个宏是否已定义。如果已定义,则返回非零值;否则返回零。

示例:

#if defined(DEBUG)
    printf("Debug mode is defined.\n");
#else
    printf("Debug mode is not defined.\n");
#endif

如果定义了 DEBUG 宏,则输出 "Debug mode is defined.";否则输出 "Debug mode is not defined."。

行号控制

行号控制通常不是通过预处理指令直接实现的,而是由编译器在编译过程中自动处理的。然而,预处理指令如#line可以用于修改编译器报告的当前行号和文件名,这在某些情况下可能很有用,比如当源代码是通过某种方式生成的,并且生成的源代码中的行号对于调试来说没有意义时。

**#line**示例

#include <stdio.h>

#line 100 "generated_code.c"
// 这行代码告诉编译器,接下来的代码应该被认为是在名为"generated_code.c"的文件的第100行开始的。

int main() {
    printf("This is a line in generated code.\n"); // 编译器会报告这行代码位于generated_code.c的第101行。
    return 0;
}

运行结果如下(https://godbolt.org/z/KeaK76r4G):open in new window

img
img

需要注意的是,#line指令通常不会在手动编写的源代码中频繁使用,而是更多地用于由工具或脚本生成的代码中。

练习

  1. 了解#pragma once指令的作用