#define 是 C 语言预处理器(Preprocessor)的一条指令,它在程序被编译之前执行,预处理器会扫描你的代码,并根据 #define 的指令进行文本替换。

#define 主要有两种核心用途:
- 定义宏
- 定义常量
定义宏
这是 #define 最强大和最常见的用法,宏可以分为两种:对象式宏和函数式宏。
a) 对象式宏
它看起来像一个常量,但实际上是一个简单的文本替换。
语法:

#define MACRO_NAME replacement_text
示例:
#include <stdio.h>
#define PI 3.14159
#define BUFFER_SIZE 1024
int main() {
double radius = 5.0;
double area = PI * radius * radius;
char buffer[BUFFER_SIZE];
printf("The area of the circle is: %f\n", area);
printf("The buffer size is: %d\n", BUFFER_SIZE);
return 0;
}
工作原理:
在编译之前,预处理器会扫描整个文件,并将所有出现的 PI 替换为 14159,将所有出现的 BUFFER_SIZE 替换为 1024,编译器实际看到的代码是:
int main() {
double radius = 5.0;
double area = 3.14159 * radius * radius;
char buffer[1024];
// ...
}
优点:
- 可读性高:
PI比14159更容易理解。 - 易于维护:如果你想改变
PI的精度,只需在#define那里修改一次,所有用到PI的地方都会自动更新。
重要提示: 在 C 语言中,推荐使用 const 关键字来定义真正的常量,因为它有类型检查,更安全。
const double PI = 3.14159; // 推荐做法 const int BUFFER_SIZE = 1024;
b) 函数式宏
它看起来像一个函数,可以带参数,但本质上是文本替换。
语法:
#define MACRO_NAME(param1, param2, ...) replacement_text
注意: 参数列表和替换文本之间不能有空格。
示例 1:简单的求和
#include <stdio.h>
#define ADD(a, b) ((a) + (b))
int main() {
int x = 5, y = 10;
int sum = ADD(x, y);
printf("Sum is: %d\n", sum); // 输出 Sum is: 15
return 0;
}
工作原理:
ADD(x, y) 会被替换为 ((x) + (y)),注意括号!括号对于宏至关重要,可以确保运算符的优先级正确。
为什么需要那么多括号?
考虑一个没有括号的错误宏定义:#define BAD_ADD(a, b) a + b
如果你调用 BAD_ADD(2, 3) * 4,它会被替换成 2 + 3 * 4,根据优先级,结果是 2 + 12 = 14,而不是预期的 (2+3)*4 = 20。
而使用正确的 ADD 宏:ADD(2, 3) * 4 会被替换成 ((2) + (3)) * 4,结果就是 20,符合预期。
示例 2:带副作用的宏(危险!) 宏的一个巨大风险是参数被多次求值。
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
int result = SQUARE(a++); // 危险!
// 预处理器替换后:((a++) * (a++))
// 第一次使用 a: a=5, 使用后 a=6
// 第二次使用 a: a=6, 使用后 a=7
// result = 5 * 6 = 30
// a 的最终值是 7,而不是期望的 6
printf("result = %d, a = %d\n", result, a); // 输出可能是 result = 30, a = 7
return 0;
}
这个例子展示了宏的危险性,如果参数有副作用(如 i++),宏可能会导致难以预料的结果,这就是为什么在现代 C 编程中,优先使用内联函数(inline)来替代简单的函数式宏。
条件编译
#define 还可以与预处理指令 #ifdef, #ifndef, #if, #else, #elif, #endif 结合使用,实现条件编译,这意味着你可以根据条件编译代码的不同部分。
常用场景:
- 跨平台开发:为 Windows (
_WIN32) 和 Linux (__linux__) 编写不同的代码。 - 调试:定义一个宏(如
DEBUG),在调试时包含打印日志的代码,在发布版本中则不包含。 - 功能开关:通过宏开启或关闭某些功能模块。
示例:调试开关
#include <stdio.h>
// 在编译时,通过命令行定义宏, gcc -D DEBUG main.c
// 或者直接在这里取消注释下一行
// #define DEBUG
int main() {
int value = 42;
#ifdef DEBUG
printf("[DEBUG] The value is: %d\n", value);
#else
printf("Release mode: No debug info.\n");
#endif
return 0;
}
如何工作:
- 如果你在编译时定义了
DEBUG(例如通过-D选项或在代码中#define DEBUG),#ifdef DEBUG为真,编译器会编译printf("[DEBUG] ...");这一行。 - 如果没有定义
DEBUG,则编译器会编译#else后面的printf("Release mode ...");。
其他条件编译指令:
#ifndef:如果未定义则编译,常用于头文件的保护,防止重复包含。#if:可以进行更复杂的条件判断,#if (VERSION == 1)。
头文件保护
这是 #define 在头文件中最重要的应用,可以防止头文件被重复包含导致的编译错误。
问题场景:
假设你有一个 myheader.h 文件,它被 main.c 和 utils.c 两个源文件包含。main.c 又包含了 utils.c,myheader.h 的内容就会被编译两次,导致“重定义”错误。
解决方案:使用 #ifndef / #define / #endif
标准写法:
// myheader.h #ifndef MYHEADER_H // MYHEADER_H 这个宏没有被定义 #define MYHEADER_H // 那么就定义它 // 头文件的实际内容,比如函数声明、结构体定义等 void my_function(); #endif // 结束条件块
工作原理:
- 第一次
#include "myheader.h":MYHEADER_H未定义,#ifndef条件为真,预处理器执行#define MYHEADER_H并编译头文件内容。 - 第二次
#include "myheader.h":MYHEADER_H已经被定义了,#ifndef条件为假,预处理器直接跳过#define和所有头文件内容,直到#endif。
这样就完美地防止了重复包含。
| 用法 | 语法 | 示例 | 优点 | 缺点/注意事项 |
|---|---|---|---|---|
| 对象式宏 | #define NAME value |
#define PI 3.14 |
提高可读性,易于维护 | 无类型检查,推荐使用 const |
| 函数式宏 | #define NAME(p) ... |
#define MAX(a,b) ((a)>(b)?(a):(b)) |
可能比函数调用更快(无函数调用开销) | 无类型检查,有求值多次的风险,难以调试 |
| 条件编译 | #ifdef NAME ... #endif |
#ifdef _WIN32 ... #endif |
实现跨平台、调试、功能开关 | 使代码逻辑变得复杂 |
| 头文件保护 | #ifndef GUARD ... #define GUARD ... #endif |
#ifndef MYHEADER_H ... #endif |
防止头文件重复包含 | 每个 .h 文件都需要一个唯一的宏名(通常是文件名全大写) |
#define 是 C 语言中一个非常基础且强大的工具,虽然现代 C/C++ 推荐使用 const、enum 和 inline 函数来替代部分宏的用法,但在条件编译和头文件保护等场景下,#define 仍然是不可或替代的。
