简介
C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。
预处理器首先会清理代码,进行删除注释、宏展开、多行语句合成一个逻辑行等工作。然后,执行#
开头的预处理指令。
预处理指令可以出现在程序的任何地方,但是习惯上,往往放在代码的开头部分。它的主要好处是,会使得程序的可读性更好,也更容易修改。
每个预处理指令都以#
开头,放在一行的行首,指令前面可以有空白字符(比如空格或制表符)。#
和指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。
所有预处理指令都是一行的,除非在行尾使用反斜杠\
,将其折行。指令结尾处不需要分号。
#include
#include
指令用于编译时将其他源码文件,加载进入当前文件,可以是头文件(通常以**.h
**结尾)或其他源文件。
-
#include <foo.h>
// 加载系统提供的文件形式一,文件名写在尖括号里面,表示该文件是系统提供的,通常是标准库的库文件,不需要写路径。因为编译器会到系统指定的安装目录里面,去寻找这些文件。
-
#include "foo.h"
// 加载用户提供的文件形式二,文件名写在双引号里面,表示该文件由用户提供,具体的路径取决于编译器的设置,可能是当前目录,也可能是项目的工作目录。如果所要包含的文件在其他位置,就需要指定路径,下面是一个例子。
#include "/usr/local/lib/foo.h"
GCC 编译器的-I
参数,也可以用来指定include
命令中用户文件的加载路径。
$ gcc -I include/ -o hello hello.c
常常用于模块化开发,将功能分割到不同的文件中,以便于维护和管理。避免在头文件中直接包含其他源文件,以免导致编译时间增加和重复定义的问题。当一个文件被包含多次时,编译器可能会产生重复定义的错误,可以通过预处理器指令 #ifndef
、#define
、#endif
来避免这种情况
// 假设有一个头文件 example.h
// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
void printHello(); // 声明一个函数
#endif // EXAMPLE_H// 在另一个源文件中使用 example.h
// main.c
#include <stdio.h> // 包含标准输入输出库的头文件
#include "example.h" // 包含自定义的头文件int main() {
printf("Hello, world!\n");
printHello(); // 使用 example.h 中声明的函数
return 0;
}
// 在另一个源文件中实现 example.h 中声明的函数
// example.c
#include <stdio.h>
#include "example.h"
void printHello() {
printf("Hello from printHello function!\n");
}
#define
#define
指令用于创建宏定义,可以将一段代码或值定义为一个标识符,以后在程序中可以直接使用该标识符来代替这段代码或值。这样可以用于简化代码、提高可读性、增加代码的灵活性。(宏可以用于替换整型、浮点型、字符型等任何类型的表达式)
-
宏的命名规则:宏的名称不允许有空格,而且必须遵守 C 语言的变量命名规则(只能使用字母、数字、下划线,且首字符不能是数字)
同名的宏可以重复定义,只要定义是相同的,就没有问题。如果定义不同,就会报错。 -
宏定义的值可以是任意的表达式,但是建议将复杂的表达式封装在括号中,以确保优先级和结合性正确。宏定义是在预处理阶段进行替换,因此不涉及类型检查和作用域的问题。宏定义是简单的文本替换,可能导致副作用和不可预见的行为。因此在定义宏时要小心,避免与程序中已有的标识符冲突。
#include <stdio.h> // 简单的宏定义,用于定义常量 #define PI 3.14159 #define MAX(a, b) ((a) > (b) ? (a) : (b)) int main() { double radius = 5.0; double area = PI * radius * radius; // 使用宏定义 PI printf("Area of circle: %f\n", area); int x = 10, y = 20; int max_value = MAX(x, y); // 使用带参数的宏定义 MAX printf("Max value: %d\n", max_value); return 0; }
-
如果宏出现在字符串里面(即出现在双引号中),或者是其他标识符的一部分,就会失效,并不会发生替换。
#define FOO 100 char *s = "FOO..."; printf("FOO\n"); // FOO printf("%s\n", s); // FOO...
-
不带参数的宏
不带参数的宏是 C 语言中一种简单的文本替换机制。它允许你在代码中定义一个标识符来代表一个特定的值或一段代码,当编译器遇到该标识符时,会将其替换为预先定义的值或代码片段。
#define PI 3.14
在上面的例子中,
PI
是一个宏,它被定义为3.14159
。在代码中使用PI
时,编译器会将其替换为3.14159
。 -
带参数的宏
带参数的宏允许你定义一个带有参数的代码模板,在宏被调用时,实际参数会替换模板中的参数,并展开为相应的代码。
#define MAX(x, y) ((x) > (y) ? (x) : (y))
在上面的例子中,
MAX
是一个宏,它接受两个参数x
和y
,并返回其中较大的一个。在代码中使用MAX
时,编译器会将其展开为相应的代码。 -
不定参数的宏
不定参数的宏允许你定义一个可以接受任意数量参数的宏。在宏定义中,
...
表示不定数量的参数。你可以使用__VA_ARGS__
来引用传递给宏的参数列表。#define PRINT(...) printf(__VA_ARGS__)
在上面的例子中,
PRINT
是一个不定参数的宏,它接受任意数量的参数,并将它们传递给printf
函数。 -
#运算符 ##运算符
-
#
运算符:宏可以用于替换整型、浮点型、字符型等任何类型的表达式,而**#
** 运算符将参数转换为字符串。例如:#define STR(x) #x printf("%s\n", STR(hello)); // 输出 "hello"
-
##
运算符:在宏定义中,##
运算符用于连接两个标记(tokens),使它们成为一个单独的标记。#define CONCAT(x, y) x##y int n = CONCAT(10, 20); // 1020
-
#undef
#undef
指令用来取消已经使用#define
定义的宏,使得该宏在之后的代码中不再起作用。
- 使用
#undef
取消宏定义后,该宏在之后的代码中将不再起作用,应确保取消宏定义不会影响到后续代码的正确性,因此要谨慎使用#undef
。 - 同名的宏如果两次定义不一样,会报错,而
#undef
后跟不存在的宏,并不会报错。
#include <stdio.h>
#define DEBUG_MODE // 定义一个调试模式的宏
int main() {
#ifdef DEBUG_MODE
printf("Debug mode is enabled\n");
#endif
#undef DEBUG_MODE // 现在取消 DEBUG_MODE 宏定义
#ifdef DEBUG_MODE
printf("This line won't be printed because DEBUG_MODE is undefined\n");
#endif
return 0;
}
GCC 的-U
选项可以在命令行取消宏的定义,相当于#undef
。
$ gcc -U MAXSIZE hello.c
上面示例中的-U
参数,取消了宏LIMIT
,相当于源文件里面的#undef LIMIT
。
#if #elif #else #endif
#if
和 #endif
用于标记条件编译的起始和结束位置,条件(可以是表达式)为真时,起始位置和结束位置之间的内码就会编译,否则不会编译。
如果#if
后紧跟着的是一个宏,那么效果就与#ifdef
等同。
-
条件编译可以通过定义预处理器宏来控制条件编译的行为,从而实现灵活的代码组织和构建。也可以根据不同的条件在编译时决定是否包含某段代码,从而实现针对不同平台、不同环境或不同配置的代码控制。
-
#if
和#endif
之间可以包含其他预处理指令,如#define
、#include
、#ifdef
、#ifndef
#include <stdio.h> #define DEBUG 1 // 定义一个调试模式的宏,值为 1 表示开启调试模式 int main() { #if DEBUG printf("Debug mode is enabled\n"); // 这里可以放置调试相关的代码 #else printf("Debug mode is disabled\n"); // 这里放置非调试模式下的代码 #endif return 0; }
GCC 的-D
参数可以在编译时指定宏的值,因此可以很方便地打开调试开关。
$ gcc -DDEBUG=1 foo.c
上面示例中,-D
参数指定宏DEBUG
为1
,相当于在代码中指定#define DEBUG 1
。
#ifdef #endif
#ifdef
和 #endif
用于标记条件编译的起始和结束位置。#ifdef
后面跟随一个标识符(通常是一个宏),如果该标识符已经被定义,则编译 #ifdef
和 #endif
之间的代码;否则,编译器会忽略这段代码。
实际上#ifdef
就是#if defined
-
defined
defined
是一个预处理运算符(不是函数和宏),用于检查指定的宏是否已经被定义过(并不能检查宏的值)。它可以用于#if
和#elif
指令中,用于执行条件编译。#define DEBUG_MODE // 定义一个宏 #if defined(DEBUG_MODE) // 如果 DEBUG_MODE 宏已经被定义,则执行此处代码 printf("Debug mode is enabled.\n"); #endif
-
使用
#ifdef
时应注意所检查的标识符是否已经在合适的位置被定义,避免出现编译错误或者意外的代码被包含。#include <stdio.h> #define def1 int main(){ #ifdef def1 printf("def1在条件编译语句之上定义, 可以输出"); #endif #ifdef def2 printf("def1在条件编译语句之下定义, 不可以输出") #endif #define def2 return 0; }
-
在编写库或跨平台代码时,常常会使用
#ifdef
来检查特定平台或编译器下的宏定义,以实现代码的兼容性。 -
这段文本讨论的是在编写库文件时,有时会出现重复加载的情况。为了避免这种情况,可以在库文件中使用
#define
定义一个空的宏。通过检查这个宏是否已经被定义,可以判断库文件是否已经被加载。
#ifndef #endif
#ifndef
和 #endif
用于标记条件编译的起始和结束位置。#ifndef
后面跟随一个标识符(通常是一个宏),如果该标识符尚未被定义,则编译 #ifndef
和 #endif
之间的代码;否则,编译器会忽略这段代码。
这和上面讲述的#ifdef #endif是完全相反的情况