C 语言中的 #define、#ifdef、#endif、#else 都属于预处理指令 。预处理发生在正式编译之前,由预处理器完成。它们不是 C 语句,因此通常行尾不加分号 ,以 # 开头,一条指令占一行,可以用反斜杠 \ 续行。
预处理概述 预处理主要做这些事:
宏定义与宏替换:#define
取消宏:#undef
条件编译:#ifdef、#ifndef、#if、#elif、#else、#endif
文件包含:#include
错误指令:#error
行控制等:#line、#pragma
其中条件编译常用于:
调试版本 / 发布版本切换
跨平台代码选择
头文件防重复包含
功能开关
根据版本号选择代码
#define 的作用与用法基本作用 #define 用来定义宏。宏本质上是文本替换 。
格式:
例如:
#define PI 3.1415926 #define MAX_SIZE 100
预处理器会把代码中出现的 PI 替换成 3.1415926,把 MAX_SIZE 替换成 100。
示例:
#include <stdio.h> #define PI 3.1415926 #define MAX_SIZE 100 int main (void ) { double area = PI * 10 * 10 ; int arr[MAX_SIZE]; printf ("area = %f\n" , area); printf ("sizeof(arr) = %zu\n" , sizeof (arr)); return 0 ; }
注意:#define 行尾不要写分号。
错误示例:
之后:
会变成:
这是语法错误。
对象宏与函数宏 对象宏 没有参数,只是简单替换:
#define BUFFER_SIZE 1024 #define VERSION "1.0.0" #define ENABLE_FEATURE
其中 ENABLE_FEATURE 是空宏,常用于条件编译。
函数宏 带参数:
#define SQUARE(x) ((x) * (x)) #define MAX(a, b) ((a) > (b) ? (a) : (b))
使用:
int a = SQUARE(5 ); int b = MAX(10 , 20 );
注意括号非常重要。
错误示例:
调用:
展开为:
结果是 5,不是 9。
正确写法:
#define SQUARE(x) ((x) * (x))
调用:
展开为:
结果是 9。
宏参数多次求值的副作用 函数宏只是文本替换,参数可能被求值多次。
例如:
#define SQUARE(x) ((x) * (x)) int i = 5 ;int r = SQUARE(i++);
展开后类似:
这是未定义行为。i 被多次修改,结果不可预测。
因此,带副作用的表达式不要传给函数宏。
如果需要类似功能,优先使用 static inline 函数:
static inline int square (int x) { return x * x; }
多行宏 宏可以写成多行,用反斜杠 \ 续行:
#define SWAP(a, b) do { \ int tmp = (a); \ (a) = (b); \ (b) = tmp; \ } while (0 )
使用:
int x = 1 , y = 2 ;SWAP(x, y);
为什么用 do { ... } while (0)?
因为这样宏在语法上表现得像一条语句:
if (cond) SWAP(x, y); else ...
展开后不会出错。如果直接写成 { ... },在 if/else 中可能出问题。
注意:反斜杠后面不能有空格。
字符串化运算符 # 在函数宏中,# 可以把参数转换成字符串。
#define STR(x) #x printf ("%s\n" , STR(hello)); printf ("%s\n" , STR(123 ));
如果参数本身是宏,需要二级展开:
#define STR(x) #x #define XSTR(x) STR(x) #define VERSION 123 printf ("%s\n" , STR(VERSION)); printf ("%s\n" , XSTR(VERSION));
记号粘贴运算符 ## ## 可以把两个记号连接成一个记号。
#define CONCAT(a, b) a##b int CONCAT (my, Var) = 10 ;
常见用法:
#define MAKE_FUNC(name) void func_##name(void) MAKE_FUNC(test) { printf ("test\n" ); }
可变参数宏 C99 支持可变参数宏,使用 ... 和 __VA_ARGS__。
#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)
使用:
LOG("x = %d, y = %d\n" , x, y);
但如果可变参数为空,标准 C99 处理起来比较麻烦。GNU 扩展可以这样写:
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
这样:
也能工作。
C23 引入了 __VA_OPT__,标准写法可以是:
#define LOG(fmt, ...) printf(fmt __VA_OPT__(,) __VA_ARGS__)
实际项目中,GCC/Clang 常用 ##__VA_ARGS__,但要注意它不是标准 C。
#undef 取消宏#define DEBUG 1 #ifdef DEBUG ... #endif #undef DEBUG #ifdef DEBUG ... #else ... #endif
#undef 之后,DEBUG 就不再是已定义宏。
预定义宏 C 语言有一些预定义宏:
__FILE__ __LINE__ __DATE__ __TIME__ __STDC__ __STDC_VERSION__
示例:
#define LOG(msg) printf("%s:%d: %s\n" , __FILE__, __LINE__, msg)
条件编译:#ifdef、#endif、#else 条件编译让预处理器根据宏是否定义、宏的值等,决定编译哪一段代码。
#ifdef作用:如果某个宏已经定义 ,则编译后面的代码。
格式:
示例:
#define DEBUG #ifdef DEBUG printf ("debug mode\n" ); #endif
只要 DEBUG 被定义过,不管它的值是什么,#ifdef DEBUG 都为真。
例如:
这时:
#ifdef DEBUG printf ("debug mode\n" ); #endif
仍然会编译 printf。因为 #ifdef 只检查“是否定义”,不检查值。
#endif作用:结束一个条件编译块。
#ifdef DEBUG printf ("debug\n" ); #endif
#endif 必须和前面的 #ifdef、#ifndef、#if 配对。
可以加注释提高可读性:
#ifdef DEBUG printf ("debug\n" ); #endif
#else作用:如果前面的条件不成立,则编译 #else 后面的代码。
格式:
#ifdef DEBUG printf ("debug mode\n" ); #else printf ("release mode\n" ); #endif
如果 DEBUG 已定义,编译第一段;否则编译第二段。
示例:
#include <stdio.h> #define DEBUG int main (void ) {#ifdef DEBUG printf ("debug build\n" ); #else printf ("release build\n" ); #endif return 0 ; }
输出:
如果没有 #define DEBUG,则输出:
#ifndef#ifndef 是 #ifdef 的反义:如果宏没有定义 ,则编译后面的代码。
格式:
最常见用途是头文件保护:
#ifndef MY_HEADER_H #define MY_HEADER_H #endif
第一次包含该头文件时,MY_HEADER_H 未定义,于是进入代码,并定义 MY_HEADER_H。第二次包含时,MY_HEADER_H 已定义,条件为假,内容被跳过,从而防止重复包含。
#if、#elif 与 defined虽然用户重点问 #ifdef、#endif、#else,但实际中经常配合 #if、#elif 使用。
#if 后面跟常量表达式:
#define VERSION 2 #if VERSION >= 2 printf ("version 2 or later\n" ); #elif VERSION == 1 printf ("version 1\n" ); #else printf ("unknown version\n" ); #endif
defined 运算符用于判断宏是否定义:
#if defined(DEBUG) printf ("debug\n" ); #endif
等价于:
#ifdef DEBUG printf ("debug\n" ); #endif
也可以组合:
#if defined(_WIN32) #define OS "Windows" #elif defined(__linux__) #define OS "Linux" #elif defined(__APPLE__) #define OS "macOS" #else #define OS "Unknown" #endif
注意:
#ifdef 后只能写宏名。
#if 后可以写常量表达式。
#if 中未定义的标识符通常按 0 处理。
#ifdef DEBUG 与 #if defined(DEBUG) 等价。
#if DEBUG 检查的是 DEBUG 的值。
例如:
#define DEBUG 0 #if DEBUG printf ("debug\n" ); #else printf ("release\n" ); #endif
这里 DEBUG 虽然定义了,但值为 0,所以编译 #else 分支。
而:
#ifdef DEBUG printf ("debug\n" ); #else printf ("release\n" ); #endif
因为 DEBUG 已定义,所以编译 printf("debug\n")。
这是非常容易混淆的地方。
综合示例 示例 1:调试日志开关 #include <stdio.h> #define DEBUG 1 #if DEBUG #define DBG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n" , __FILE__, __LINE__, ##__VA_ARGS__) #else #define DBG(fmt, ...) ((void)0) #endif int main (void ) { int x = 10 ; DBG("program start" ); DBG("x = %d" , x); printf ("hello\n" ); return 0 ; }
如果 DEBUG 为 1,会输出调试信息。
如果改成:
则 DBG(...) 被替换为 ((void)0),不会产生任何操作。
这里使用 #if DEBUG 而不是 #ifdef DEBUG,是因为我们关心的是值,而不是是否定义。
示例 2:头文件保护 my_header.h:
#ifndef MY_HEADER_H #define MY_HEADER_H #include <stdio.h> void hello (void ) ;#endif
my_header.c:
#include "my_header.h" void hello (void ) { printf ("hello\n" ); }
main.c:
#include "my_header.h" #include "my_header.h" int main (void ) { hello(); return 0 ; }
如果没有头文件保护,重复包含可能导致结构体、函数声明等重复定义。
示例 3:跨平台选择 #include <stdio.h> #if defined(_WIN32) #define PLATFORM "Windows" #elif defined(__linux__) #define PLATFORM "Linux" #elif defined(__APPLE__) #define PLATFORM "macOS" #else #define PLATFORM "Unknown" #endif int main (void ) { printf ("platform = %s\n" , PLATFORM); return 0 ; }
这里用 #if defined(...) 比多个 #ifdef 更清晰。
示例 4:功能开关 #define FEATURE_A #include <stdio.h> int main (void ) {#ifdef FEATURE_A printf ("feature A enabled\n" ); #else printf ("feature A disabled\n" ); #endif #ifdef FEATURE_B printf ("feature B enabled\n" ); #else printf ("feature B disabled\n" ); #endif return 0 ; }
输出:
feature A enabled feature B disabled
示例 5:宏函数、字符串化、连接 #include <stdio.h> #define SQUARE(x) ((x) * (x)) #define STR(x) #x #define XSTR(x) STR(x) #define CONCAT(a, b) a##b #define VERSION 123 int main (void ) { int CONCAT (my, Var) = 5 ; printf ("SQUARE(1+2) = %d\n" , SQUARE(1 + 2 )); printf ("STR(VERSION) = %s\n" , STR(VERSION)); printf ("XSTR(VERSION) = %s\n" , XSTR(VERSION)); printf ("myVar = %d\n" , myVar); return 0 ; }
输出:
SQUARE(1+2) = 9 STR(VERSION) = VERSION XSTR(VERSION) = 123 myVar = 5
示例 6:多行宏 #include <stdio.h> #include <stdlib.h> #define CHECK(cond) do { \ if (!(cond)) { \ fprintf (stderr , "check failed: %s\n" , #cond); \ exit (EXIT_FAILURE); \ } \ } while (0 ) int main (void ) { int x = 10 ; CHECK(x > 0 ); CHECK(x < 100 ); printf ("all checks passed\n" ); return 0 ; }
CHECK 展开后是一条完整的 do while 语句,可以安全地放在 if/else 中。
常见陷阱与最佳实践 #define 末尾不要加分号错误:
正确:
宏参数和整体都要加括号 错误:
#define SQUARE(x) x * x #define ADD(a, b) a + b
正确:
#define SQUARE(x) ((x) * (x)) #define ADD(a, b) ((a) + (b))
避免参数有副作用 #define SQUARE(x) ((x) * (x)) int i = 5 ;int r = SQUARE(i++);
优先用 static inline 函数。
#ifdef 只判断是否定义#define DEBUG 0 #ifdef DEBUG #endif
如果想按值判断,用:
#else 必须与条件编译配对#ifdef A ... #else ... #endif
#else 不能单独出现。
#endif 不要遗漏条件编译可以嵌套:
#ifdef A #ifdef B ... #else ... #endif #else ... #endif
每个 #ifdef、#ifndef、#if 都必须有对应的 #endif。
多行宏反斜杠后不要有空格 错误:
#define FOO(x) do { \ ... \ } while (0 )
如果 \ 后有多余空格,续行可能失效。
头文件保护宏名要唯一 例如:
#ifndef PROJECT_MODULE_FILE_H #define PROJECT_MODULE_FILE_H ... #endif
不要用太通用的名字,比如 HEADER_H。
宏没有类型、没有作用域 宏从定义处到 #undef 或文件结尾都有效,不受 {} 块作用域限制。
#define X 10 void f (void ) { }
命令行定义宏 编译时可以用 -D 定义宏:
gcc -DDEBUG=1 main.c -o main
等价于在文件开头写:
也可以用 -U 取消宏:
gcc -UDEBUG main.c -o main
总结
指令
作用
#define
定义宏,进行文本替换
#ifdef
如果宏已定义,则编译后续代码
#ifndef
如果宏未定义,则编译后续代码
#else
条件不成立时编译另一段代码
#endif
结束条件编译块
#if
根据常量表达式进行条件编译
#elif
类似 else if
#undef
取消宏定义
简单记忆:
#define DEBUG 1 #ifdef DEBUG #else #endif #if DEBUG #else #endif
#define 负责定义宏和文本替换;#ifdef、#endif、#else 负责根据宏是否定义来选择性地编译代码。它们组合起来,可以实现调试开关、平台适配、头文件保护、功能裁剪等非常实用的功能。