1 引言
C语言程序从源代码到可执行文件经历了四个阶段:预处理、编译、汇编、链接。预处理器是第一个阶段,它处理以#开头的指令,对源代码进行文本替换。
c
#include <stdio.h> /* 文件包含 */
#define PI 3.14159 /* 宏定义 */
#define SQUARE(x) ((x)*(x)) /* 带参数的宏 */
int main(void)
{
printf("PI = %f\n", PI);
printf("5的平方 = %d\n", SQUARE(5));
return 0;
}
预处理后的代码:
c
/* 展开 #include <stdio.h> 的内容(约800行) */
/* 展开 PI → 3.14159 */
/* 展开 SQUARE(5) → ((5)*(5)) */
int main(void)
{
printf("PI = %f\n", 3.14159);
printf("5的平方 = %d\n", ((5)*(5)));
return 0;
}
本章我们将深入探讨预处理器的各种用法。
2 #define定义符号常量
2.1 基本用法
#define用于定义符号常量,在预处理阶段将符号替换为指定的文本。
c
#define MAX_SIZE 100
#define PI 3.14159
#define FILE_NAME "data.txt"
int arr[MAX_SIZE]; /* 预处理后 → int arr[100]; */
double area = PI * r * r; /* → 3.14159 * r * r */
2.2 命名规范
-
通常使用全大写字母,单词间用下划线分隔
-
这样可以与变量名区分,提高可读性
c
#define MAX_BUFFER 1024
#define DEFAULT_PORT 8080
#define ERROR_FILE_NOT_FOUND -1
2.3 使用const还是#define?
| 特性 | #define | const |
|---|---|---|
| 类型检查 | 无(纯文本替换) | 有 |
| 内存占用 | 无(替换后消失) | 有(占用内存) |
| 调试可见 | 调试器看不到宏名 | 调试器可见变量名 |
| 作用域 | 从定义到文件结束 | 遵循C语言作用域规则 |
建议:
-
简单的数值常量可以用
#define -
需要类型检查或调试的场景用
const -
C++推荐用
const或constexpr
2.4 注意事项
c
/* 错误:不要加分号 */
#define MAX 100; /* 展开后 arr[100;]; 语法错误! */
/* 正确 */
#define MAX 100
/* 错误:宏定义中不要使用等号 */
#define PI = 3.14159 /* 展开后 area = = 3.14159 * r * r; 语法错误! */
3 宏函数(带参数的宏)
3.1 基本语法
宏函数可以带参数,参数在替换时被展开。
c
#define SQUARE(x) ((x)*(x))
#define MAX(a,b) ((a)>(b)?(a):(b))
#define ABS(x) ((x)<0?-(x):(x))
int main(void)
{
int a = SQUARE(5); /* → ((5)*(5)) = 25 */
int b = MAX(10, 20); /* → ((10)>(20)?(10):(20)) = 20 */
int c = ABS(-5); /* → ((-5)<0?-(-5):(-5)) = 5 */
return 0;
}
3.2 为什么要加这么多括号
宏是文本替换,不加括号可能导致运算优先级问题。
c
/* 错误:缺少括号 */
#define SQUARE(x) x*x
int r = SQUARE(2+3); /* 展开为 2+3*2+3 = 2+6+3 = 11,不是25! */
/* 正确:加上括号 */
#define SQUARE(x) ((x)*(x))
int r = SQUARE(2+3); /* 展开为 ((2+3)*(2+3)) = 25 */
c
/* 错误:缺少外层括号 */
#define DOUBLE(x) (x)+(x)
int r = 2 * DOUBLE(5); /* 展开为 2 * (5)+(5) = 10+5 = 15,不是20! */
/* 正确 */
#define DOUBLE(x) ((x)+(x))
int r = 2 * DOUBLE(5); /* 展开为 2 * ((5)+(5)) = 20 */
规则:宏定义中的每个参数都要用括号括起来,整个表达式也要用括号括起来。
3.3 宏函数 vs 函数
| 特性 | 宏函数 | 普通函数 |
|---|---|---|
| 执行效率 | 无调用开销(直接展开) | 有调用开销(压栈、跳转) |
| 代码体积 | 每次调用都展开,体积增大 | 代码只出现一次 |
| 类型检查 | 无(任何类型都可传入) | 有(严格类型检查) |
| 副作用 | 参数可能被多次求值 | 参数只求值一次 |
| 调试 | 难以调试 | 可设断点 |
c
/* 宏函数的副作用问题 */
#define SQUARE(x) ((x)*(x))
int a = 5;
int r = SQUARE(a++); /* 展开为 ((a++)*(a++)),a被自增两次! */
printf("r=%d, a=%d\n", r, a); /* 结果不确定(未定义行为) */
3.4 多语句宏
如果宏函数包含多条语句,需要用do { ... } while(0)包裹。
c
/* 错误:多条语句在if中展开会有问题 */
#define SWAP(a,b) { int t=a; a=b; b=t; }
if (x > y)
SWAP(x, y);
else
x = 0;
/* 展开后:
if (x > y)
{ int t=x; x=y; y=t; };
else
x = 0;
/* 分号导致else孤立,编译错误! */
c
/* 正确:用 do-while(0) 包裹 */
#define SWAP(a,b) do { int t=a; a=b; b=t; } while(0)
/* 展开后:
if (x > y)
do { int t=x; x=y; y=t; } while(0);
else
x = 0;
/* 语法正确,且不影响流程 */
4 条件编译
4.1 #ifdef / #ifndef / #endif
条件编译用于根据条件决定哪些代码被编译。
c
#define DEBUG 1
#ifdef DEBUG /* 如果 DEBUG 被定义 */
printf("调试信息:x=%d\n", x);
#endif
#ifndef NDEBUG /* 如果 NDEBUG 未被定义 */
printf("调试信息\n");
#endif
4.2 #if / #elif / #else
可以进行更复杂的条件判断。
c
#define VERSION 2
#if VERSION == 1
printf("版本1的功能\n");
#elif VERSION == 2
printf("版本2的新功能\n");
#else
printf("未知版本\n");
#endif
4.3 防止头文件重复包含
这是条件编译最常见的应用------头文件守卫。
c
/* myheader.h */
#ifndef MYHEADER_H
#define MYHEADER_H
/* 头文件内容 */
#endif
4.4 调试代码
c
#include <stdio.h>
#define DEBUG 1
int main(void)
{
int x = 10;
#ifdef DEBUG
printf("调试模式:x = %d\n", x);
#endif
#if DEBUG > 1
printf("详细调试信息\n");
#endif
return 0;
}
4.5 跨平台代码
c
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM_WINDOWS
#include <windows.h>
#elif defined(__linux__)
#define PLATFORM_LINUX
#include <unistd.h>
#elif defined(__APPLE__)
#define PLATFORM_MACOS
#include <mach/clock.h>
#else
#error "Unsupported platform"
#endif
void sleep_ms(int ms)
{
#ifdef PLATFORM_WINDOWS
Sleep(ms);
#else
usleep(ms * 1000);
#endif
}
5 #和##运算符
5.1 #运算符:字符串化
# 将宏参数转换为字符串字面量。
c
#define STR(x) #x
#define PRINT_INT(x) printf(#x " = %d\n", x)
int main(void)
{
printf("%s\n", STR(Hello)); /* 输出 "Hello" */
printf("%s\n", STR(123)); /* 输出 "123" */
int a = 10;
PRINT_INT(a); /* 输出 "a = 10" */
PRINT_INT(a + 5); /* 输出 "a + 5 = 15" */
return 0;
}
5.2 ##运算符:连接
## 将两个记号连接成一个新记号。
c
#define CONCAT(a,b) a##b
#define MAKE_VAR(name, num) name##num
int main(void)
{
int var1 = 10;
int var2 = 20;
printf("%d\n", CONCAT(var, 1)); /* 输出 10 */
printf("%d\n", MAKE_VAR(var, 2)); /* 输出 20 */
return 0;
}
5.3 综合应用:泛型宏
c
#include <stdio.h>
#define PRINT_INT(x) printf(#x " = %d\n", x)
#define PRINT_FLOAT(x) printf(#x " = %f\n", x)
#define PRINT_STRING(x) printf(#x " = %s\n", x)
/* 根据类型选择不同的打印宏 */
#define PRINT(x, type) \
(type == 1 ? PRINT_INT(x) : \
type == 2 ? PRINT_FLOAT(x) : \
PRINT_STRING(x))
/* 更好的方法:利用C11的_Generic(这里不展开) */
int main(void)
{
int a = 10;
float b = 3.14;
char *c = "Hello";
PRINT(a, 1);
PRINT(b, 2);
PRINT(c, 3);
return 0;
}
5.4 应用:自动生成枚举和字符串映射
c
#include <stdio.h>
/* 定义宏来自动生成枚举和字符串数组 */
#define COLORS \
X(RED) \
X(GREEN) \
X(BLUE) \
X(YELLOW)
/* 生成枚举 */
typedef enum {
#define X(name) name,
COLORS
#undef X
} Color;
/* 生成字符串数组 */
const char* color_names[] = {
#define X(name) #name,
COLORS
#undef X
};
int main(void)
{
for (int i = 0; i < 4; i++) {
printf("Color %d: %s\n", i, color_names[i]);
}
return 0;
}
6 预定义宏
C语言预定义了一些有用的宏:
| 宏 | 含义 |
|---|---|
__LINE__ |
当前行号 |
__FILE__ |
当前文件名 |
__DATE__ |
编译日期("MMM DD YYYY") |
__TIME__ |
编译时间("HH:MM:SS") |
__func__ |
当前函数名(C99) |
__STDC__ |
是否遵循ANSI C标准 |
c
#include <stdio.h>
void debug(const char *msg)
{
printf("[%s:%d] %s\n", __FILE__, __LINE__, msg);
}
int main(void)
{
printf("编译日期:%s\n", __DATE__);
printf("编译时间:%s\n", __TIME__);
debug("程序开始");
return 0;
}
7 常见错误与注意事项
7.1 宏定义后加分号
c
#define MAX 100; /* 错误! */
int arr[MAX]; /* 展开为 int arr[100;]; 语法错误 */
7.2 宏函数参数缺少括号
c
#define SQUARE(x) x*x /* 错误!缺少括号 */
int r = SQUARE(2+3); /* 展开为 2+3*2+3 = 11 */
7.3 宏函数重复求值副作用
c
#define MAX(a,b) ((a)>(b)?(a):(b))
int x = 5, y = 10;
int m = MAX(x++, y++); /* x++ 和 y++ 各被求值两次,不确定行为 */
7.4 宏名与变量名冲突
c
#define MAX 100
int MAX = 200; /* 错误!MAX已被定义为宏,展开后 int 100 = 200; */
7.5 忘记#undef
c
#define SIZE 100
/* ... 使用SIZE ... */
/* 之后想用SIZE作为变量名,但宏还存在 */
#undef SIZE /* 取消宏定义 */
int SIZE = 50; /* 现在合法 */
7.6 条件编译的#else忘记处理
c
#ifdef DEBUG
printf("调试信息\n");
/* 忘记 #else 或 #endif,后续代码被意外包含 */
#endif
8 本章小结
本章系统介绍了预处理器的各种功能:
1. #define定义符号常量
-
语法:
#define 名称 替换文本 -
用于定义常量、数组大小、错误码等
-
避免使用分号,使用全大写命名
2. 宏函数
-
语法:
#define 宏名(参数) 替换文本 -
每个参数都要加括号,整个表达式也要加括号
-
多语句宏用
do { ... } while(0)包裹 -
注意副作用问题
3. 条件编译
-
#ifdef/#ifndef/#endif:检查宏是否定义 -
#if/#elif/#else:条件判断 -
用途:头文件守卫、调试代码、跨平台开发
4. #和##运算符
-
#参数:将参数转换为字符串 -
参数1##参数2:连接两个记号 -
用于生成代码、泛型宏、自动生成枚举/字符串映射
5. 预定义宏
__LINE__、__FILE__、__DATE__、__TIME__、__func__
6. 注意事项
-
宏定义后不要加分号
-
宏参数必须加括号
-
注意宏的副作用
-
避免宏名冲突
-
使用
#undef取消宏定义