什么是预处理?

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。

简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。

指令 描述
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

#include

#include 是C语言中的一个预处理指令,它的主要作用是将外部的头文件(header files)内容包含到当前的源代码文件中。

头文件通常包含了函数声明、宏定义、结构体和其他变量的声明等,以便在当前源代码文件中使用这些声明和定义。

具体作用如下:

  1. 代码重用:通过包含头文件,可以重用其他文件中定义的函数和变量,而不必重新编写它们。这有助于减少代码的冗余,提高代码的可维护性。

  2. 接口分离:头文件通常包含了模块或库的公共接口。通过包含适当的头文件,您可以访问这些接口,而不必了解其内部实现细节。

  3. 模块化编程:C语言支持将代码分为多个文件,每个文件对应一个模块。头文件有助于定义模块的接口,使代码更易于组织和管理。

  4. 解决符号冲突:在大型程序中,多个源文件可能会包含相同的函数或变量名。使用头文件可以避免命名冲突,因为头文件通常包含了对这些符号的声明。

下面是一个示例,演示了如何使用#include 指令来包含头文件以及其作用:

示例头文件 myheader.h

1
2
3
4
5
6
7
8
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

int add(int a, int b); // 函数声明,其实现通常在同名头文件.c 这里是myheader.c

#define MAX_VALUE 100 // 宏定义
#endif

头文件代码实现myheader.c

1
2
3
4
5
#include "myheader.h"

int add(int a,int b){
return a + b;
}

示例源文件 main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>      // 标准C库头文件
#include "myheader.h" // 自定义头文件

int main() {
int x = 5;
int y = 7;

int sum = add(x, y); // 调用myheader.h中声明的add函数,其实现在myheader.c中

printf("%d + %d = %d\n", x, y, sum);
printf("MAX_VALUE 的值是 %d\n", MAX_VALUE); // 使用自定义的宏

return 0;
}

在上面的示例中,#include 指令用于包含标准C库头文件<stdio.h>和自定义头文件"myheader.h"。这样,main.c 中的代码可以使用 add 函数和 MAX_VALUE 宏,尽管它们的实现和定义分别在 myheader.h 文件中。这有助于代码的模块化和可维护性,以及避免冲突和错误。

#define

#define 允许你为标识符定义一个文本替代品。

请务必牢记!宏替代是文本替换,宏替代是文本替换,宏替代是文本替换。

当编译器遇到这个标识符时,它会被替换为宏的定义。

这个替换发生在代码的预处理阶段,因此在编译时宏的名字将被它的值所取代

宏定义的语法

#define 指令通常以以下形式出现:

1
#define 宏名 替代文本
  • 宏名 是标识符,它可以是字母、数字和下划线的组合,但不能以数字开头。

  • 替代文本 是要替代的文本字符串。

宏的用途

  • 符号常量:宏可以用来定义符号常量,以增加代码的可读性和维护性,如定义一些常量值。

    1
    2
    3
    4
    5
    #define MAX_VALUE 100
    int main() {
    int x = MAX_VALUE;
    // ...
    }

    这里,MAX_VALUE 被定义为符号常量,可以在代码中使用,而不需要多次写入值100。

  • 宏函数:你可以使用宏来创建简单的函数替代品,例如,它们可以接受参数并生成相应的代码。

    1
    2
    3
    4
    5
    #define SQUARE(x) ((x) * (x))
    int main() {
    int result = SQUARE(5);
    // ...
    }

    在此示例中,SQUARE 是一个宏函数,它接受一个参数 x 并返回 x 的平方。在代码中使用宏函数SQUARE(x)时,它会被替换为 ((x) * (x))

  • 条件编译:宏可以用于条件编译,根据不同的条件定义或排除代码块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define DEBUG
    int main() {
    #ifdef DEBUG
    printf("Debug模式开启\n");
    #else
    printf("Debug模式关闭\n");
    #endif
    // ...
    }

    在这里,DEBUG 宏用于条件编译。根据宏是否定义,不同的代码块会被包括或排除。

  • 代码重用:宏可以用于代码重用,减少代码的重复性。

    1
    2
    3
    4
    5
    6
    #define PRINT_TWICE(x) printf("%d\n%d\n", x, x)
    int main() {
    int number = 42;
    PRINT_TWICE(number);
    // ...
    }

    这个示例定义了一个宏 PRINT_TWICE,用于打印给定的值两次。在 main 函数中,PRINT_TWICE(number) 会被替换为 printf("%d\n%d\n", number, number)

宏替代的注意事项

  • 宏替代是文本替换,没有类型检查。
  • 替代文本不需要分号或其他终结符。
  • 替代文本中可以包含其他宏。
  • 宏名和替代文本之间不需要空格。

