简介
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是完全相反的情况
本文详细介绍了C语言预处理器的工作原理,包括#include指令的使用,宏定义的功能和规则,以及条件编译(#if、#ifdef、ifndef、#endif)在代码组织中的应用。特别关注了如何避免头文件重复加载的问题。
1102

被折叠的 条评论
为什么被折叠?



