C、C++预处理详解
什么是预处理?
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。
简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。
指令 | 描述 |
---|---|
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
#include
#include
是C语言中的一个预处理指令,它的主要作用是将外部的头文件(header files)内容包含到当前的源代码文件中。
头文件通常包含了函数声明、宏定义、结构体和其他变量的声明等,以便在当前源代码文件中使用这些声明和定义。
具体作用如下:
-
代码重用:通过包含头文件,可以重用其他文件中定义的函数和变量,而不必重新编写它们。这有助于减少代码的冗余,提高代码的可维护性。
-
接口分离:头文件通常包含了模块或库的公共接口。通过包含适当的头文件,您可以访问这些接口,而不必了解其内部实现细节。
-
模块化编程:C语言支持将代码分为多个文件,每个文件对应一个模块。头文件有助于定义模块的接口,使代码更易于组织和管理。
-
解决符号冲突:在大型程序中,多个源文件可能会包含相同的函数或变量名。使用头文件可以避免命名冲突,因为头文件通常包含了对这些符号的声明。
下面是一个示例,演示了如何使用#include
指令来包含头文件以及其作用:
示例头文件 myheader.h
:
1 |
|
头文件代码实现myheader.c
1 |
|
示例源文件 main.c
:
1 |
|
在上面的示例中,#include
指令用于包含标准C库头文件<stdio.h>
和自定义头文件"myheader.h"
。这样,main.c
中的代码可以使用 add
函数和 MAX_VALUE
宏,尽管它们的实现和定义分别在 myheader.h
文件中。这有助于代码的模块化和可维护性,以及避免冲突和错误。
#define
#define
允许你为标识符定义一个文本替代品。
请务必牢记!宏替代是文本替换,宏替代是文本替换,宏替代是文本替换。
当编译器遇到这个标识符时,它会被替换为宏的定义。
这个替换发生在代码的预处理阶段,因此在编译时宏的名字将被它的值所取代。
宏定义的语法
#define
指令通常以以下形式出现:
1 |
|
-
宏名
是标识符,它可以是字母、数字和下划线的组合,但不能以数字开头。 -
替代文本
是要替代的文本字符串。
宏的用途:
-
符号常量:宏可以用来定义符号常量,以增加代码的可读性和维护性,如定义一些常量值。
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 |
|
其中 macro_name
是要取消定义的宏的名称。
#define
用于创建宏定义,通常用于定义符号常量或用于代码的文本替换。
使用 #undef
可以用于以下情况:
取消宏定义: 如果在程序中不再需要某个宏定义,可以使用 #undef
来删除它,以避免在后续的代码中使用它。这对于确保程序的一致性和可维护性很重要。
示例代码:
1 |
|
在上面的示例中,#undef MY_CONSTANT
取消了 MY_CONSTANT
的宏定义,因此在取消定义后,再次尝试使用它会导致编译错误。
修改宏定义: 您可以使用 #undef
取消一个宏定义,然后重新定义它以更改宏的值或定义。这可以用于在程序的不同部分重新定义宏,以适应不同的需求。
示例代码:
1 |
|
在这个示例中,#undef
用于取消 MY_CONSTANT
的定义,然后使用 #define
重新定义了该宏,以修改宏的值(该影响对其之后代码适用)。
#ifdef 和 ifndef
#ifdef
用于检查某个宏是否被定义,并且根据这个宏的定义情况来决定是否编译相关代码块。
#ifdef
用于检查某个宏是否未被定义,并且根据这个宏的未定义情况来决定是否编译相关代码块。
详尽解释:
检查宏是否已定义: #ifdef
后面跟着一个宏的名字,编译器将检查这个宏是否已经在之前的代码中使用 #define
定义过。如果宏已经被定义,则相关的代码块将被编译,否则将被忽略。
条件编译: 如果宏已经被定义,与 #ifdef
相关的代码块将被包含在编译中。如果宏未被定义,与 #ifndef
相关的代码块将被包含在编译中。
#ifdef
和#ifndef
不一定非要同时存在。
示例代码:
1 |
|
在上面的示例中,我们定义了一个名为 DEBUG
的宏,并使用 #ifdef
来检查它是否已经定义。
由于 DEBUG
宏已经定义,与 #ifdef DEBUG
相关的代码块将被包含在编译中。
另外,我们使用 #ifndef RELEASE
来检查 RELEASE
宏是否未定义,如果未定义,相关的代码块也将被包含在编译中。
通过条件编译,你可以根据不同的需求和环境选择性地包含或排除代码,这在开发跨平台应用程序、调试代码或进行性能优化时非常有用。
#if,#else,#elif,#endif
#if
#if
指令用于开始一个条件编译块,根据表达式的真假来决定是否编译其中的代码。- 如果条件为真,包含在
#if
到#endif
之间的代码将被编译,否则将被忽略。
示例:
1 |
|
在这个示例中,因为 DEBUG
宏被定义为1,#if DEBUG
为真,所以 “Debug mode is active.” 这行代码会被编译。
#else
#else
用于在条件不成立时执行的代码块。- 如果前面的
#if
或#elif
的条件为假,#else
后面的代码块将会被编译。
示例:
1 |
|
因为 DEBUG
宏被定义为0,所以 printf("Debug模式开启\n");
这行代码会被编译。
#elif
#elif
是#if
的可选补充,用于在前面的条件不成立时测试另一个条件。- 可以有多个
#elif
,它们会逐一测试条件,直到找到一个条件为真,或者所有条件都为假。
示例:
1 |
|
因为 OS_LINUX
宏被定义为1(非0数,为true),OS_WINDOWS
宏被定义为0,,所以 “printf(“Linux系统\n”);” 这行代码会被编译。
如果改成这样:
1 |
|
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 |
|
在上面的示例中,我们首先使用 #ifndef
来检查是否定义了 MY_MACRO
这个宏。
如果没有定义,就会触发 #error
指令,其中包含了一条自定义错误消息:“MY_MACRO 未定义,这是一个必需的宏”(上方给出的图片)。
当编译这个程序时,如果 MY_MACRO
没有被定义,编译器将会报告以下错误消息:
#error
在调试和开发过程中非常有用,因为它可以提前发现潜在的错误,而不必在运行时才发现问题。
当然,在实验室所编写的代码中,你可能没有地方用到#error
,作为小项目,代码结构不会很复杂。
#pragma
#pragma
用于向编译器提供特定的编译指令或控制编译器的行为。
它通常是用来告诉编译器执行一些特定的操作或配置编译环境。
#pragma
不是C语言标准的一部分,而是特定编译器的扩展,因此它的具体行为和支持的指令可以因编译器而异。
以下是一些常见的 #pragma
用途:
优化控制:#pragma
可以用来告诉编译器如何优化代码,例如,可以控制循环展开、内联函数、优化级别等。
示例:
1 |
|
警告控制:#pragma
可以用来控制编译器的警告消息,例如,可以禁用特定的警告或设置警告级别。
示例:
1 |
|
包含文件路径:#pragma
可以用于指定头文件的搜索路径。
示例:
1 |
|
对齐和结构体填充:#pragma
可以用于指示编译器如何对齐数据结构以及是否进行结构体填充(字节对齐,详情百度)。
示例:
1 |
|
标识符重命名:#pragma
有时可以用来为变量或函数指定特定的编译器名称,这对于处理平台特定的函数名约定很有用。
示例:
1 |
|
- 其他用途:不同的编译器可能支持其他特定的
#pragma
指令,用于各种目的。
请注意,
#pragma
指令的具体行为和支持的指令取决于使用的编译器。在编写跨平台的代码时,应小心使用
#pragma
,因为它可能导致不可移植的问题。最好的做法是避免使用
#pragma
,除非绝对需要特定的编译器行为。如果需要进行特定的编译器配置,最好在编译器选项中进行配置,而不是依赖
#pragma