C/C++-技巧-宏

来源:互联网 发布:java高级应用课件 编辑:程序博客网 时间:2024/05/18 00:44

一、宏基础

宏在c/c++中扮演者比较重要的角色,虽然难以阅读和调试的缺点让宏的使用饱受诟病,但是在一些特殊的情况下,使用宏会带来极大的方便,甚至可以实现一些用其他方式无法实现的功能。

在c/c++程序编译的过程中,编译器对宏的处理是在预编译阶段进行的,处理方式的核心思想是:简单替换,编译器并不会对宏本身和宏的参数进行任何类型、语法上的检查,这也是导致宏不易阅读、不易调试的原因,也可能产生一些比较隐蔽的陷阱破环程序原本设计的逻辑。

1、宏的分类

宏对象:没有参数的宏。这类宏常常被用来定义常量,通常比较简单,例如:

[cpp] view plain copy
  1. #define MAX_NUM 100  
宏函数:带有参数的宏。这类宏的应用场景很多,比如定义函数、产生代码等等,随着用法的不同,难易程度也有很大的波动,例如:

[cpp] view plain copy
  1. #define MAX(a, b) ((a)>(b) ? (a) : (b))  

2、宏的操作符

#:字符串化一个宏参数,即在参数名字前后加上"。例如:

[cpp] view plain copy
  1. #define STRINGIZE(arg) #arg  
注意:当arg中包含空格的时候,预处理器只会保留一个空格,比如STRINGIZE(abc    abc)将会被替换成"abc abc",但是arg前后的空格将被忽略;当arg中包含特殊字符时,预处理器会自动添加上转义字符'/'以保证#arg返回完整的字符串化后的arg,比如STRINGIZE("a'b/c")将返回"/"a/'b//c/"",但是前提是arg本身不会对宏STRINGIZE语句参数影响,比如STRINGIZE(abc')将产生错误。

#@:字符化一个宏参数,即在参数名字前后加上'。例如:

[cpp] view plain copy
  1. #define CHARIZE(arg) #@arg  
##:拼接宏参数和另一个符号,即连接两个符号生成一个新的符号。例如:

[cpp] view plain copy
  1. #define SYMBOL_CATENATE(arg1, arg2) arg1 ## arg2  

注意:如果#、##操作的参数也是一个宏,那么这个宏将不会被继续展开,但是如果确实需要#、##后的宏继续展开,也可以定义辅助宏过度一下:

[cpp] view plain copy
  1. #define CHARIZE_WITH_MACRO(arg) CHARIZE(arg)  
  2. #define SYMBOL_CATENATE_WITH_MACRO(arg1, arg2) SYMBOL_CATENATE(arg1, arg2)  

\:换行,即开始新的一行继续定义宏体。例如:

[cpp] view plain copy
  1. #define DEFINE_VARIABLE(name1, name2, type) type name1; \  
  2.     type name2;  

3、变参宏

宏函数也可以接受个数不定的参数,形参写为...,在宏体内获取形参使用__VA_ARGS__,例如:

[cpp] view plain copy
  1. #define PRINTF(format, ...) printf(format, __VA_ARGS__);  
注意:当__VA_ARGS__作为宏实参再次被传入另一个宏函数的时候,在VC下直接编译时__VA_ARGS__只会被解释为一个参数,例如下面代码:

[cpp] view plain copy
  1. #define ATTR_1(arg) printf(arg);  
  2. #define ATTR_2(arg, ...) ATTR_1(arg) ATTR_1(__VA_ARGS__)  
  3. #define ATTR_3(arg, ...) ATTR_1(arg) ATTR_2(__VA_ARGS__)  
ATTR_3("1", "2", "3")将会产生编译错误,因为查看其宏展开后的实际代码为:printf("1"); printf("2", "3"); printf();,即ATTR_2(__VA_ARGS__)将("2", "3")当成了一个参数。解决办法是使用辅助宏ATTR():

[cpp] view plain copy
  1. #define ATTR(args) args  
  2. #define ATTR_1(arg) printf(arg);  
  3. #define ATTR_2(arg, ...) ATTR_1(arg) ATTR(ATTR_1(__VA_ARGS__))  
  4. #define ATTR_3(arg, ...) ATTR_1(arg) ATTR(ATTR_2(__VA_ARGS__))  

4、内置宏

c/c++标准中预定义了几个宏,只要编译器是支持标准的即可以在代码中直接使用这些宏:

