文章目录
- 前言
-
- 本文旨在系统性地介绍C语言中的预处理。
- 一、预定义符号
- 二、#define定义宏
-
- 2.1、#define定义标识符(无参宏)
- 2.2、宏定义中括号的使用
- 2.3、带有副作用的宏参数
- 2.4、宏替换的规则
- 2.5、宏和函数的对比
- 三、命名约定
- 四、#undef
- 五、条件编译
- 六、头文件包含
-
- 6.1、头文件被包含的方式
- 6.2、嵌套文件包含
前言
本文旨在系统性地介绍C语言中的预处理。
一、预定义符号
预定义符号是编译器预先定义的宏,可直接使用,无需 #define。它们在预处理阶段被替换为对应的值。
__FILE__ //进⾏编译的源⽂件名
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
代码示例:
#include <stdio.h>
int main()
{
printf("文件:%s\\n", __FILE__);
printf("行号:%d\\n", __LINE__);
printf("编译日期:%s\\n", __DATE__);
printf("编译时间:%s\\n", __TIME__);
return 0;
}
二、#define定义宏
宏定义允许将参数替换到文本中,其语法形式如下:
#define name(parament–list) stuff
name:宏名 (parament-list):参数列表,由逗号分隔的符号表 stuff:替换文本,参数可以出现在其中
注意事项:参数列表的左括号必须紧挨宏名,不能有空格。如果有空格,参数列表被当成替换文本。
2.1、#define定义标识符(无参宏)
基本语法
#define name stuff
代码示例:
#define MAX 1000
#define reg register // 为 register 关键字起简短别名
#define do_forever for(;;) // 用形象符号替换无限循环
#define CASE break;case // case 语句自动加 break
当 stuff 过长时,可以用反斜杠\\(续写符)分行编写(最后一行不加)告诉预处理器:下一行是当前行的延续,在预处理阶段会合并成一行。如果没有续行符,预处理后,宏定义只到第一行结束,后面的内容被当作普通代码,导致编译错误。 代码示例:
#define DEBUG_PRINT printf("file:%s\\tline:%d\\t" \\
"date:%s\\ttime:%s\\n", \\
__FILE__, __LINE__, \\
__DATE__, __TIME__)
末尾不建议加分号,否则可能引发语法错误。 代码示例:
#define MAX 1000;
if (condition)
max = MAX; // 替换后 → max = 1000;;
// 替换后if 和 else 之间变成两条语句,导致编译错误。
else
max = 0;
2.2、宏定义中括号的使用
宏定义中,给每个参数加上括号,再给整个表达式加上括号,可以避免因操作符优先级导致的意外结果。
①参数未加括号(宏是纯文本替换,参数中的操作符优先级影响计算顺序)
#define SQUARE(x) x * x
SQUARE(a + 1)
// 替换后 → a + 1 * a + 1
// 实际计算:a + (1 * a) + 1
// 结果与预期不符
解决办法:给宏体的每个参数加上括号。
#define SQUARE(x) (x) * (x)
SQUARE(a + 1)
// 替换后 → (a + 1) * (a + 1)
②整个宏体未加括号(宏体本身没有括号,导致外部运算符影响计算顺序)
#define DOUBLE(x) (x) + (x)
10 * DOUBLE(a)
// 替换后 → 10 * (5) + (5)
// 实际计算:10 * 5 + 5 = 55
解决办法:给整个宏体加上括号。
#define DOUBLE(x) ((x) + (x))
10 * DOUBLE(a)
// 替换后 → 10 * ((5) + (5)) = 100
2.3、带有副作用的宏参数
宏参数在宏体中多次出现时,带副作用的参数会被多次求值,导致不可预测的结果。应避免在宏参数中使用 ++、– 等操作,或用函数替代宏。
副作用是指表达式求值时永久改变变量值的效果。
x + 1; // 无副作用(x 不变)
x++; // 有副作用(x 被修改)
代码示例:
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int x = 5;
int y = 8;
// 宏调用:MAX(x++, y++)
// 预处理后展开为:((x++) > (y++) ? (x++) : (y++))
// 执行过程:
// ① 比较 (x++) > (y++),即 5 > 8(假),此时 x=6,y=9
// ② 条件为假,执行 (y++),取 y 的当前值 9 赋给 z,然后 y 变为 10
// ③ 最终:x=6,y=10,z=9
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\\n", x, y, z);
return 0;
}
2.4、宏替换的规则
① 参数检查:检查宏参数是否包含 #define 定义的符号,如果有则先替换 ② 文本插入:将替换文本插入到宏调用的位置,参数名被对应的值替换 ③ 再次扫描:对结果文件再次扫描,检查是否还有 #define 定义的符号,如果有则重复上述过程
代码示例:
#define PI 3.14
#define AREA(r) (PI * (r) * (r))
double s = AREA(5);
替换过程: ①检查参数 5,没有 #define 定义的符号; ②插入替换文本:(PI * (5) * (5)); ③再次扫描,发现 PI 是 #define 定义的符号; ④替换 PI:(3.14 * (5) * (5))。
注意事项: ①宏不能递归:宏不能调用自身; ②字符串内容不搜索:字符串字面量中的内容不会被宏替换。
代码示例:
#define PI 3.14
printf("PI = %f", PI); // 宏被替换
printf("PI"); // 字符串中的 PI 不会被替换
2.5、宏和函数的对比
| 执行时机 | 预处理阶段展开 | 运行时调用 |
| 执行效率 | 高(无调用开销) | 低(有调用和返回开销) |
| 代码长度 | 大型宏多次展开使程序膨胀 | 代码只存一份 |
| 类型检查 | 无 | 有 |
| 调试 | 无法调试 | 可逐语句调试 |
| 类型通用 | 类型无关 | 参数类型固定 |
| 参数为类型 | 可以(如 MALLOC(10, int)) | 不可以 |
| 副作用风险 | 参数多次求值 | 参数只求值一次 |
| 内存开销 | 不占栈空间 | 每次调用占用栈空间 |
| 递归 | 不可递归 | 可递归 |
| 操作符优先级 | 易受邻近操作符影响 | 安全(参数先求值) |
三、命名约定
宏名全部大写,函数名不要全部大写,C语言本身没有区分,目的是让代码可读性更强,便于区分宏和函数。
四、#undef
#undef 指令用于移除一个已存在的宏定义。如果需要重新定义一个宏名,或取消某个宏的作用,可以先用 #undef 移除旧定义。
语法形式
#undef NAME
代码示例:
#include <stdio.h>
#define MAX 100
// 此时 MAX 已定义
#undef MAX // 移除 MAX 的宏定义
// 此时 MAX 未定义,不能再使用
#ifdef MAX
printf("MAX 已定义\\n");
#else
printf("MAX 未定义\\n");
#endif
五、条件编译
条件编译用于在预处理阶段根据条件决定哪些代码被保留、哪些被丢弃,常用于调试信息控制、头文件保护和跨平台适配。
常见条件编译指令: ①单分支条件编译
#if 常量表达式
// 表达式为真时编译
#endif
#define __DEBUG__ 1
#if __DEBUG__
printf("调试模式\\n"); // 被编译
#endif
②多分支条件编译
#define VERSION 2
#if VERSION == 1
printf("版本1\\n");
#elif VERSION == 2
printf("版本2\\n");
#else
printf("其他版本\\n");
#endif
③判断是否被定义
| #if defined(symbol) | symbol 已定义时为真 |
| #ifdef symbol | 同上(简写) |
| #if !defined(symbol) | symbol 未定义时为真 |
| #ifndef symbol | 同上(简写) |
#define DEBUG
#ifdef DEBUG
printf("调试模式\\n"); // 被编译
#endif
#ifndef RELEASE
printf("非发布模式\\n"); // 不被编译
#endif
④嵌套条件编译
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
六、头文件包含
6.1、头文件被包含的方式
| #include <xxx.h> | 直接去标准系统路径查找 | 库文件(标准库、第三方库) |
| #include “xxx.h” | 先在当前源文件所在目录查找,找不到再去标准路径查找 | 本地头文件(项目自定义) |
库文件也可以用 “” 包含,但是查找效率低,不易区分是库还是本地文件。
6.2、嵌套文件包含
头文件被重复包含
// test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
预处理后,test.h 的内容会被拷贝 5 次,导致代码膨胀和编译压力增加。
解决方案1:头文件守卫(条件编译)
#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
void test();
struct Stu
{
int id;
char name[20];
};
#endif // __TEST_H__
解决方案2:#pragma once
#pragma once
// 头文件的内容
void test();
struct Stu
{
int id;
char name[20];
};




