在 C++ 中,可以通过宏定义命令 #define 简化代码编写过程,为开发提供极大的便利,但初学时我们常常不能理解该命令的本质,导致代码出现逻辑错误或者无法通过编译。本文总结了一些宏定义的常见用法,打算日后偶尔拿来当个字典用用(

宏替换发生的时机

  当我们对一些源文件进行编译时,编译器内部实际进行了预处理、编译、汇编和连接几个过程。其中预处理器产生编译器的输出,它实现了以下的功能:

  • 文件包含

  将源文件中的 #include 扩展为文件正文,即把包含的 .h 文件找到并展开到 #include 所在位置

  • 条件编译

  预处理器根据 #if#ifdef 等编译命令及其后的条件,将源文件中的某部分包含进来或排除在外(通常把排除在外的语句转换成空行)

  • 宏展开

  预处理器将源文件中出现的对宏的引用(即 #define 相关定义)展开成相应的宏定义,由预处理器来完成。在这个阶段只是纯粹的字符串替换与展开,没有进行任何计算,理解了这一点,就能有效避免对 #define 命令的误用

常用宏定义参考

无参宏定义

  无参宏的宏名后不带参数,其定义的一般形式为:

1
#define <标识符> <字符串>

  其中的 # 表示这是一条预处理命令。凡是以 # 开头的均为预处理命令。 #define 为宏定义命令。<标识符> 为所定义的宏名。字符串 可以是常数、表达式、格式串等。例如:

1
#define MAXN 23333

  这样 MAXN 就被定义为 23333,在宏展开过程中,会将 MAXN 简单的替换为 23333 进行处理。

有参宏定义

  C++ 语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要进行宏展开,而且要用实参去代换形参。带参宏定义的一般形式为:

1
#define 宏名(<参数表>) 字符串 

  在字符串中含有各个形参。在使用时调用带参宏调用的一般形式为:宏名(实参表),例如:

1
2
3
4
5
6
7
#include <iostream>
#define add(x, y) (x + y)

int main() {
std::cout << "1 + 1 = " << add(1, 1) << std::endl;
return(0);
}

  这个「函数」定义了加法,但是没有类型检查,有点类似无模板安全的模板,所以不建议使用这样的方式去定义一个「函数」。

宏定义中的特殊操作符

#

  假如希望在字符串中包含宏参数,在类函数宏的替换部分,可以将 # 符号用作一个预处理运算符,它将把语言符号转化成字符串。例如,如果 x 是一个宏参量,那么 #x 可以把参数名转化成相应的字符串。该过程称为字符串化

  讲人话,就是相当于在宏参数左右加双引号:

1
2
3
4
5
6
7
8
#include <cstdio>
#define psqr(n) printf("The square of '"#n"' is %d\n", (n)*(n))

int main() {
int n = 2; psqr(n);
psqr(1 + 2);
return 0;
}
输出
1
2
The square of 'n' is 4
The square of '1 + 2' is 9

##

  不同于 # 的操作方式,## 会把宏参数直接作为代码拼接到字符串中,例如,定义一个 #define conn(a, b) a##b,在代码中使用 conn(123, 456) 时,最终将被替换为 123456,或者可以像下面那样去使用:

1
2
3
4
5
6
7
8
9
10
#include <cstdio>
#define prtx(n) printf("x%d = %d\n", n, x##n)

int main() {
int x1 = 1, x2 = 2, x3 = 4;
prtx(1);
prtx(2);
prtx(3);
return 0;
}
输出
1
2
3
x1 = 1
x2 = 2
x3 = 4

...__VA_ARGS__

  __VA_ARGS__ 是一个可变参数的宏,这个宏是新的 C99 规范中新增的,实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏 __VA_ARGS__ 就可以被用在替换部分中,替换省略号所代表的字符串。

1
2
3
4
5
6
7
8
#include <cstdio>
#define pddln(...) printf("print: %d %d\n", __VA_ARGS__)

int main() {
pddln(1, 2);
pddln(3, 4);
return 0;
}
输出
1
2
print: 1 2
print: 3 4

需要注意的问题

  在编写代码的过程中,要时刻记住宏定义只是进行简单的字符串替换,否则就可能出现如下的问题:

1
2
3
4
5
6
7
8
#include <iostream>
#define N 2 + 2
using namespace std;

int main() {
cout << N * N << endl;
return 0;
}

  编译运行,程序并没有像预料之中的那样输出 16,而是输出了 8,因为在宏展开过程中只是简单的进行了字符串替换,这段代码在解析后会变成 2 + 2 * 2 + 2,导致结果为 8。解决方法也很简单,在做宏定义时加上括号就可以了

1
2
3
4
5
6
7
8
#include <iostream>
#define N (2 + 2)
using namespace std;

int main() {
cout << N * N << endl;
return 0;
}

  只要做宏定义的时候时刻记住,宏定义只是简单的替换,此时真正的编译过程尚未开始,就不会在这样的问题上出错

参考