C++ #define 宏定义

在 C++ 中,我们可以使用 #define 预处理指令来定义一个 “宏(Macro)”。宏的作用非常简单,用一句话概括就是:在正式编译开始前的预处理阶段,把代码中出现的宏名直接替换成指定的内容。

根据是否接收参数,#define 宏定义一般分为 2 种:一种是 “不带参数的宏定义”,另一种是 “带参数的宏定义”。

不带参数的宏定义

不带参数的宏定义是最基础的一种形式,常用于给某个固定的数字、字符串或代码片段起一个名字。它相当于在编译开始前,对当前源文件执行了一次文本上的 “查找与替换”。

语法:

#define 宏名 替换内容

说明:

为了和普通变量区分开来,宏名通常全部使用大写字母,多个单词之间使用下划线连接,比如 MAX_USERS、PI。此外,#define 是一条预处理指令,而不是普通的 C++ 语句,所以行尾千万不要加英文分号(;)。

示例 1:定义常量宏

#include <iostream>

#define MAX_USERS 100

int main() {
    std::cout << MAX_USERS << '\n';
    return 0;
}

运行结果如下。

100

分析:

#define MAX_USERS 100 表示定义一个名为 MAX_USERS 的常量宏。预处理器在正式编译前扫描后续代码时,只要遇到独立的标识符 MAX_USERS,就会把它直接替换成 100。需要注意的是,双引号字符串内部的 "MAX_USERS" 属于普通文本,不会被替换。

很多初学的小伙伴在写 #define 时,会习惯性地在末尾加上一个英文分号,比如写成 #define MAX_USERS 100;。为什么末尾不能加分号呢?因为 #define 做的是纯文本替换,连这个分号也会一起替换进去,上面的输出语句就会变成 std::cout << 100; << '\n';。可以看到,100 后面多出来一个分号,直接把输出语句从中间截断了,编译时自然就会报错。

提示 与普通变量不同,#define 定义的宏不受圆括号、花括号 {} 和命名空间的限制,从定义那一行开始一直到当前源文件末尾都会生效。如果我们希望在某个位置提前取消宏定义,直接写一行 #undef MAX_USERS 即可。

带参数的宏定义

除了替换固定内容,#define 还可以在宏名后面加上圆括号来接收一个或多个参数。这种宏的调用写法很像普通函数,但底层依然是编译前的纯文本替换。

语法:

#define 宏名(参数列表) 替换内容

说明:

当宏接收多个参数时,参数之间使用英文逗号(,)隔开。为了防止替换后运算顺序出错,我们通常会把每个参数和整个替换算式都用圆括号括起来。

此外要特别注意,定义带参数的宏时,宏名和左圆括号之间必须紧挨着,中间不能留空格。一旦留了空格,预处理器就会把它当成 “不带参数的宏”,把左括号也算进替换内容里。

// 正确方式
#define ADD(a, b) ((a) + (b))

// 错误方式
#define ADD (a, b) ((a) + (b))

下面我们先来看一个使用带参数宏的基础例子。

示例 2:定义带参数的宏

#include <iostream>

#define ADD(a, b) ((a) + (b))

int main() {
    std::cout << ADD(10, 20) << '\n';
    return 0;
}

运行结果如下。

30

分析:

#define ADD(a, b) ((a) + (b)) 表示定义了一个接收两个参数的宏。当预处理器遇到 ADD(10, 20) 时,会把实参 10 和 20 分别填入 a 和 b 的位置,将代码原地替换为 ((10) + (20)),最终算出结果 30。

此外,我们在阅读开源项目代码时,如果遇到某个宏的替换内容比较长、一行写不下的情况,经常会看到在行尾加上一个反斜杠(\)来换行续写:

#define PRINT_SUM(a, b) \
    std::cout << ((a) + (b)) << '\n'

这里的反斜杠(\)叫做 “续行符”,表示下一行依然属于当前这条 #define 指令。编写时要注意反斜杠后面必须直接按 Enter 键换行,后面不要多敲空格。

带参数宏的常见陷阱

