预处理器(preprocessor)在编译之前对源代码进行文本替换和条件处理。预处理指令以 # 开头,由预处理器在编译阶段展开。
#define PI 3.14159265358979
#define BUFFER_SIZE 1024
#define NULL ((void *)0)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
MAX(a,b) 是宏,MAX (a,b) 不是#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 10;
int result = MAX(x++, y++);
// 展开为: ((x++) > (y++) ? (x++) : (y++))
// x++ 执行了 2 次,y++ 执行了 2 次(或 1 次)
这是宏最危险的陷阱:参数被多次求值。解决方案:使用 GCC 的语句表达式(({...})) 或改用 inline 函数。
| 特性 | #define |
const |
|---|---|---|
| 处理阶段 | 预处理(文本替换) | 编译(类型检查) |
| 类型安全 | 无 | 有 |
| 调试 | 可能显示替换后的值 | 保留符号名 |
| 作用域 | 定义后到 #undef 或文件末尾 |
遵循 C 作用域规则 |
| 内存占用 | 不占用(除非取地址) | 占用(有存储地址) |
| 适用场景 | 编译时常量、条件编译、位掩码 | 运行时常量 |
#include <stdio.h> /* 在系统头文件目录中查找 */
#include "myheader.h" /* 先在当前目录查找,再在系统目录查找 */
#include 可以嵌套,但深度有限制(通常 8-16 层)#ifndef MYHEADER_H
#define MYHEADER_H
/* 头文件内容 */
#endif /* MYHEADER_H */
#pragma once
/* 头文件内容 */
对比:
| 特性 | #ifndef/#define/#endif |
#pragma once |
|---|---|---|
| 标准支持 | C89 起所有编译器 | 非标准,但所有主流编译器支持 |
| 可移植性 | 完美 | 99% 场景可用 |
| 符号冲突风险 | 有(宏名可能冲突) | 无 |
| 对符号链接/硬链接 | 可能重复包含 | 正确处理 |
#ifdef DEBUG
/* 调试代码 */
#endif
#ifndef NDEBUG
/* 非调试代码 */
#endif
#if defined(__linux__)
/* Linux 特定代码 */
#elif defined(_WIN32)
/* Windows 特定代码 */
#else
/* 其他平台 */
#endif
#if VERSION >= 2
/* 新版本特性 */
#endif
条件编译在 Linux 内核中大量使用,用于支持多平台编译(ARM、x86、MIPS 等)。
#define LIMIT 100
/* ... */
#undef LIMIT
/* LIMIT 不再定义 */
用于重新定义宏或限制宏的作用域。
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
#error "This code requires C11 or later"
#endif
在编译时产生错误消息,用于编译期条件检查。
| 宏 | 含义 |
|---|---|
__FILE__ |
当前源文件名(字符串) |
__LINE__ |
当前行号(整数) |
__DATE__ |
编译日期(字符串,如 "Aug 31 2026") |
__TIME__ |
编译时间(字符串) |
__func__ |
当前函数名(C99,字符串) |
__STDC__ |
编译器遵循 ANSI C(1) |
__STDC_VERSION__ |
C 标准版本号(C99: 199901L, C11: 201112L, C17: 201710L) |
__STDC_HOSTED__ |
实现是否 hosted(1)或 freestanding(0) |
将宏参数转换为字符串字面量:
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)
int x = 42;
PRINT_VAR(x); /* 展开为: printf("x" " = %d\n", x); 即 printf("x = %d\n", x); */
将两个 token 拼接为一个:
#define CONCAT(a, b) a##b
#define MAKE_STRUCT(prefix, name) struct prefix##_##name
int var10 = 100;
printf("%d\n", CONCAT(var, 10)); /* 输出 100 */
MAKE_STRUCT(sensor, data) { /* 展开为: struct sensor_data { ... }; */
int value;
};
#define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
#define DEBUG_LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)
C99 引入 __VA_ARGS__,C23 引入 __VA_OPT__(,) 用于处理可变参数为空的情况。
#define SWAP(a, b) do { \
typeof(a) _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
do { ... } while (0) 模式确保宏在 if-else 语句中安全使用。
#include <stdio.h>
#define PI 3.14159265358979
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define STRINGIFY(x) #x
#define CONCAT(a, b) a##b
#define DEBUG_LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define SWAP(a, b) do { \
__typeof__(a) _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
int main(void) {
printf("=== 对象宏 ===\n");
printf("PI = %.15f\n", PI);
printf("sizeof(int) = %zu\n", sizeof(int));
printf("\n=== 函数宏 ===\n");
int x = 5;
printf("SQUARE(%d) = %d\n", x, SQUARE(x));
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MIN(3, 7) = %d\n", MIN(3, 7));
printf("\n=== # 运算符(字符串化)===\n");
int var = 42;
printf("STRINGIFY(hello) = \"%s\"\n", STRINGIFY(hello world));
printf("STRINGIFY(1+2) = \"%s\"\n", STRINGIFY(1 + 2));
printf("var 的值: ");
printf(#var " = %d\n", var); /* 等价于 printf("var = %d\n", var); */
printf("\n=== ## 运算符(token 拼接)===\n");
int val10 = 100;
int val20 = 200;
printf("CONCAT(val, 10) = %d\n", CONCAT(val, 10));
printf("CONCAT(val, 20) = %d\n", CONCAT(val, 20));
printf("\n=== SWAP 宏 ===\n");
int a = 10, b = 20;
printf("交换前: a=%d, b=%d\n", a, b);
SWAP(a, b);
printf("交换后: a=%d, b=%d\n", a, b);
printf("\n=== DEBUG_LOG ===\n");
DEBUG_LOG("程序启动,x = %d", x);
return 0;
}
编译命令:
gcc -std=c99 -Wall -Wextra -o preprocess1 preprocess1.c
./preprocess1
config.h(自定义头文件):
#ifndef CONFIG_H
#define CONFIG_H
#define APP_NAME "PreprocessorDemo"
#define APP_VERSION "1.0.0"
#define MAX_DEVICES 16
typedef struct {
int id;
char name[32];
} Device;
#endif /* CONFIG_H */
main.c:
#include <stdio.h>
#include "config.h"
int main(void) {
printf("App: %s v%s\n", APP_NAME, APP_VERSION);
printf("Max devices: %d\n", MAX_DEVICES);
Device dev = { .id = 1, .name = "sensor_01" };
printf("Device: [%d] %s\n", dev.id, dev.name);
return 0;
}
编译命令:
gcc -std=c99 -Wall -Wextra -o include_demo main.c
./include_demo
#include <stdio.h>
/* 编译器检测 */
#if defined(__GNUC__)
#define COMPILER "GCC " __VERSION__
#elif defined(_MSC_VER)
#define COMPILER "MSVC " _MSC_VER
#else
#define COMPILER "Unknown Compiler"
#endif
/* 平台检测 */
#if defined(__linux__)
#define PLATFORM "Linux"
#elif defined(_WIN32)
#define PLATFORM "Windows"
#elif defined(__APPLE__)
#define PLATFORM "macOS"
#else
#define PLATFORM "Unknown"
#endif
/* 调试模式开关 */
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
#define LOG(fmt, ...) ((void)0)
#endif
/* C 标准版本检查 */
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
#define C_STANDARD "C11"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
#define C_STANDARD "C99"
#else
#define C_STANDARD "C89/C90"
#endif
/* 错误检查 */
#if MAX_DEVICES < 1
#error "MAX_DEVICES must be at least 1"
#endif
/* 调试与发布模式下不同的数组大小 */
#ifdef DEBUG
#define TRACE_SIZE 1024
#else
#define TRACE_SIZE 256
#endif
void print_info(void) {
LOG("print_info 被调用");
printf("Compiler: %s\n", COMPILER);
printf("Platform: %s\n", PLATFORM);
printf("C Standard: %s\n", C_STANDARD);
printf("File: %s\n", __FILE__);
printf("Date: %s %s\n", __DATE__, __TIME__);
printf("Function: %s\n", __func__);
printf("Trace buffer size: %d\n", TRACE_SIZE);
}
int main(void) {
LOG("程序启动");
print_info();
/* 条件编译示例:不同平台不同行为 */
#if defined(__linux__)
printf("Linux 特定代码\n");
#elif defined(_WIN32)
printf("Windows 特定代码\n");
#else
printf("其他平台代码\n");
#endif
LOG("程序结束");
return 0;
}
编译命令:
# 普通模式
gcc -std=c99 -Wall -Wextra -DDEBUG -o preprocess3 preprocess3.c
./preprocess3
# 发布模式(关闭 DEBUG)
gcc -std=c99 -Wall -Wextra -o preprocess3_release preprocess3.c
./preprocess3_release
#include <stdio.h>
/* 编译期版本检查 */
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L
#error "This code requires C99 or later"
#endif
/* 检查特定头文件是否可用 */
#if defined(__has_include)
#if __has_include(<stdint.h>)
#define HAS_STDINT_H 1
#else
#define HAS_STDINT_H 0
#endif
#else
#define HAS_STDINT_H 1 /* 假设有 */
#endif
#if HAS_STDINT_H
#include <stdint.h>
#endif
/* 用 #undef 重定义宏 */
#define LIMIT 100
#undef LIMIT
#define LIMIT 200
int main(void) {
printf("LIMIT = %d\n", LIMIT);
#if HAS_STDINT_H
printf("stdint.h available, sizeof(uint32_t) = %zu\n", sizeof(uint32_t));
#endif
return 0;
}
编译命令:
gcc -std=c99 -Wall -Wextra -o preprocess4 preprocess4.c
./preprocess4
宏参数不加括号的危险:
#define SQUARE_BAD(x) x * x
SQUARE_BAD(2 + 3) /* 展开为 2 + 3 * 2 + 3 = 11,而非 25 */
宏中的分号陷阱:
#define SET(x) x = 0;
if (cond)
SET(val)
else /* 语法错误!分号导致 if-else 断裂 */
do_something();
#define 不加分号:#define PI 3.14; 会导致 PI 被替换为 3.14;,使用时产生语法错误。
头文件保护宏命名冲突:不同头文件使用相同保护宏名会导致其中一个被静默跳过。使用唯一命名(如 PROJECT_MODULE_FILENAME_H)。
## 拼接空 token:C 标准禁止 ## 产生空 token,但 GCC 扩展允许。可移植代码应避免。
#if 中的 0 而非 false:#if 只接受整数常量表达式,不能用 true/false(C99 之前没有 _Bool)。
多行宏的续行符 \:反斜杠后不能有空格,否则续行失败。续行符后必须紧跟换行符。
Q1:#define 和 inline 函数有什么区别?
A:(1) #define 是文本替换,无类型检查;inline 是函数,有完整类型检查。(2) #define 可能导致参数多次求值(副作用);inline 只求值一次。(3) #define 在预处理阶段展开,调试困难;inline 函数在编译阶段处理,保留符号信息。(4) #define 可以用于条件编译、token 拼接等无法用函数替代的场景。
Q2:#pragma once 和 include guard 哪个更好?
A:两者各有优势。#pragma once 更简洁、无符号冲突风险、对硬链接正确。include guard 是 C 标准方式,可移植性完美(适用于所有符合标准的编译器)。实际中,主流编译器(GCC、Clang、MSVC)都支持 #pragma once,但如果需要极致可移植性,用 include guard。
Q3:为什么内核代码大量使用条件编译?
A:Linux 内核需要在数百种硬件平台上编译运行。条件编译用于:(1) 平台特定代码(ARM vs x86);(2) 编译配置(CONFIG_SMP 多处理器支持);(3) 调试/性能分析开关;(4) 不同编译器版本兼容。这使得同一份代码可以在极不同的环境下编译。
Q4:#include 尖括号和双引号的本质区别是什么?
A:预处理器在不同路径集合中查找。尖括号 <stdio.h> 在系统指定的 include 路径(如 /usr/include)中查找。双引号 "myheader.h" 先在当前源文件所在目录查找,找不到再回退到系统路径。用户自定义头文件应始终用双引号。
Q5:如何安全地定义函数宏?
A:遵循以下规则:(1) 每个参数都加括号;(2) 整个表达式加括号;(3) 使用 do { ... } while (0) 包裹多语句宏;(4) 使用双下划线前缀的临时变量名避免命名冲突;(5) 如果可能,优先使用 static inline 函数替代函数宏。