宏是编译时预处理阶段用到的一种强大的工具,宏可以实现对指定代码片段的替换。依照笔者的理解,宏实际上是给某个特定的代码段起了一个别名。在预处理阶段,编译器将代码中的这个别名替换成相应的代码段。在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 #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),提示代码复用率。
- 宏在编译时期展开,运行时没有开销。
缺点
-
- 使用宏会造成代码调试困难。
- 宏不进行类型检测,容易造成类型错误。