__LINE__ // 当前代码行的行号
__FILE__ // 源程序的完整路径
__DATE__ // 系统日期
__TIME__ // 系统时间
__TIMESTAMP__   // 系统时间戳
__FUNCTION__ // 当前代码行所在的函数的名字
__STDC__ // 当要求程序严格遵循ANSI C标准时该标识被赋值为1
__cplusplus // 当编写C++程序时该标识符被定义

另外有一些是编译器相关的预定义宏:

VC:_MSC_VER// VC编译器版本号

更多参考:点击打开链接

GCC/G++:__GNUC__// GNU编译器版本号

更多参考:点击打开链接、点击打开链接

二、常用宏技巧

1、遍历变参宏的每个参数

宏只是简单替换的过程,所以不支持任何逻辑判断语句,但是依然可以用多条宏来实现相同的功能。

在实现遍历遍历每个宏参数之前,先看看怎么实现简单的统计参数的个数。首先编译器没有提供任何可以直接使用来计算参数个数的方法,所以需要使用一点技巧来实现这个功能:数轴占位,即把参数依次放到数轴每个点上,那么最后一个没被安放位置上的数就是参数的个数,不过这里需要颠倒一下占位,实现:

[cpp] view plain copy
  1. // 假设宏参数个数上限为10,否则需要手动扩展  
  2. #define COUNT_PARMS_IMP(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, NUM, ...) NUM  
  3. #define COUNT_PARMS(...) \  
  4.     ATTR(COUNT_PARMS_IMP(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0))  
利用类似的思路,使用多条宏语句,来分离出每一个参数,即可以模拟遍历参数的功能:

[cpp] view plain copy
  1. // 假设宏参数个数上限为10,否则需要手动扩展  
  2. #define ARG_1(arg) printf(arg);  
  3. #define ARG_2(arg, ...) ARG_1(arg) ATTR(ARG_1(__VA_ARGS__))  
  4. #define ARG_3(arg, ...) ARG_1(arg) ATTR(ARG_2(__VA_ARGS__))  
  5. #define ARG_4(arg, ...) ARG_1(arg) ATTR(ARG_3(__VA_ARGS__))  
  6. #define ARG_5(arg, ...) ARG_1(arg) ATTR(ARG_4(__VA_ARGS__))  
  7. #define ARG_6(arg, ...) ARG_1(arg) ATTR(ARG_5(__VA_ARGS__))  
  8. #define ARG_7(arg, ...) ARG_1(arg) ATTR(ARG_6(__VA_ARGS__))  
  9. #define ARG_8(arg, ...) ARG_1(arg) ATTR(ARG_7(__VA_ARGS__))  
  10. #define ARG_9(arg, ...) ARG_1(arg) ATTR(ARG_8(__VA_ARGS__))  
  11. #define ARG_10(arg, ...) ARG_1(arg) ATTR(ARG_9(__VA_ARGS__))  

但是这样的宏有个缺点就是在使用时必须明确地指定调用有几个参数的版本,不过有了前面实现的获取参数个数的宏,可以借用这个宏来自动选择哪个版本的参数遍历宏:

[cpp] view plain copy
  1. #define ARGS(...) \  
  2.     ATTR(SYMBOL_CATENATE_WITH_MACRO(ARG_, ATTR(COUNT_PARMS(__VA_ARGS__)))(__VA_ARGS__))  

2、跨平台程序开发

一些编译器提供的平台相关的预定义宏,可以很方便的用来做跨平台开发,例如:

[cpp] view plain copy
  1. #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)  
  2.   
  3. // windows  
  4.   
  5. #elif defined(__linux__) || defined(__linux)  
  6.   
  7. // linux  
  8.   
  9. #endif  
更多参考:点击打开链接点击打开链接

3、利用预定义宏调试程序

__FILE__、__LINE__、__FUNCTION__等可以很方便的获取程序相关的信息,当程序出现错误时,利用这些宏可以及时地生成错误信息并输出到日志中,以便查看和调试。

4、调试宏定义

宏的缺点之一就是难以调试,一旦宏体的定义出现问题导致编译错误,编译器将报一些令人费解的错误。不过对于宏定义导致的编译错误,还是有一些方法调试的:

(1)、查看宏展开后的完整代码

VC下可以利用"生成预处理文件"选项,宏展开后的代码将输出到.i文件中,操作参考:点击打开链接

GCC下使用编译选项-E即可。

5、宏元编程

参考:点击打开链接

原创粉丝点击