C/C++ 宏详解

众多c++书籍都忠告我们c语言宏是万恶之首,但事情总不如我们想象的那么坏,就如同goto一样。宏有一个很大的作用,就是自动为我们产生代码。如果说模板
众多c++书籍都忠告我们c语言宏是万恶之首,但事情总不如我们想象的那么坏,就如同goto一样。宏有
一个很大的作用,就是自动为我们产生代码。如果说模板可以为我们产生各种型别的代码(型别替换),
那么宏其实可以为我们在符号上产生新的代码(即符号替换、增加)。
关于宏的一些语法问题,可以在google上找到。相信我,你对于宏的了解绝对没你想象的那么多。如果你
还不知道#和##,也不知道prescan,那么你肯定对宏的了解不够。
我稍微讲解下宏的一些语法问题(说语法问题似乎不妥,macro只与preprocessor有关,跟语义分析又无关):
1. 宏可以像函数一样被定义,例如:
#define min(x,y) (x 但是在实际使用时,只有当写上min(),必须加括号,min才会被作为宏展开,否则不做任何处理。
2. 如果宏需要参数,你可以不传,编译器会给你警告(宏参数不够),但是这会导致错误。如c++书籍中所描
述的,编译器(预处理器)对宏的语法检查不够,所以更多的检查性工作得你自己来做。
3. 很多程序员不知道的#和##
#符号把一个符号直接转换为字符串,例如:
#define string(x) #x
const char *str = string( test_string ); str的内容就是test_string,也就是说#会把其后的符号
直接加上双引号。
##符号会连接两个符号,从而产生新的符号(词法层次),例如:
#define sign( x ) int_##x
int sign( 1 ); 宏被展开后将成为:int int_1;
4. 变参宏,这个比较酷,它使得你可以定义类似的宏:
#define log( format, ... ) printf( format, __va_args__ )
log( %s %d, str, count );
__va_args__是系统预定义宏,被自动替换为参数列表。
5. 当一个宏自己调用自己时,会发生什么?例如:
#define test( x ) ( x + test( x ) )
test( 1 ); 会发生什么?为了防止无限制递归展开,语法规定,当一个宏遇到自己时,就停止展开,也就是
说,当对test( 1 )进行展开时,展开过程中又发现了一个test,那么就将这个test当作一般的符号。test(1)
最终被展开为:1 + test( 1) 。
6. 宏参数的prescan,
当一个宏参数被放进宏体时,这个宏参数会首先被全部展开(有例外,见下文)。当展开后的宏参数被放进宏体时,
预处理器对新展开的宏体进行第二次扫描,并继续展开。例如:
#define param( x ) x
#define addparam( x ) int_##x
param( addparam( 1 ) );
因为addparam( 1 ) 是作为param的宏参数,所以先将addparam( 1 )展开为int_1,然后再将int_1放进param。
例外情况是,如果param宏里对宏参数使用了#或##,那么宏参数不会被展开:
#define param( x ) #x
#define addparam( x ) int_##x
param( addparam( 1 ) ); 将被展开为addparam( 1 )。
使用这么一个规则,可以创建一个很有趣的技术:打印出一个宏被展开后的样子,这样可以方便你分析代码:
#define to_string( x ) to_string1( x )
#define to_string1( x ) #x
to_string首先会将x全部展开(如果x也是一个宏的话),然后再传给to_string1转换为字符串,现在你可以这样:
const char *str = to_string( param( addparam( 1 ) ) );去一探param展开后的样子。
7. 一个很重要的补充:就像我在第一点说的那样,如果一个像函数的宏在使用时没有出现括号,那么预处理器只是
将这个宏作为一般的符号处理(那就是不处理)。
我们来见识一下宏是如何帮助我们自动产生代码的。如我所说,宏是在符号层次产生代码。我在分析boost.
模块时,因为它使用了大量的宏(宏嵌套,再嵌套),导致我压根没看明白代码。后来发现了一个小型的模板库ttl,说的
是开发一些小型组件去取代部分boost(这是一个好理由,因为boost确实太大)。同样,这个库也包含了一个库。
这里的也就是我之前提到的functor。ttl.库里为了自动产生很多类似的代码,使用了一个宏:
#define ttl_func_build_functor_caller(n) /
template /
struct functor_caller_base##n /
///...
该宏的最终目的是:通过类似于ttl_func_build_functor_caller(1)的调用方式,自动产生很多functor_caller_base模板:
template struct functor_caller_base1
template struct functor_caller_base2
template struct functor_caller_base3
///...
那么,核心部分在于ttl_tparams(n)这个宏,可以看出这个宏最终产生的是:
typename t1
typename t1, typename t2
typename t1, typename t2, typename t3
///...
我们不妨分析ttl_tparams(n)的整个过程。分析宏主要把握我以上提到的一些要点即可。以下过程我建议你翻着ttl的代码,
相关代码文件:.hpp, macro_params.hpp, macro_repeat.hpp, macro_misc.hpp, macro_counter.hpp。
so, here we go
分析过程,逐层分析,逐层展开,例如ttl_tparams(1):
#define ttl_tparams(n) ttl_tparamsx(n,t)
=> ttl_tparamsx( 1, t )
#define ttl_tparamsx(n,t) ttl_repeat(n, ttl_tparam, ttl_tparam_end, t)
=> ttl_repeat( 1, ttl_tparam, ttl_tparam_end, t )
#define ttl_tparam(n,t) typename t##n,
#define ttl_tparam_end(n,t) typename t##n
#define ttl_repeat(n, m, l, p) ttl_append(ttl_repeat_, ttl_dec(n))(m,l,p) ttl_append(ttl_last_repeat_,n)(l,p)
注意,ttl_tparam, ttl_tparam_end虽然也是两个宏,他们被作为ttl_repeat宏的参数,按照prescan规则,似乎应该先将
这两个宏展开再传给ttl_repeat。但是,如同我在前面重点提到的,这两个宏是-like macro,使用时需要加括号,
如果没加括号,则不当作宏处理。因此,展开ttl_repeat时,应该为:
=> ttl_append( ttl_repeat_, ttl_dec(1))(ttl_tparam,ttl_tparam_end,t) ttl_append( ttl_last_repeat_,1)(
ttl_tparam_end,t)
这个宏体看起来很复杂,仔细分析下,可以分为两部分:
ttl_append( ttl_repeat_, ttl_dec(1))(ttl_tparam,ttl_tparam_end,t)以及
ttl_append( ttl_last_repeat_,1)(ttl_tparam_end,t)
先分析第一部分:
#define ttl_append( x, y ) ttl_append1(x,y) //先展开x,y再将x,y连接起来
#define ttl_append1( x, y ) x ## y
#define ttl_dec(n) ttl_append(ttl_cntdec_, n)
根据先展开参数的原则,会先展开ttl_dec(1)
=> ttl_append(ttl_cntdec_,1) => ttl_cntdec_1
#define ttl_cntdec_1 0 注意,ttl_cntdec_不是宏,ttl_cntdec_1是一个宏。
=> 0 , 也就是说,ttl_dec(1)最终被展开为0。回到ttl_append部分:
=> ttl_repeat_0 (ttl_tparam,ttl_tparam_end,t)
#define ttl_repeat_0(m,l,p)
ttl_repeat_0这个宏为空,那么,上面说的第一部分被忽略,现在只剩下第二部分:
ttl_append( ttl_last_repeat_,1)(ttl_tparam_end,t)
=> ttl_last_repeat_1 (ttl_tparam_end,t) // ttl_append将ttl_last_repeat_和1合并起来
#define ttl_last_repeat_1(m,p) m(1,p)
=> ttl_tparam_end( 1, t )
#define ttl_tparam_end(n,t) typename t##n
=> typename t1 展开完毕。
虽然我们分析出来了,但是这其实并不是我们想要的。我们应该从那些宏里去获取作者关于宏的编程思想。很好地使用宏
看上去似乎是一些偏门的奇技淫巧,但是他确实可以让我们编码更自动化。

区块链能否解决拜占庭将军的问题
通过MDIO接口管理PHY芯片的验证设计方案
2018年VR/AR行业数据报告,VR/AR在各自的领域开疆扩土
【曦哥论币】币圈投资如何从根摆脱亏损!原来关键在这里!
三星环绕全面屏的滑盖智能手机概念图
C/C++ 宏详解
新闻 | 采用SOT-223-3小型封装的600V耐压Super Junction MOSFET
测土施肥仪器的使用方法是怎样的
直线电机磁悬浮列车亮相国内外优佳工业设计成果展
博信视通发布CMMB信号源全系列产品,助力CMMB终端产业发
中国“5G车联网”大动作!如此精彩内容!您舍得错过?
HID Global®正式宣布推出HID® FARGO® INK1000打印机
氢氧燃料电池的使用范围及存在问题
电子系统中电源如何搞定EMI电磁干扰
一文带你浅析谐波
Telefonica与华为携手 验证URLLC可支持5G-V2X
这家企业,在打造5G高频柔性覆铜板生产线领域标杆
C8051F023设计的液晶触摸屏技术及应用
苹果和谷歌这两大死对头竟然破天荒地走在一起
Intersil推出ISL8270M/71M系列数字电源模块