带参数的宏虽然外表很像函数,但它并不会像函数那样 “先算出实参的值,再传进函数”,而只是把参数文本直接贴过去。如果不留心,很容易踩中下面这两个常见陷阱。

1. 漏加括号引发优先级错乱

如果在定义带参数的宏时,没有给每个参数和整个替换表达式都加上圆括号,一旦传入带有加减运算的算式,原本的运算顺序就会乱掉。下面我们来看一个对比示例。

示例 3:宏参数的括号保护

#include <iostream>

#define SQUARE_BAD(x) x * x      // 未加括号
#define SQUARE(x) ((x) * (x))    // 加上括号保护

int main() {
    std::cout << SQUARE_BAD(2 + 3) << '\n';
    std::cout << SQUARE(2 + 3) << '\n';
    return 0;
}

运行结果如下。

11
25

分析:

2 + 3 等于 5,5 的平方明明是 25,为什么 SQUARE_BAD(2 + 3) 算出来却是 11 呢?对于这个例子来说,上面两行输出语句经过预处理替换后,实际等价于下面代码。

std::cout << 2 + 3 * 2 + 3 << '\n';
std::cout << ((2 + 3) * (2 + 3)) << '\n';

在第 1 行中,按照 C++ 的运算符优先级,乘号(*)高于加号(+)。因此程序会先算中间的 3 * 2 = 6,再算 2 + 6 + 3,结果就变成了 11。而在第 2 行中,由于每个参数都用圆括号包了起来,保证了先算加法再算乘法,这才算出正确的 25。

那为什么最外层还要再套一对圆括号呢?假如我们只给参数加了括号,写成 #define DOUBLE_VAL(x) (x) + (x),当外部用除法调用 20 / DOUBLE_VAL(2) 时,展开后就会变成 20 / (2) + (2)。程序会先算除法得到 10,再加 2 算出错误结果 12,而不是预期的 5。

因此,平时编写带参数的宏时,小伙伴们记得把 “每个参数” 和 “整个表达式” 都用圆括号包起来。

2. 自增参数引发重复求值

就算给每个参数和外层整体都加足了圆括号,带参数的宏依然存在隐患。如果调用宏时传入了带有自增(++)、自减(--)或函数调用的表达式,这个表达式就可能会重复执行多次。

示例 4:传入自增表达式

#include <iostream>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int x = 3;
    int result = MAX(++x, 2);

    std::cout << "x = " << x << '\n';
    std::cout << "result = " << result << '\n';
    return 0;
}

运行结果如下。

x = 5
result = 5

分析:

在这个例子中,x 的初始值是 3。调用 MAX(++x, 2) 时,我们原本期望 ++x 只执行一次变成 4,再和 2 比较大小得出 4。但实际运行后发现,x 和 result 竟然都变成了 5。

这是因为 MAX(++x, 2) 经过文本替换后,实际变成了 int result = ((++x) > (2) ? (++x) : (2));。可以看到,参数 ++x 在替换后的三元运算符中出现了两次。第 1 次判断条件 (++x) > (2) 时,x 自增变成了 4。由于条件成立,程序接着计算问号后面的 (++x),导致 x 又自增了一次变成 5。

因此,大家在调用带参数的宏时,千万不要传入带有 ++ 或 -- 的表达式。

现代 C++ 的替代方案

#define 宏定义是从 C 语言继承过来的特性。由于宏只做机械的文本替换,没有数据类型检查,也无法在调试器中单步跟踪,所以在现代 C++ 开发中,我们通常会优先使用更安全的语法来替代它:

  • 定义常量:优先使用 const 常量,比如写成 const int MAX_USERS = 100;。它具有明确的数据类型和作用域,编译器也会帮我们做严格的类型检查。
  • 封装简短计算逻辑:优先编写普通函数或内联函数。如果希望同一段计算逻辑同时支持 int、double 等多种类型,可以使用前面学过的函数模板,避开优先级错乱和参数重复求值的问题。

那么在实际项目中,什么时候才真正需要用到 #define 呢?主要是在编写头文件保护,以及配合条件编译控制跨平台代码或调试开关的场景中。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号