宏(macro)是 C/C++ 预处理器提供的文本替换机制,在正式编译之前由预处理器完成替换。本文介绍对象式宏、条件编译、宏函数、# 与 ## 运算符,以及预定义宏,并说明现代 C++ 中宏的替代方案。

一、宏的本质

宏不是变量也不是函数,而是编译前的文本替换。比如:

1
#define NUM 123

预处理阶段,代码里所有的 NUM 都会被原样替换成 123,然后才进入编译。理解这一点是看懂宏所有「坑」的前提。

二、对象式宏(不带参数)

1
2
#define NUM 123        // 定义
#undef NUM // 取消定义,之后 NUM 恢复为未定义状态
  • 宏名通常全大写,和普通变量区分(约定俗成,非语法要求);
  • 定义行末尾不要加分号——分号会被一起替换进去,多数场景下多余甚至出错;
  • 宏没有类型,不做类型检查,只是纯文本。

现代 C++ 中,常量优先用 const / constexpr:

1
constexpr int NUM = 123;   // 有类型、有作用域、编译器会检查,更安全

三、条件编译

条件编译指令根据「宏是否被定义」决定某段代码是否参与编译。

3.1 判断宏是否定义:#ifdef / #ifndef

1
2
3
4
5
6
#ifdef NUM            // 如果 NUM 已定义
#undef NUM
#define NUM 10
#else // 否则
#define NUM 20
#endif

配套指令:

  • #ifdef X:X 已定义时编译;
  • #ifndef X:X 未定义时编译(常用于头文件保护);
  • #else / #elif:分支;
  • #endif:结束,每个 #if 都必须有对应结尾。

3.2 按宏的值做比较:#if(注意不是 #ifdef)

⚠️ #ifdef 只能判断「是否定义」,不能写 #ifdef NUM > 10 这种表达式,预处理器不支持这种语法。

要按值比较必须用 #if:

1
2
3
4
5
6
7
#if NUM > 10
// NUM 已定义且值大于 10 时编译
#elif NUM == 10
// 等于 10
#else
// 其他情况
#endif

判断「已定义且满足条件」可以组合 defined():

1
2
3
#if defined(NUM) && NUM > 10
// NUM 被定义,且值大于 10
#endif

3.3 头文件保护

防止头文件被重复包含:

1
2
3
4
5
// 老式写法
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 头文件内容……
#endif
1
2
// 现代编译器通用写法,等价且更简洁
#pragma once

四、宏函数(带参数的宏)

语法:

1
#define 标识符(参数列表) 替换表达式

4.1 括号陷阱

1
#define MUL(a, b) a * b

宏是文本替换,调用 MUL(3, 1+1) 会变成:

1
3 * 1 + 1      // 结果是 4,不是预期的 6!

给每个参数和整个表达式都加上括号即可:

1
#define MUL(a, b) ((a) * (b))

现在 MUL(3, 1+1) 展开为 ((3) * (1+1)) = 6。

4.2 参数被重复求值的副作用

宏没有函数调用机制,参数可能在展开后出现多次:

1
2
3
4
#define SQUARE(x) ((x) * (x))

int i = 3;
int y = SQUARE(i++); // 展开为 ((i++) * (i++)),i 被自增两次,行为未定义!

这是宏函数最危险的坑之一。同样的问题用内联函数则完全不会发生。

4.3 用 inline 函数替代

C++ 中,类型相关的「宏函数」应优先用 inline 函数:

1
2
3
inline int mul(int a, int b) {
return a * b;
}

内联函数有类型检查、参数只求值一次、遵循作用域规则,还能被调试器单步,远比宏安全。现代 C++ 的经验法则是:能用 constexpr / inline / 模板解决的,就不要用宏。宏主要保留给条件编译、头文件保护、日志中需要 #/## 的场景。

五、# 运算符:字符串化

#参数 会把传入的实参转成字符串字面量:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;

#define PRINT(a) cout << #a << " = " << (a) << endl

int main() {
float a = 3.0f;
PRINT(a * 2 + 3);
return 0;
}

PRINT(a * 2 + 3) 展开后相当于:

1
cout << "a * 2 + 3" << " = " << (a * 2 + 3) << endl;

输出:

1
a * 2 + 3 = 9

常用于调试日志——同时打印「表达式文本」和「计算结果」。

六、## 运算符:标记连接

## 把左右两个标记(token)拼接成一个新标记:

1
2
3
#define MEMBER(type, name) type m_##name

MEMBER(int, a); // 展开为 int m_a;

注意连接的是标识符片段,不是字符串;拼接结果必须是合法的标记。常用于批量生成变量名、函数名。

七、预定义宏

编译器内置了一组宏(注意两边都是双下划线):

宏 示例 说明
__FILE__ "main.cpp" 当前源文件名(字符串)
__LINE__ 42 当前行号(整数)
__DATE__ "Sep 23 2026" 编译日期
__TIME__ "10:30:00" 编译时间
__cplusplus 201703L C++ 标准版本号,C 编译器中不定义

一个简单的调试打印宏:

1
#define LOG(msg) cout << "[" << __FILE__ << ":" << __LINE__ << "] " << msg << endl

利用 __cplusplus 还可以区分当前按 C 还是 C++、按哪个标准编译:

1
2
3
#if __cplusplus >= 201703L
// 当前是 C++17 或更新
#endif

八、小结

  • 宏是预处理阶段的文本替换,没有类型、不做检查;
  • 对象式宏行尾不要加分号,常量优先用 const/constexpr;
  • #ifdef 只判断「是否定义」,比较值要用 #if + defined();头文件用 #ifndef 或 #pragma once 防重复包含;
  • 宏函数要给参数和整体都加括号,但仍有副作用参数重复求值的致命坑,能用 inline 就不用宏;
  • # 把参数字符串化,## 拼接标记;
  • 预定义宏 __FILE__、__LINE__、__DATE__、__TIME__、__cplusplus 两边都是双下划线。