C provides certain language facilities by means of a preprocessor,which is conceptually a separate first step in compilation.
The two most frequently used features are #include, to include the contents of a file during compilation, and #define, to replace a token by an arbitrary sequence of characters.
Other features described in this section include conditional compilation and macros with arguments.
【来自:《The C Programming Language(Second Edition)》 (美) Brian W.Kernighan Dennis M.Ritchie】
【C语言】C 预处理器:
- 编译器在编译程序时,预处理器在实际编译之前进行预处理:先清理代码(删除注释、多行语句合并成一个逻辑行)、执行预处理指令。
- 预处理器不是编译器的组成部分,而是编译过程中独立的第一步,是文本替代工具。
- 预处理指令常用的有#include和#define。其它的还有条件编译、带参数的宏等。
- 预处理指令必须是#开头,一般在代码开头,不需要分号结尾。
- 预处理指令一般是一行,可在行尾用"\"折返成多行。
指令 | 描述 |
---|---|
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
1、#include 引入头文件
头文件:
① 扩展名为.h的文件。
② 包含系统全局变量、宏定义、函数声明。被多个源文件引用共享。
③ 引入的头文件中,也可能引入其它头文件。
④ 有两种类型的头文件:
- 系统头文件:编译器自带的头文件。在系统目录中。
- 用户头文件:程序员编写的头文件。在当前目录中,也可能在其它目录中。若在其它目录下,需要指定路径。
#include <stdio.h> // 引入系统头文件,在系统目录下查找
#include "example.h" // 引入用户头文件,在当前目录下查找,若没有,再在系统目录下查找
#include "D:/hello.h" // 引入用户头文件,且该文件不在当前目录,而在D盘
注意:
- 一个#include指令只能引入一个头文件。引入多个头文件,需多个#include指令。
- 引入头文件的地方将替换为该头文件的内容。类似于在引入头文件的地方将该头文件内容复制粘贴,与当前源文件连接成一个源文件。
Any source line of the form #include "filename" or #include <filename> is replaced by the contents of the file filename.
引入系统头文件 | 概述 | 说明 |
---|---|---|
#include <assert.h> | 程序诊断 | 设定插入点(assert),用于程序诊断 |
#include <ctype.h> | 字符 | 判断和映射字符 |
#include <errno.h> | 错误 | 全局整型变量errno,发生错误时记录错误码 |
#include <float.h> | 浮点数 | 浮点数的限制值(基数最大值/最小值、指数最大值/最小值等) |
#include <limits.h> | 限制值 | 基本数据类型的限制值(整型最大值/最小值、无符号整型最大值/最小值等) |
#include <locale.h> | 本地化 | 设置特定地域(日期格式/货币符号等) |
#include <math.h> | 数学 | 用于数学计算的函数 |
#include <setjmp.h> | 跳转 | 绕过正常的调用/返回规则。保存和恢复当前环境 |
#include <signal.h> | 信号 | 处理程序执行期间报告的不同信号(中断、异常等的处理) |
#include <stdarg.h> | 参数 | 参数个数可变时获取函数参数 |
#include <stddef.h> | 常用定义 | 定义常用类型和宏, 其中宏offsetof(结构体成员相对于结构体开头的偏移量) |
#include <stdio.h> | 输入/输出 | 输入/输出(标准/文件等) |
#include <stdlib.h> | 通用函数 | 内存分配、字符串转为整数等、程序终止、二分查找等其它通用工具函数 |
#include <string.h> | 字符串 | 操作字符串 |
#include <time.h> | 日期时间 | 操作日期和时间 |
2、#define, #undef 宏定义, 取消宏定义
(2-1)不带参数的宏定义
#define 宏名 替换文本
// 例如: example.h
#define NUMBER 10
#define 告诉预处理器,将代码中所有出现的宏名替换成指定的替换文本。
但字符串双引号中的以及与其它标识符(例如:局部变量名)冲突的不会被替换。
#include <stdio.h>
#include "example.h" // 已定义宏NUMBER
int main(void)
{
printf("NUMBER is %d\n", NUMBER); // 字符串双引号中的不会被替换
return 0;
}
// 结果:
NUMBER is 10
同名的宏可以重复定义,但定义必须相同,否则报错。
#include "example.h" // 已定义宏NUMBER
#define NUMBER 5 // warning: "NUMBER" redefined
#include "example.h" // 已定义宏NUMBER
#define NUMBER 10 // 没有报错
若要重新定义宏,需先取消之前的定义,再定义。
#include <stdio.h>
#include "example.h" // 已定义宏NUMBER
#undef NUMBER // 取消已定义的宏
#define NUMBER 5 // 重新定义宏
int main(void)
{
printf("NUMBER is %d\n", NUMBER);
return 0;
}
// 结果:
NUMBER is 5
(2-2)带参数的宏定义
宏定义可以带参数,称为带参数的宏。 #define 宏名(形参列表) 替换文本
注意:
- 替换文本中所有参数都用括号括起来,整个替换文本也用括号括起来,避免错误。
- 使用时必须保持参数正确,否则导致编译错误或运行错误。
#include <stdio.h>
#define MAX(a,b) ((a > b) ? (a) : (b)) // 带参数的宏
int main(void)
{
int m = 2, n = 3;
printf("%d and %d, max is %d\n", m, n, MAX(m,n)); // 预处理器将MAX(m,n)文本替换为((m>n)?(m):(n))
return 0;
}
// 结果:
2 and 3, max is 3
#include <stdio.h>
#define SQUARE(x) x*x // 没有正确使用括号的错误案例
int main(void)
{
int m = 2, n = 3;
printf("square(%d+%d) = %d\n", m, n, SQUARE(m+n)); // 预处理器将SQUARE(m,n)文本替换为m+n*m+n
return 0;
}
// 结果:
square(2+3) = 11 // 2+3*2+3=11
#include <stdio.h>
#define SQUARE(x) ((x)*(x)) // 正确使用括号
int main(void)
{
int m = 2, n = 3;
printf("square(%d+%d) = %d\n", m, n, SQUARE(m+n)); // 预处理器将SQUARE(m,n)文本替换为((m+n)*(m+n))
return 0;
}
// 结果:
square(2+3) = 25 // ((2+3)*(2+3))=25
宏定义中的参数也可以是不定参数(即不确定参数数量),称为不定参数宏。
形参列表中使用"..."表示不定参数(可变参数),替换文本中使用"__VA_ARGS__"表示"..."匹配的所有传递给宏的参数。
注意:
- 不定参数宏一般用于日志记录、调试信息输出等。
- "..."在形参列表最后。
- 若有固定参数,替换文本最好使用"##__VA_ARGS__"。例如:#define PRINT(str, ...) printf(str, ##__VA_ARGS__)。
- 若有固定参数,使用宏时固定参数必须提供。若使用时除固定参数外没有其它参数,"##__VA_ARGS__"可移除逗号(固定参数后有逗号","),否则报错error: expected expression before ')' token。
- "__VA_ARGS__"只能在支持C99标准的编译器上使用。
- 因不定参数宏的复杂性,需谨慎使用。
#include <stdio.h>
#define PRINT(...) printf(__VA_ARGS__) // 不定参数宏
int main(void)
{
int m = 2, n = 3;
PRINT("Output: %d, %d", m, n); // 预处理器将PRINT(...)文本替换为printf("Output: %d, %d", m, n)
return 0;
}
// 结果:
Output: 2, 3
#include <stdio.h>
#define PRINT(str, ...) printf(str, ##__VA_ARGS__) // 有固定参数str
int main(void)
{
int m = 2, n = 3;
PRINT("no extra argument\n"); // 预处理器将PRINT(...)文本替换为printf("no extra argument\n")
PRINT("two extra arguments:%d %d\n", m, n); // 预处理器将PRINT(...)文本替换为printf("two extra arguments:%d %d\n", m, n)
return 0;
}
// 结果:
no extra argument
two extra arguments:2 3
#include <stdio.h>
#define PRINT(str, ...) printf(str, __VA_ARGS__) // 固定参数str,可变参数...
int main(void)
{
int m = 2, n = 3;
PRINT("Output: no extra argument\n"); // 没有可变参数,报错:error: expected expression before ')' token
PRINT(); // 没有固定参数,报错:error: expected expression before ',' token
return 0;
}
(2-3)预处理器运算符
"#"运算符:将宏的某个参数转为字符串。
#include <stdio.h>
#define STR(x) #x
int main(void)
{
int m = 2, n = 3;
printf(STR(m+n) "=%d\n", m+n); // 预处理器将STR(m+n)文本替换为"m+n"
printf("%s=%d\n", STR(m+n), m+n); // 预处理器将STR(m+n)文本替换为"m+n"
return 0;
}
// 结果:
m+n=5
m+n=5
"##"运算符:合并宏中的两个参数,可将宏中两个独立的标记拼接成一个新的标记。
若拼接之后的标记没有声明,会报错。确保拼接后的标记已声明。
若参数也是宏,且报错,可尝试再定义一个宏,做中间转换。
#include <stdio.h>
#define CONCATE(a,b) a##b
int main(void)
{
int m = 2, n = 3;
printf("%d\n", CONCATE(2,3)); // 预处理器将CONCATE(2,3)文本替换为23
printf("%d\n", CONCATE(m,n)); // 报错:error: 'mn' undeclared。预处理器将CONCATE(m,n)文本替换为mn,但mn没有声明
return 0;
}
#include <stdio.h>
#define CONCATE(a,b) a##b
int main(void)
{
int m = 2, n = 3;
int mn = 18;
printf("%d\n", CONCATE(2,3)); // 预处理器将CONCATE(2,3)文本替换为23
printf("%d\n", CONCATE(m,n)); // 预处理器将CONCATE(m,n)文本替换为mn
return 0;
}
// 结果:
23
18
#include <stdio.h>
#define M 2
#define N 3
#define _CONCATE(a,b) a##b
#define CONCATE(a,b) _CONCATE(a,b)
int main(void)
{
printf("%d\n", CONCATE(M,N)); // 宏的参数也是宏,预处理器将CONCATE(M,N)文本替换为23。(_CONCATE(M,N)即M##N即2##3即23)
return 0;
}
// 结果:
23
注意宏的正确使用和潜在风险。
尽量保持宏的简洁易用,避免过于复杂,确保代码清晰、易于维护。
过于复杂的宏会增加代码的阅读难度和理解难度,甚至导致编译时错误或运行时发生错误。
尽量使用标准库已定义的宏。
3、条件编译
#if ... #elif ... #else ... #endif
#if defined ... #endif 判断宏名是否定义过#if !defined ... #endif 判断宏名是否没有定义过
#include <stdio.h>
#if !defined(NUMBER) // 若条件为真,执行下面语句
#define NUMBER 10
#endif // 条件语句块结束
int main(void)
{
printf("NUMBER is %d\n", NUMBER);
return 0;
}
// 结果:
NUMBER is 10
#include <stdio.h>
#include "example.h"
#if !defined(NUMBER) // 若条件为真,执行下面语句
#define NUMBER 10
#else // 若if条件为假,执行下面语句
#undef NUMBER
#define NUMBER 5
#endif // 条件语句块结束
int main(void)
{
printf("NUMBER is %d\n", NUMBER);
return 0;
}
// 结果:
NUMBER is 5
#ifdef ... #endif 判断宏名是否定义过
注意:#ifdef 宏名 等同于 #if defined(宏名) 。
#include <stdio.h>
#include "example.h"
#ifdef NUMBER // 若条件为真,执行下面语句
#undef NUMBER
#define NUMBER 5
#endif // 条件语句块结束
int main(void)
{
printf("NUMBER is %d\n", NUMBER);
return 0;
}
// 结果:
NUMBER is 5
#ifndef ... #endif 判断宏名是否没有定义过
注意:#ifndef 宏名 等同于 #if !defined(宏名) 。
#include <stdio.h>
#ifndef NUMBER // 若条件为真,执行下面语句
#define NUMBER 10
#endif // 条件语句块结束
int main(void)
{
printf("NUMBER is %d\n", NUMBER);
return 0;
}
// 结果:
NUMBER is 10
4、预定义宏
预定义宏:编译器提前定义好的宏,可以使用不能修改。
宏 | 描述 |
---|---|
__DATE__ | 当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。 |
__TIME__ | 当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。 |
__FILE__ | 这会包含当前文件名,一个字符串常量。 |
__LINE__ | 这会包含当前行号,一个十进制常量。 |
__STDC__ | 当编译器以 ANSI 标准编译时,则定义为 1。 |
__STDC_HOSTED__ | 当编译器可以提供完整的标准库时,则定义为1。否则为0(嵌入式系统的标准库一般不完整) |
__STDC_VERSION__ | 编译使用的C语言版本,格式为yyyymmL 的长整数,C99 版本为“199901L”,C11 版本为“201112L”,C17 版本为“201710L” |
#include <stdio.h>
int main(void)
{
printf("function: %s\n", __func__);
printf("date: %s\n", __DATE__);
printf("time: %s\n", __TIME__);
printf("file: %s\n", __FILE__);
printf("line: %d\n", __LINE__);
printf("ANSI: %d\n", __STDC__);
printf("full standard library: %d\n", __STDC_HOSTED__);
printf("C version: %ld\n", __STDC_VERSION__);
return 0;
}
// 结果:
function: main
date: Apr 11 2024
time: 17:00:26
file: preprocessor.c
line: 9
ANSI: 1
full standard library: 1
C version: 201710
补充:宏与函数的区别
宏 | 函数 |
---|---|
只是文本替换 | 完成某个特定功能的任务。可对表达式进行计算 |
文本替换时,代码长度增加 | 函数定义在一个地方,每次调用都是同一地方的代码 |
不占用内存空间 | 占用内存空间(开辟栈空间,压栈出栈,记录返回地址) |
不用遵循函数调用和返回规则,速度更快 | 遵循函数调用和返回规则,有额外开销 |
参数可以任何类型,只要参数类型合法 | 参数需标明类型,不同类型的参数使用不同函数解决 |
因括号或参数没有正确使用以及运算符优先级问题,可能导致编译或运行错误 | 运行时可能错误 |
宏在预处理阶段就进行文本替换了,无法调试 | 可以调试 |
使用#undef 取消之前的定义,可以确保是函数而不是宏。
Names may be undefined with #undef, usually to ensure that a routine is really a function, not a macro.
#undef getchar // 取消getchar的宏定义
int getchar(void){...} // 确保gechar是函数,而不是宏