宏是编译时预处理阶段用到的一种强大的工具,宏可以实现对指定代码片段的替换。依照笔者的理解,宏实际上是给某个特定的代码段起了一个别名。在预处理阶段,编译器将代码中的这个别名替换成相应的代码段。在C++当中,我们可以使用#define指令来定义宏。

#define PI 3.14159265358979323846

  在这个宏中,就给3.14159265358979323846起了PI这样一个别名,在C++中,可以使用PI来替代圆周率,这样代码看上去会更加清晰明了。

宏的作用

  常量替换

    如上文所述,使用宏可以实现代码常量的替换,避免使用魔数(magic numbers)的产生,提高代码的可读性。例如上面提到的圆周率这个例子,一般约定,常量的别名都是大写。

  实现简单的函数定义

    使用宏可以在一定程度上替代函数,在C++中,常常使用宏来替代简单的函数。例如下面这个例子:

#define MAX(x,y) ((x)>(y)?(x):(y))

    因为宏是纯粹的文本替换,在用它来代替函数时,如果使用不慎,可能造成意想不到的逻辑错误,例如下面这个例子。

1 // 错误的示例,使用宏实现平方根计算
2 #include <iostream>
3 #define AREA(x) x*x
4 
5 int main()
6 {
7     std::cout<<AREA(2+2)<<std::endl;
8     return 0;
9 }

    这段代码的预期结果是16,但他的实际运行结果是8。这是因为在宏替换之后,第七行的代码实际上变成了:

std::cout<<2+2*2+2<<std::endl;

    解决的方式是使用函数宏,给每个用到参数的地方加上括号。仅仅给参数加上括号是不够的,如果我们将AREA的结果作为除数,也可能产生替换错误。

1 // 错误的示例2 预期结果是4,实际是16
2 #include <iostream>
3 #define AREA(x) (x)*(x)
4 
5 int main()
6 {
7     std::cout<<AREA(2+2)/AREA(1+1)<<std::endl;
8     return 0;
9 }

    第7行的代码实际上变成了这样:

std::cout<<(2+2)*(2+2)/(1+1)*(1+1)<<std::endl;

    因此像函数一样使用宏时,我们需要给整个要替换的代码段加上括号。

#define AREA(x) ((x)*(x))

 多行宏

    在C++中,如果你需要将一个宏定义分成多行来写,可以使用反斜杠(\)来指示宏定义在下一行继续。另外,如果宏的内容比较复杂,那么建议使用do...while(0)结构将要替换的代码段包括起来。

 1 //多行宏
 2 #include<iostream>
 3 #define SWAP(x,y) \
 4     do {    \
 5         decltype(x) tmp;\
 6         tmp = x; \
 7         x = y; \
 8         y = tmp;\
 9     }while(0);
10 
11 int main()
12 {
13     int x = 1;
14     int y = 2;
15     SWAP(x,y);
16     std::cout<<"x:"<< x <<"   y:"<<y<<std::endl;
17     return 0;
18 }

    这样做的好处是可以将宏的内容成为一个独立的语法单元,避免调用时产生的错误。

 宏定义中的特殊符号的含义

    在C++的宏当中,有2个特殊符号。

  1. #:用来为参数加上双引号。
  2. ##:用来将两端内容连接在一起。
 1 // 宏当中的特殊符号
 2 #include<iostream>
 3 #define CONNECT(x,y) x##y 
 4 #define TOSTRING(x) #x
 5 
 6 int main()
 7 {
 8     int n = CONNECT(11,22);
 9     std::cout<<n<<std::endl;
10     std::string str = TOSTRING(Hello World);
11     std::cout<<str<<std::endl;
12     return 0;
13 }

运行结果如下:

 预定义宏

  gcc 的预处理器预定了很多宏,这些宏被称为“预定义宏”(Predefined Macros)。对于 C 语言,预定义宏包含三类:标准的(standard)、通用的(common)和特定于系统的(system-specific)。对于 C++ 语言,除了上述三类,还多了一类:命名运算符(named operators)。

  标准的预定义宏:由相关的语言标准指定,因此实现这些标准的所有编译器都可以使用它们。

  通用的预定义宏:GNU C 扩展的预定义宏。

  特定于系统的预定义宏:表示系统和机器类型的宏。

  命名运算符:在 C++ 中,有 11 个关键字,它们是常用标点符号的替代写法。即使在预处理器中,也可以使用这些关键词,如在 #if 中拿它们充当运算符。在 C 中,可以通过包含 iso646.h 来要求这些关键字具有 C++ 的含义。该头文件将它们定义为普通的宏,并扩展到相应的标点符号。

更多预定义宏的相关知识,可以参考链接:https://gcc.gnu.org/onlinedocs/cpp/Predefined-Macros.html#Predefined-Macros

宏当中的不定长参数

C99标准中,__VA_ARGS__是一个用来表示不定长参数的宏。

#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__);
  预处理器执行这条指令时,会将"...",也就是不定长参数替换__VA_ARGS__,##在这里的作用是,如果可变长参数为空,那么忽略前面多出的逗号。

在gcc编译器中,可以使用另外一种语法来实现宏的不定长参数,在"..."前加上args,args可替代原来的__VA_ARGS__。

#define LOG(fmt, args...) printf(fmt, ##args);

调用它的效果和第一条指令是完全一样的

总结:宏的优点和缺点

  优点

    • 使用宏能避免魔数(magic number),提示代码复用率。
    • 宏在编译时期展开,运行时没有开销。

  缺点

    • 使用宏会造成代码调试困难。
    • 宏不进行类型检测,容易造成类型错误。