C 预处理深度精讲:宏、条件编译与头文件
写代码时,总有这种时刻:同一处常量改十遍、调试信息删了又加、不同平台来回注释。预处理就像给你一把“遥控器”,按几个键就能全局生效、按场景切换。本文用好懂的例子带你把它玩顺手。
目录
1. 预定义符号
这些是预处理阶段就能用的“内置变量”,无需自己声明:
-
__FILE__:当前编译的源文件名(字符串) -
__LINE__:当前行号(整型) -
__DATE__:编译日期(字符串) -
__TIME__:编译时间(字符串) -
__STDC__:遵循 ANSI C 则为 1,否则未定义
示例:定位日志信息
#include <stdio.h>
int main() {
printf("file: %s line: %d date: %s time: %s\n",
__FILE__, __LINE__, __DATE__, __TIME__);
return 0;
}
2. #define 定义常量
语法:
#define NAME stuff
示例:
#define MAX 1000
#define reg register // 给关键字取“别名”
#define do_forever for(;;) // 语义化代替实现
#define CASE break;case // 写 switch 时自动补上 break
// 续行写法(行尾加反斜杠)
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n", \
__FILE__, __LINE__, __DATE__, __TIME__)
注意:常量宏后面别加分号 ;
示例:
#define MAX 1000; // ❌
if (ok)
x = MAX; // 展开后 if 分支里有两条语句,且无 {},语法容易出错
else
x = 0;
正确写法:
#define MAX 1000 // ✅
3. #define 定义宏(带参数)
语法:
#define NAME(param-list) stuff
-
左括号必须紧贴宏名,中间不能有空格,否则参数表会被当成替换文本的一部分。
注意:运算优先级与括号
3.1 给形参和整体都加括号
#define SQUARE(x) (x) * (x)
int a = 5;
printf("%d\n", SQUARE(a + 1)); // 期望是 36
// 预处理后: (a + 1) * (a + 1) ✅
如果忘记括号:
#define SQUARE_BAD(x) x * x
printf("%d\n", SQUARE_BAD(a + 1)); // 变成 a + 1 * a + 1 => 11 ❌
再看一个:
#define DOUBLE(x) ((x) + (x))
printf("%d\n", 10 * DOUBLE(5)); // 10 * ((5) + (5)) => 100 ✅
如果没有给“整体”再套一层括号:
#define DOUBLE_BAD(x) (x) + (x)
printf("%d\n", 10 * DOUBLE_BAD(5)); // 10 * 5 + 5 => 55 ❌
结论:
用于数值表达式的宏,应该写成:
对每个形参加括号,再对整个表达式加一层括号。
4. 宏参数的副作用
当同一个参数在宏里被用多次,而这个参数带副作用(如 x++),会出现多次执行的问题。
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int x = 5, y = 8;
int z = MAX(x++, y++);
// 展开:z = ((x++) > (y++) ? (x++) : (y++));
// x、y 可能各自自增两次,结果不可控(x=6 y=10 z=9 这类“意外”)
结论:
-
宏不要对带副作用的实参做多次求值。
-
若无法避免,考虑改成内联函数(C99 也可用
static inline)或普通函数。
5. 宏替换规则(发生了什么)
当你写下一个宏调用,预处理器的大致流程是:
-
先检查实参里是否还包含其他宏,若有,先替换实参里的宏。
-
把宏体替换到源代码上,对应参数名替换为刚处理过的实参文本。
-
对替换后的结果再次扫描,若还包含能替换的宏,继续替换。
补充:
-
宏的参数和宏体中,都可以再出现其他
#define的名字,但宏不能递归展开。 -
字符串字面量中的内容不会被继续展开。
6. 宏 vs 函数(何时用宏,何时用函数)
宏的优势:
-
零调用开销:没有函数调用/返回成本。适合非常短小、频繁调用的逻辑。
-
类型无关:参数只做“文本替换”,可对多种类型工作(只要操作符支持)。
宏的劣势:
-
可读性/可调试性差:宏无法单步调试,错误定位不直观。
-
代码膨胀:每用一次就“贴”一段代码,太长会让目标文件暴增。
-
容易踩坑:优先级、副作用、命名冲突等。
例:类型作为参数(函数做不到,但宏能“生成代码”)
#define MALLOC(num, type) ((type*)malloc((num) * sizeof(type)))
int *p = MALLOC(10, int);
// 展开后: (int*)malloc(10 * sizeof(int))
结论实用版:
-
短小、纯表达式、不涉及副作用的场景,可以用宏。
-
复杂逻辑、需要调试/类型检查、安全性更高时,用函数或内联函数。
7. # 和 ##
7.1 #(字符串化,stringize)
把宏参数变成字符串字面量。
#include <stdio.h>
#define PRINT(n) printf("the value of " #n " is %d\n", (n))
int a = 10;
PRINT(a); // 预处理后 => printf("the value of " "a" " is %d\n", a);
输出:
the value of a is 10
7.2 ##(记号粘合,token pasting)
把 ## 两边的符号拼接生成新的记号(标识符等)。
示例:用一个“模板宏”生成不同类型的 max 函数名
#define GENERIC_MAX(type) \
type type##_max(type x, type y) { \
return x > y ? x : y; \
}
// 生成两个函数:int_max / float_max
GENERIC_MAX(int)
GENERIC_MAX(float)
int main() {
int m = int_max(2, 3);
float fm = float_max(3.5f, 4.5f);
printf("%d\n%f\n", m, fm);
return 0;
}
8. 命名约定
-
宏名通常全大写,例如:
MAX_SIZE、PRINT_DEBUG。 -
函数名遵循普通命名法,不全部大写。
这能让读代码时一眼区分“这是宏还是函数”。
9. #undef
用于移除一个已有的宏定义,常见场景:准备重新定义。
#define MAX 1000
#undef MAX
#define MAX 2048 // 重新定义
10. 命令行定义
很多编译器支持在命令行直接定义宏,常用于按环境切配置。
示例:数组大小由命令行决定
#include <stdio.h>
#ifndef ARRAY_SIZE
#define ARRAY_SIZE 8
#endif
int main() {
int a[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; ++i) a[i] = i;
for (int i = 0; i < ARRAY_SIZE; ++i) printf("%d ", a[i]);
puts("");
return 0;
}
编译:
# Linux 示例
gcc -DARRAY_SIZE=16 main.c -o main
11. 条件编译(按需“开关”代码)
开发中常常希望“调试时多打印,发布时不打印”,或“不同平台走不同实现”——用条件编译就很舒服。
最常用的几类:
// 1) 简单开关
#define __DEBUG__
#ifdef __DEBUG__
printf("debug info...\n");
#endif
// 2) 常量表达式
#define FOO 1
#if FOO
puts("FOO is on");
#endif
// 3) 多分支
#if defined(OS_UNIX)
unix_impl();
#elif defined(OS_MSDOS)
msdos_impl();
#else
generic_impl();
#endif
// 4) 是否被定义
#if defined(SYMBOL) // 等价于 #ifdef SYMBOL
#if !defined(SYMBOL) // 等价于 #ifndef SYMBOL
示例:只在调试时打印数组赋值过程
#include <stdio.h>
// #define __DEBUG__ // 打开试试
int main() {
int arr[10] = {0};
for (int i = 0; i < 10; ++i) {
arr[i] = i;
#ifdef __DEBUG__
printf("arr[%d]=%d\n", i, arr[i]);
#endif
}
return 0;
}
12. 头文件的包含
12.1 两种包含方式
-
本地文件:
#include "filename.h"
先从当前源文件目录找;找不到再去标准路径。 -
库头文件:
#include <filename.h>
直接从标准路径找。
需要效率/语义清晰时:库头用尖括号,本地头用引号。
12.2 防止重复包含
一个头文件被多次 #include,其内容会被拷贝多次,导致重复定义或编译极慢。
解决方案:头文件保护(Include Guard) 或 #pragma once。
写法一:Include Guard
// test.h
#ifndef __TEST_H__
#define __TEST_H__
void test(void);
struct Stu {
int id;
char name[20];
};
#endif // __TEST_H__
写法二:#pragma once
#pragma once
// 其余内容同上
多数主流编译器支持 #pragma once,简洁,但 Include Guard 更标准、可移植。
13. 其他预处理指令
-
#error:在预处理阶段主动报错并终止编译(常用于条件不满足时阻断构建)。 -
#pragma:编译器相关的特殊指令(如结构体对齐#pragma pack)。 -
#line:改变编译器认为的行号/文件名(主要给工具链用)。
这类指令按需了解即可;#pragma pack 在结构体内存布局时最常用。
完
为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。
更多推荐


所有评论(0)