三白草

注册

 

发新话题 回复该主题

为宏正名什么我忘了去上ldquo [复制链接]

1#
北京治白癜风最好医院 https://jbk.39.net/yiyuanzaixian/bjzkbdfyy/
在前面的文章《本应写入教科书的“世界设定”》中我们了解到:宏会在预编译阶段被“处理掉”——宏会被逐级展开、其最终代表的字符串会被替换到对应的文本文件中(只不过通常这个文本文件就是".c"文件)——它不仅活不到正式的编译(make)阶段,更无法对程序运行时刻的行为产生丝毫影响。简而言之,通过宏所确定的内容是在编译时刻就固化下来的。很多人都了解这一点,也很擅长使用宏的方式来固化一些常数,比如,教科书中最常见的一个例子是:

//!非闰年的情况下,一年中有多少秒#defineSEC_IN_A_YEAR(60ul*60ul*24ul*ul)staticuint32_ts_wTotalSecInAYear=SEC_IN_A_YEAR;

例子虽然简单,但立马引出了一个有趣的问题:宏展开后,make时编译器看到的究竟是上述常量表达式的计算结果:

staticuint32_ts_wTotalSecInAYear=ul;

还是原样的字符串替换呢?

staticuint32_ts_wTotalSecInAYear=(60ul*60ul*24ul*ul);

感兴趣的读者可以通过“-E”来研究一下:

SETPATH=C:\Keil_v5\ARM\ARMCLANG\Bin;armclang-xc-std=gnu11--target=arm-arm-none-eabi-mcpu=cortex-m4-E-o"preprocessed_main.c""main.c"

这里,命令行使用armclang(ArmCompiler6)对“main.c”进行预编译("-E"的结果),并将结果输出到一个名为“preprocessed_main.c”的文件中——而这一文件就是我们在后面文章中要经常观察的,比如,针对前面的例子,一个可能的输出结果是:

#1"main.c"#1"built-in"1#1"built-in"3#"built-in"3#1"
  什么是参数宏的重载?——要回答这个问题,哪怕你连“重载(overload)”是什么都不知道也不要紧,我们来看一个最实际的例子:在前面的文章中,我们不止一次使用过一个胶水宏CONNECT3,它的作用是将三个字符串粘连在一起变成一个完整的字符串。如果我们要粘连的字符串数量不同,比如,2个、4个、5个……n个,我们就要编写对应的版本:

#define__CONNECT2(__0,__1)__0##__1#define__CONNECT3(__0,__1,__2)__0##__1##__2#define__CONNECT4(__0,__1,__2,__3)__0##__1##__2##__3...#define__CONNECT8(__0,__1,__2,__3,__4,__5,__6,__7)\__0##__1##__2##__3##__4##__5##__6##__7#define__CONNECT9(__0,__1,__2,__3,__4,__5,__6,__7,__8)\__0##__1##__2##__3##__4##__5##__6##__7##__8//!安全“套”#defineCONNECT2(__0,__1)__CONNECT2(__0,__1)#defineCONNECT3(__0,__1,__2)__CONNECT3(__0,__1,__2)#defineCONNECT4(__0,__1,__2,__3)__CONNECT4(__0,__1,__2,__3)...#defineCONNECT8(__0,__1,__2,__3,__4,__5,__6,__7)\__CONNECT8(__0,__1,__2,__3,__4,__5,__6,__7)#defineCONNECT9(__0,__1,__2,__3,__4,__5,__6,__7,__8)\__CONNECT9(__0,__1,__2,__3,__4,__5,__6,__7,__8)

这里定义了最大连接9个的CONNECT版本,看似麻烦,实际上复制粘贴、一劳永逸——还是挺划算的——当然,如果你比较“耿直”,还可以做得更多,比如16个。所谓宏的重载是说:我们不必亲自去数要粘贴的字符串的数量而“手工选取正确的版本”,而直接让编译器自己替我们挑选。

比如,我们举一个组装16进制数字的例子:

#defineHEX_U8_VALUE(__B1,__B0)\CONNECT3(0x,__B1,__B0)#defineHEX_U16_VALUE(__B3,__B2,__B1,__B0)\CONNECT5(0x,__B3,__B2,__B1,__B0)#defineHEX_U32_VALUE(__B7,__B6,__B4,__B4,__B3,__B2,__B1,__B0)\CONNECT9(0x,__B7,__B6,__B4,__B4,__B3,__B2,__B1,__B0)

在支持重载的情况下,我们希望这样使用:

#defineHEX_U8_VALUE(__B1,__B0)\CONNECT(0x,__B1,__B0)#defineHEX_U16_VALUE(__B3,__B2,__B1,__B0)\CONNECT(0x,__B3,__B2,__B1,__B0)#defineHEX_U32_VALUE(__B7,__B6,__B4,__B4,__B3,__B2,__B1,__B0)\CONNECT(0x,__B7,__B6,__B4,__B4,__B3,__B2,__B1,__B0)

如你所见,无论实际给出的参数是多少个,我们都可以使用同一个参数宏CONNECT(),而CONNCT()会自动计算用户给出参数的个数,从而正确的替换为CONNETn()版本。假设这一切都是可能做到的,那么实际上我们还可以对上述宏定义进行简化:

