C语言宏与条件编译

白天迁就生活,夜晚打理灵魂

Posted by yishuifengxiao on 2026-10-14

C 语言中的 #define、#ifdef、#endif、#else 都属于预处理指令。预处理发生在正式编译之前,由预处理器完成。它们不是 C 语句,因此通常行尾不加分号,以 # 开头,一条指令占一行,可以用反斜杠 \ 续行。


预处理概述

预处理主要做这些事:

  • 宏定义与宏替换:#define
  • 取消宏:#undef
  • 条件编译:#ifdef、#ifndef、#if、#elif、#else、#endif
  • 文件包含:#include
  • 错误指令:#error
  • 行控制等:#line、#pragma

其中条件编译常用于:

  • 调试版本 / 发布版本切换
  • 跨平台代码选择
  • 头文件防重复包含
  • 功能开关
  • 根据版本号选择代码

#define 的作用与用法

基本作用

#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 MAX_SIZE 100;

之后:

int arr[MAX_SIZE];

会变成:

int arr[100;];

这是语法错误。


对象宏与函数宏

对象宏

没有参数,只是简单替换:

#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);      // ((5) * (5))
int b = MAX(10, 20); // ((10) > (20) ? (10) : (20))

注意括号非常重要。

错误示例:

#define SQUARE(x) x * x

调用:

SQUARE(1 + 2)

展开为:

1 + 2 * 1 + 2

结果是 5,不是 9。

正确写法:

#define SQUARE(x) ((x) * (x))

调用:

SQUARE(1 + 2)

展开为:

((1 + 2) * (1 + 2))

结果是 9。


宏参数多次求值的副作用

函数宏只是文本替换,参数可能被求值多次。

例如:

#define SQUARE(x) ((x) * (x))

int i = 5;
int r = SQUARE(i++);

展开后类似:

int r = ((i++) * (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)); // 输出 "hello"
printf("%s\n", STR(123)); // 输出 "123"

如果参数本身是宏,需要二级展开:

#define STR(x) #x
#define XSTR(x) STR(x)

#define VERSION 123

printf("%s\n", STR(VERSION)); // 输出 "VERSION"
printf("%s\n", XSTR(VERSION)); // 输出 "123"

记号粘贴运算符 ##

## 可以把两个记号连接成一个记号。

#define CONCAT(a, b) a##b

int CONCAT(my, Var) = 10;
// 等价于 int myVar = 10;

常见用法:

#define MAKE_FUNC(name) void func_##name(void)

MAKE_FUNC(test) {
printf("test\n");
}
// 展开为 void func_test(void) { ... }

可变参数宏

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__)

这样:

LOG("hello\n");

也能工作。

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__ // 是否符合标准 C
__STDC_VERSION__ // C 标准版本

示例:

#define LOG(msg) printf("%s:%d: %s\n", __FILE__, __LINE__, msg)

条件编译:#ifdef、#endif、#else

条件编译让预处理器根据宏是否定义、宏的值等,决定编译哪一段代码。

#ifdef

作用:如果某个宏已经定义,则编译后面的代码。

格式:

#ifdef 宏名
// 如果宏已定义,编译这里
#endif

示例:

#define DEBUG

#ifdef DEBUG
printf("debug mode\n");
#endif

只要 DEBUG 被定义过,不管它的值是什么,#ifdef DEBUG 都为真。

例如:

#define DEBUG 0

这时:

#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 /* DEBUG */

#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;
}

输出:

debug build

如果没有 #define DEBUG,则输出:

release build

#ifndef

#ifndef 是 #ifdef 的反义:如果宏没有定义,则编译后面的代码。

格式:

#ifndef 宏名
// 如果宏未定义,编译这里
#endif

最常见用途是头文件保护:

#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,会输出调试信息。

如果改成:

#define DEBUG 0

则 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_H */

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
// #define FEATURE_B

#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 MAX 100;

正确:

#define MAX 100

宏参数和整体都要加括号

错误:

#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++); // 危险,i 被多次求值

优先用 static inline 函数。

#ifdef 只判断是否定义

#define DEBUG 0

#ifdef DEBUG
// 会编译这里
#endif

如果想按值判断,用:

#if 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) {
// X 在这里也有效
}

命令行定义宏

编译时可以用 -D 定义宏:

gcc -DDEBUG=1 main.c -o main

等价于在文件开头写:

#define DEBUG 1

也可以用 -U 取消宏:

gcc -UDEBUG main.c -o main

总结

指令 作用
#define 定义宏,进行文本替换
#ifdef 如果宏已定义,则编译后续代码
#ifndef 如果宏未定义,则编译后续代码
#else 条件不成立时编译另一段代码
#endif 结束条件编译块
#if 根据常量表达式进行条件编译
#elif 类似 else if
#undef 取消宏定义

简单记忆:

#define DEBUG 1

#ifdef DEBUG
// DEBUG 已定义,编译这里
#else
// DEBUG 未定义,编译这里
#endif

#if DEBUG
// DEBUG 的值非 0,编译这里
#else
// DEBUG 的值为 0,编译这里
#endif

#define 负责定义宏和文本替换;#ifdef、#endif、#else 负责根据宏是否定义来选择性地编译代码。它们组合起来,可以实现调试开关、平台适配、头文件保护、功能裁剪等非常实用的功能。