添加宏定义后,大概的编译过程

  • 预处理器处理源代码中的 #define 指令,将代码中所用到改宏名的地方全部替换为相应的替代文本。
  • 文本替代后,编译器将处理替代后的源代码生成目标代码。

需要注意的是,#define 指令并不会为宏分配内存,而是在编译时进行替代。

这意味着宏定义不会占用程序的内存空间,而只是在编译时用于代码生成。

#undef

#undef 用于取消或删除之前使用 #define 定义的宏(宏定义)。它的作用是从预处理阶段中删除一个宏定义,从而在编译时不再使用该宏。

以下是 #undef 的一般语法:

1
#undef macro_name

其中 macro_name 是要取消定义的宏的名称。

#define 用于创建宏定义,通常用于定义符号常量或用于代码的文本替换。

使用 #undef 可以用于以下情况:

取消宏定义: 如果在程序中不再需要某个宏定义,可以使用 #undef 来删除它,以避免在后续的代码中使用它。这对于确保程序的一致性和可维护性很重要。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#define MY_CONSTANT 42

int main() {
printf("MY_CONSTANT的值为: %d\n", MY_CONSTANT);

#undef MY_CONSTANT // 取消宏定义
// printf("MY_CONSTANT的值为: %d\n", MY_CONSTANT); // 这将导致编译错误,因为 MY_CONSTANT 已被取消定义

return 0;
}

在上面的示例中,#undef MY_CONSTANT 取消了 MY_CONSTANT 的宏定义,因此在取消定义后,再次尝试使用它会导致编译错误。

修改宏定义: 您可以使用 #undef 取消一个宏定义,然后重新定义它以更改宏的值或定义。这可以用于在程序的不同部分重新定义宏,以适应不同的需求。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

#define MY_CONSTANT 42

int main() {
printf("MY_CONSTANT的值为: %d\n", MY_CONSTANT);

#undef MY_CONSTANT
#define MY_CONSTANT 24 // 重新定义 MY_CONSTANT

printf("更新后,MY_CONSTANT的值为: %d\n", MY_CONSTANT);

return 0;
}

在这个示例中,#undef 用于取消 MY_CONSTANT 的定义,然后使用 #define 重新定义了该宏,以修改宏的值(该影响对其之后代码适用)。

#ifdef 和 ifndef

#ifdef 用于检查某个宏是否被定义,并且根据这个宏的定义情况来决定是否编译相关代码块。

#ifdef 用于检查某个宏是否未被定义,并且根据这个宏的未定义情况来决定是否编译相关代码块。

详尽解释:

检查宏是否已定义: #ifdef 后面跟着一个宏的名字,编译器将检查这个宏是否已经在之前的代码中使用 #define 定义过。如果宏已经被定义,则相关的代码块将被编译,否则将被忽略。

条件编译: 如果宏已经被定义,与 #ifdef 相关的代码块将被包含在编译中。如果宏未被定义,与 #ifndef 相关的代码块将被包含在编译中。

#ifdef#ifndef不一定非要同时存在。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

#define DEBUG // 定义名为 DEBUG 的宏

int main() {
#ifdef DEBUG
printf("Debug模式开启\n");
// 在这个代码块中,因为 DEBUG 宏已经被定义,所以这行代码将被编译
#endif

printf("这行代码在任何情况下都会被编译\n");
// 这行代码将在任何情况下都被编译

#ifndef RELEASE
printf("这段代码将会在RELEASE没有被定义的时候编译\n");
// 在这个代码块中,因为 RELEASE 宏没有被定义,所以这行代码将被编译
#endif

return 0;
}

在上面的示例中,我们定义了一个名为 DEBUG 的宏,并使用 #ifdef 来检查它是否已经定义。

由于 DEBUG 宏已经定义,与 #ifdef DEBUG 相关的代码块将被包含在编译中。

另外,我们使用 #ifndef RELEASE 来检查 RELEASE 宏是否未定义,如果未定义,相关的代码块也将被包含在编译中。

通过条件编译,你可以根据不同的需求和环境选择性地包含或排除代码,这在开发跨平台应用程序、调试代码或进行性能优化时非常有用。

#if,#else,#elif,#endif

#if

  • #if 指令用于开始一个条件编译块,根据表达式的真假来决定是否编译其中的代码。
  • 如果条件为,包含在 #if#endif 之间的代码将被编译,否则将被忽略。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#define DEBUG 1

int main() {
#if DEBUG
printf("Debug模式开启\n");
#endif

printf("这段内容总会被打印\n");
return 0;
}

在这个示例中,因为 DEBUG 宏被定义为1,#if DEBUG 为真,所以 “Debug mode is active.” 这行代码会被编译。

#else

  • #else 用于在条件不成立时执行的代码块。
  • 如果前面的 #if#elif 的条件为假,#else 后面的代码块将会被编译。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

#define DEBUG 0