#defineHEX_VALUE(...)CONNECT(0x,__VA_ARGS__)#defineHEX_U8_VALUE(__B1,__B0)\HEX_VALUE(__B1,__B0)#defineHEX_U16_VALUE(__B3,__B2,__B1,__B0)\HEX_VALUE(__B3,__B2,__B1,__B0)#defineHEX_U32_VALUE(__B7,__B6,__B4,__B4,__B3,__B2,__B1,__B0)\HEX_VALUE(__B7,__B6,__B4,__B4,__B3,__B2,__B1,__B0)

是的,一个HEX_VALUE()就足够了,你随便添几个参数都行(只要小于等于你实现的CONNECTn的数量)。

既然前景如此诱人,怎么实现宏的重载呢?为了简化这个问题,我们假设有一个“魔法宏”:它可以告诉我们用户实际传递了多少个参数,我们不妨叫它VA_NUM_ARGS():

#defineVA_NUM_ARGS(...)/*这里暂时先不管怎么实现*/借助它,我们可以这样来编写宏CONNECT()p>#defineCONNECT(...)\CONNECT2(CONNECT,VA_NUM_ARGS(__VA_ARGS__))/*part1*/\(__VA_ARGS__)/*part2*/当用户使用CONNECT()时,VA_NUM_ARGS(__VA_ARGS__)会给出参数的数量;"part1"中CONNECT2()的作用就是将字符串“CONNCET”与这个数组组合起来变成一个新的“参数宏的名字”;而"part2"的作用则是给这个组装出来的参数宏传递参数。如果你觉得头晕了,我们不妨来举一个例子:假设用户想用HEX_VALUE()组装一个数字

uint16_thwValue=HEX_VALUE(D,E,A,D);//!0xDEAD它会被首先展开为:

uint16_thwValue=CONNECT(0x,D,E,A,D);进而

uint16_thwValue=CONNECT2(CONNECT,VA_NUM_ARGS(0x,D,E,A,D))(0x,D,E,A,D);由于VA_NUM_ARGS()告诉我们有5个参数,最终实际展开为:

uint16_thwValue=CONNECT5(0x,D,E,A,D);完美!那么我们就来逆推这个问题:如何实现我们的魔法宏“VA_NUM_ARGS()”呢?答案如下:

#defineVA_NUM_ARGS_IMPL(_1,_2,_3,_4,_5,_6,_7,_8,_9,__N,...)__N#defineVA_NUM_ARGS(...)\VA_NUM_ARGS_IMPL(__VA_ARGS__,9,8,7,6,5,4,3,2,1)这里,首先构造了一个特殊的参数宏,VA_NUM_ARGS_IMPL():

在涉及"..."之前,它要用用户至少传递10个参数;

这个宏的返回值就是第十个参数的内容;

多出来的部分会被"..."吸收掉,不会产生任何后果

VA_NUM_ARGS()的巧妙在于,它把__VA_ARGS__放在了参数列表的最前面,并随后传递了"9,8,7,6,5,4,3,2,1"这样的序号:

当__VA_ARGS__里有1个参数时,“1”对应第十个参数__N,所以返回值是1当__VA_ARGS__里有2个参数时,“2”对应第十个参数__N,所以返回值是2...当__VA_ARGS__里有9个参数时,"9"对应第十个参数__N,所以返回值是9

如果觉得上述过程似懂非懂,我们不妨对前面的例子做一个展开:

VA_NUM_ARGS(0x,D,E,A,D)展开为:

VA_NUM_ARGS_IMPL(0x,D,E,A,D,9,8,7,6,5,4,3,2,1)从左往右数,第十个参数,正好是“5”。

宏的重载非常有用,可以极大的简化用户"选择困难",你甚至可以将VA_NUM_ARGS()与函数名结合在一起,从而实现简单的函数重载(即,函数参数不同的时候,可以通过这种方法在编译阶段有预编译器根据用户输入参数的数量自动选择对应的函数),比如:

externdevice_write1(constchar*pchString);externdevice_write2(uint8_t*pchStream,uint_fast16_thwLength);externdevice_write3(uint_fast32_twAddress,uint8_t*pchStream,uint_fast16_thwLength);#definedevice_write(...)\CONNECT2(device_write,VA_NUM_ARGS(__VA_ARGS__))\(__VA_ARGS__)

使用时:

device_write("helloworld");//!发送字符串externuint8_tchBuffer[32];device_write(chBuffer,32);//!发送缓冲//!向指定偏移量写数据#defineLCD_DISP_MEM_START0xxxxxexternuint16_thwDisplayBuffer

  • ;device_write(LCD_DISP_MEM_START,(uint8_t*)hwDisplayBuffer,sizeof(hwDisplayBuffer));

    往期推荐

    1、深度好文

    面试官:进程和线程,我只问这19个问题2、他来了,他来了,C++17新特性精华都在这了3、一文让你搞懂设计模式4、C++11新特性,所有知识点都在这了!

    如果喜欢这篇文章,请点赞、在看,支持一下哦~谢谢!

    预览时标签不可点收录于话题#个上一篇下一篇
  • 分享 转发
    TOP
    发新话题 回复该主题