int main() {
#if DEBUG
printf("Debug模式开启\n");
#else
printf("Debug模式关闭\n");
#endif

return 0;
}

因为 DEBUG 宏被定义为0,所以 printf("Debug模式开启\n"); 这行代码会被编译。

#elif

  • #elif#if 的可选补充,用于在前面的条件不成立时测试另一个条件。
  • 可以有多个 #elif,它们会逐一测试条件,直到找到一个条件为真,或者所有条件都为假。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

#define OS_LINUX 1
#define OS_WINDOWS 0

int main() {
#if OS_LINUX
printf("Linux系统\n");
#elif OS_WINDOWS
printf("Windows系统\n");
#else
printf("未知系统\n");
#endif

return 0;
}

因为 OS_LINUX 宏被定义为1(非0数,为true),OS_WINDOWS 宏被定义为0,,所以 “printf(“Linux系统\n”);” 这行代码会被编译。

如果改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

#define OS_LINUX 1
#define OS_WINDOWS 1

int main() {
#if OS_LINUX
printf("Linux系统\n");
#elif OS_WINDOWS
printf("Windows系统\n");
#else
printf("未知系统\n");
#endif

return 0;
}

OS_LINUX 宏和OS_WINDOWS 宏均被定义为1,这样被编译的代码是哪个?

展开查看代码
依旧是 printf("Linux系统\n"); 这行代码被编译
因为#if OS_LINUX判断为真,进入该分段,不再往#elif情况下判断(类似于if(),else if())

#endif

  • #endif 用于结束条件编译块,它将前面的 #if#elif#else 块的范围标志结束。

#error

#error 的主要作用是在预处理阶段产生一个编译错误,并且在编译器的错误信息中显示指定的错误消息,如下图:

#error通常用于条件编译或者编译时断言,以确保在编译过程中检查特定条件是否满足。

当条件不满足时,#error 将会导致编译中止,并且显示指定的错误消息,帮助程序员识别问题。

以下是示例代码和解释:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

// 定义一个宏,如果未定义该宏,则触发 #error 指令
#ifndef MY_MACRO
#error "MY_MACRO 未定义,这是一个必需的宏"
#endif

int main() {
printf("Hello, World!\n");
return 0;
}

在上面的示例中,我们首先使用 #ifndef 来检查是否定义了 MY_MACRO 这个宏。

如果没有定义,就会触发 #error 指令,其中包含了一条自定义错误消息:“MY_MACRO 未定义,这是一个必需的宏”(上方给出的图片)。

当编译这个程序时,如果 MY_MACRO 没有被定义,编译器将会报告以下错误消息:

#error 在调试和开发过程中非常有用,因为它可以提前发现潜在的错误,而不必在运行时才发现问题。

当然,在实验室所编写的代码中,你可能没有地方用到#error,作为小项目,代码结构不会很复杂。

#pragma

#pragma 用于向编译器提供特定的编译指令或控制编译器的行为。

它通常是用来告诉编译器执行一些特定的操作或配置编译环境。

#pragma 不是C语言标准的一部分,而是特定编译器的扩展,因此它的具体行为和支持的指令可以因编译器而异。

以下是一些常见的 #pragma 用途:

优化控制#pragma 可以用来告诉编译器如何优化代码,例如,可以控制循环展开、内联函数、优化级别等。

示例:

1
2
3
4
5
#pragma GCC optimize("O2")
int main() {
// 优化级别设置为 O2,具体什么叫02优化,03优化,请自行搜索,这里不做讲解
return 0;
}

警告控制#pragma 可以用来控制编译器的警告消息,例如,可以禁用特定的警告或设置警告级别。

示例:

1
2
3
4
5
#pragma GCC diagnostic ignored "-Wformat"
void someFunction() {
// 禁用针对格式错误的警告
printf("Hello, World");
}

包含文件路径#pragma 可以用于指定头文件的搜索路径。

示例:

1
2
#pragma GCC system_header
#include <my_header.h>

对齐和结构体填充#pragma 可以用于指示编译器如何对齐数据结构以及是否进行结构体填充(字节对齐,详情百度)。

示例:

1
2
3
4
5
#pragma pack(1)
struct MyStruct {
int a;
char b;
};

标识符重命名#pragma 有时可以用来为变量或函数指定特定的编译器名称,这对于处理平台特定的函数名约定很有用。

示例:

1
#pragma alias("my_function", "platform_specific_function")
  1. 其他用途:不同的编译器可能支持其他特定的 #pragma 指令,用于各种目的。

请注意,#pragma 指令的具体行为和支持的指令取决于使用的编译器。

在编写跨平台的代码时,应小心使用 #pragma,因为它可能导致不可移植的问题。

最好的做法是避免使用 #pragma,除非绝对需要特定的编译器行为。

如果需要进行特定的编译器配置,最好在编译器选项中进行配置,而不是依赖 #pragma