宏定义在C系开发中可以说占有举足轻重的作用。底层框架自不必说,为了编译优化和方便,以及跨平台能力,宏被大量使用,可以说底层开发离开define将寸步难行。而在更高层级进行开发时,我们会将更多的重心放在业务逻辑上,似乎对宏的使用和依赖并不多。但是使用宏定义的好处是不言自明的,在节省工作量的同时,代码可读性大大增加。如果想成为一个能写出漂亮优雅代码的开发者,宏定义绝对是必不可少的技能(虽然宏本身可能并不漂亮优雅XD)。但是因为宏定义对于很多人来说,并不像业务逻辑那样是每天会接触的东西。即使是能偶尔使用到一些宏,也更多的仅仅只停留在使用的层级,却并不会去探寻背后发生的事情。有一些开发者确实也有探寻的动力和意愿,但却在点开一个定义之后发现还有宏定义中还有其他无数定义,再加上满屏幕都是不同于平时的代码,既看不懂又不变色,于是乎心生烦恼,怒而回退。本文希望通过循序渐进的方式,通过几个例子来表述C系语言宏定义世界中的一些基本规则和技巧,从0开始,希望最后能让大家至少能看懂和还原一些相对复杂的宏。考虑到我自己现在objc使用的比较多,这个站点的读者应该也大多是使用objc的,所以有部分例子是选自objc,但是本文的大部分内容将是C系语言通用。

入门

如果您完全不知道宏是什么的话,可以先来热个身。很多人在介绍宏的时候会说,宏嘛很简单,就是简单的查找替换嘛。嗯,只说对了的一半。C中的宏分为两类,对象宏(object-like macro)和函数宏(function-like macro)。对于对象宏来说确实相对简单,但却也不是那么简单的查找替换。对象宏一般用来定义一些常数,举个例子:

//This defines PI
#define M_PI        3.14159265358979323846264338327950288
#define`关键字表明即将开始定义一个宏,紧接着的`M_PI`是宏的名字,空格之后的数字是内容。类似这样的`#define X A`的宏是比较简单的,在编译时编译器会在语义分析认定是宏后,将X替换为A,这个过程称为宏的展开。比如对于上面的`M_PI
#define M_PI        3.14159265358979323846264338327950288double r = 10.0;
double circlePerimeter = 2 * M_PI * r;
// => double circlePerimeter = 2 * 3.14159265358979323846264338327950288 * r;printf("Pi is %0.7f",M_PI);
//Pi is 3.1415927

那么让我们开始看看另一类宏吧。函数宏顾名思义,就是行为类似函数,可以接受参数的宏。具体来说,在定义的时候,如果我们在宏名字后面跟上一对括号的话,这个宏就变成了函数宏。从最简单的例子开始,比如下面这个函数宏

//A simple function-like macro
#define SELF(x)      x
NSString *name = @"Macro Rookie";
NSLog(@"Hello %@",SELF(name));
// => NSLog(@"Hello %@",name);
//   => Hello Macro Rookie

这个宏做的事情是,在编译时如果遇到SELF,并且后面带括号,并且括号中的参数个数与定义的相符,那么就将括号中的参数换到定义的内容里去,然后替换掉原来的内容。 具体到这段代码中,SELF接受了一个name,然后将整个SELF(name)用name替换掉。嗯…似乎很简单很没用,身经百战阅码无数的你一定会认为这个宏是写出来卖萌的。那么接受多个参数的宏肯定也不在话下了,例如这样的:

#define PLUS(x,y) x + y
printf("%d",PLUS(3,2));
// => printf("%d",3 + 2);
//  => 5

相比对象宏来说,函数宏要复杂一些,但是看起来也相当简单吧?嗯,那么现在热身结束,让我们正式开启宏的大门吧。

宏的世界,小有乾坤

因为宏展开其实是编辑器的预处理,因此它可以在更高层级上控制程序源码本身和编译流程。而正是这个特点,赋予了宏很强大的功能和灵活度。但是凡事都有两面性,在获取灵活的背后,是以需要大量时间投入以对各种边界情况进行考虑来作为代价的。可能这么说并不是很能让人理解,但是大部分宏(特别是函数宏)背后都有一些自己的故事,挖掘这些故事和设计的思想会是一件很有意思的事情。另外,我一直相信在实践中学习才是真正掌握知识的唯一途径,虽然可能正在看这篇博文的您可能最初并不是打算亲自动手写一些宏,但是这我们不妨开始动手从实际的书写和犯错中进行学习和挖掘,因为只有肌肉记忆和大脑记忆协同起来,才能说达到掌握的水准。可以说,写宏和用宏的过程,一定是在在犯错中学习和深入思考的过程,我们接下来要做的,就是重现这一系列过程从而提高进步。

第一个题目是,让我们一起来实现一个MIN宏吧:实现一个函数宏,给定两个数字输入,将其替换为较小的那个数。比如MIN(1,2)出来的值是1。嗯哼,simple enough?定义宏,写好名字,两个输入,然后换成比较取值。比较取值嘛,任何一本入门级别的C程序设计上都会有讲啊,于是我们可以很快写出我们的第一个版本:

//Version 1.0
#define MIN(A,B) A < B ? A : B

Try一下

int a = MIN(1,2);
// => int a = 1 < 2 ? 1 : 2;
printf("%d",a);
// => 1

输出正确,打包发布!

但是在实际使用中,我们很快就遇到了这样的情况

int a = 2 * MIN(3, 4);
printf("%d",a);
// => 4

看起来似乎不可思议,但是我们将宏展开就知道发生什么了

int a = 2 * MIN(3, 4);
// => int a = 2 * 3 < 4 ? 3 : 4;
// => int a = 6 < 4 ? 3 : 4;
// => int a = 4;

嘛,写程序这个东西,bug出来了,原因知道了,事后大家就都是诸葛亮了。因为小于和比较符号的优先级是较低的,所以乘法先被运算了,修正非常简单嘛,加括号就好了。

//Version 2.0
#define MIN(A,B) (A < B ? A : B)

这次2 * MIN(3, 4)这样的式子就轻松愉快地拿下了。经过了这次修改,我们对自己的宏信心大增了…直到,某一天一个怒气冲冲的同事跑来摔键盘,然后给出了一个这样的例子:

int a = MIN(3, 4 < 5 ? 4 : 5);
printf("%d",a);
// => 4

简单的相比较三个数字并找到最小的一个而已,要怪就怪你没有提供三个数字比大小的宏,可怜的同事只好自己实现4和5的比较。在你开始着手解决这个问题的时候,你首先想到的也许是既然都是求最小值,那写成MIN(3, MIN(4, 5))是不是也可以。于是你就随手这样一改,发现结果变成了3,正是你想要的…接下来,开始怀疑之前自己是不是看错结果了,改回原样,一个4赫然出现在屏幕上。你终于意识到事情并不是你想像中那样简单,于是还是回到最原始直接的手段,展开宏。

int a = MIN(3, 4 < 5 ? 4 : 5);
// => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5);  //希望你还记得运算符优先级
//  => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5);  //为了您不太纠结,我给这个式子加上了括号
//   => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
//    => int a = (3 < 5 ? 4 : 5)
//     => int a = 4

找到问题所在了,由于展开时连接符号和被展开式子中的运算符号优先级相同,导致了计算顺序发生了变化,实质上和我们的1.0版遇到的问题是差不多的,还是考虑不周。那么就再严格一点吧,3.0版!

//Version 3.0
#define MIN(A,B) ((A) < (B) ? (A) : (B))

至于为什么2.0版本中的MIN(3, MIN(4, 5))没有出问题,可以正确使用,这里作为练习,大家可以试着自己展开一下,来看看发生了什么。

经过两次悲剧,你现在对这个简单的宏充满了疑惑。于是你跑了无数的测试用例而且它们都通过了,我们似乎彻底解决了括号问题,你也认为从此这个宏就妥妥儿的哦了。不过如果你真的这么想,那你就图样图森破了。生活总是残酷的,该来的bug也一定是会来的。不出意外地,在一个雾霾阴沉的下午,我们又收到了一个出问题的例子。

float a = 1.0f;
float b = MIN(a++, 1.5f);
printf("a=%f, b=%f",a,b);
// => a=3.000000, b=2.000000

拿到这个出问题的例子你的第一反应可能和我一样,这TM的谁这么二货还在比较的时候搞++,这简直乱套了!但是这样的人就是会存在,这样的事就是会发生,你也不能说人家逻辑有错误。a是1,a++表示先使用a的值进行计算,然后再加1。那么其实这个式子想要计算的是取a和b的最小值,然后a等于a加1:所以正确的输出a为2,b为1才对!嘛,满眼都是泪,让我们这些久经摧残的程序员淡定地展开这个式子,来看看这次又发生了些什么吧:

float a = 1.0f;
float b = MIN(a++, 1.5f);
// => float b = ((a++) < (1.5f) ? (a++) : (1.5f))

其实只要展开一步就很明白了,在比较a++和1.5f的时候,先取1和1.5比较,然后a自增1。接下来条件比较得到真以后又触发了一次a++,此时a已经是2,于是b得到2,最后a再次自增后值为3。出错的根源就在于我们预想的是a++只执行一次,但是由于宏展开导致了a++被多执行了,改变了预想的逻辑。解决这个问题并不是一件很简单的事情,使用的方式也很巧妙。我们需要用到一个GNU C的赋值扩展,即使用({...})的形式。这种形式的语句可以类似很多脚本语言,在顺次执行之后,会将最后一次的表达式的赋值作为返回。举个简单的例子,下面的代码执行完毕后a的值为3,而且b和c只存在于大括号限定的代码域中

int a = ({int b = 1;int c = 2;b + c;
});
// => a is 3

有了这个扩展,我们就能做到之前很多做不到的事情了。比如彻底解决MIN宏定义的问题,而也正是GNU C中MIN的标准写法

//GNUC MIN
#define MIN(A,B)    ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })

这里定义了三个语句,分别以输入的类型申明了__a__b,并使用输入为其赋值,接下来做一个简单的条件比较,得到__a__b中的较小值,并使用赋值扩展将结果作为返回。这样的实现保证了不改变原来的逻辑,先进行一次赋值,也避免了括号优先级的问题,可以说是一个比较好的解决方案了。如果编译环境支持GNU C的这个扩展,那么毫无疑问我们应该采用这种方式来书写我们的MIN宏,如果不支持这个环境扩展,那我们只有人为地规定参数不带运算或者函数调用,以避免出错。

关于MIN我们讨论已经够多了,但是其实还存留一个悬疑的地方。如果在同一个scope内已经有__a或者__b的定义的话(虽然一般来说不会出现这种悲剧的命名,不过谁知道呢),这个宏可能出现问题。在申明后赋值将因为定义重复而无法被初始化,导致宏的行为不可预知。如果您有兴趣,不妨自己动手试试看结果会是什么。Apple在Clang中彻底解决了这个问题,我们把Xcode打开随便建一个新工程,在代码中输入MIN(1,1),然后Cmd+点击即可找到clang中 MIN的写法。为了方便说明,我直接把相关的部分抄录如下:

//CLANG MIN
#define __NSX_PASTE__(A,B) A##B#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)#define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })

似乎有点长,看起来也很吃力。我们先美化一下这宏,首先是最后那个__NSMIN_IMPL__内容实在是太长了。我们知道代码的话是可以插入换行而不影响含义的,宏是否也可以呢?答案是肯定的,只不过我们不能使用一个单一的回车来完成,而必须在回车前加上一个反斜杠\。改写一下,为其加上换行好看些:

#define __NSX_PASTE__(A,B) A##B#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)#define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); \__typeof__(B) __NSX_PASTE__(__b,L) = (B); \(__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); \})

但可以看出MIN一共由三个宏定义组合而成。第一个__NSX_PASTE__里出现的两个连着的井号##在宏中是一个特殊符号,它表示将两个参数连接起来这种运算。注意函数宏必须是有意义的运算,因此你不能直接写AB来连接两个参数,而需要写成例子中的A##B。宏中还有一切其他的自成一脉的运算符号,我们稍后还会介绍几个。接下来是我们调用的两个参数的MIN,它做的事是调用了另一个三个参数的宏__NSMIN_IMPL__,其中前两个参数就是我们的输入,而第三个__COUNTER__我们似乎不认识,也不知道其从何而来。其实__COUNTER__是一个预定义的宏,这个值在编译过程中将从0开始计数,每次被调用时加1。因为唯一性,所以很多时候被用来构造独立的变量名称。有了上面的基础,再来看最后的实现宏就很简单了。整体思路和前面的实现和之前的GNUC MIN是一样的,区别在于为变量名__a__b添加了一个计数后缀,这样大大避免了变量名相同而导致问题的可能性(当然如果你执拗地把变量叫做__a9527并且出问题了的话,就只能说不作死就不会死了)。

花了好多功夫,我们终于把一个简单的MIN宏彻底搞清楚了。宏就是这样一类东西,简单的表面之下隐藏了很多玄机,可谓小有乾坤。作为练习大家可以自己尝试一下实现一个SQUARE(A),给一个数字输入,输出它的平方的宏。虽然一般这个计算现在都是用inline来做了,但是通过和MIN类似的思路我们是可以很好地实现它的,动手试一试吧

【转自猫大】宏定义的黑魔法 - 宏菜鸟起飞手册相关推荐

  1. c语言宏定义(c语言宏定义是什么意思)

    C语言宏定义名可以有括号和分号吗? 宏定义的名称,是C语言标识符的一种,和函数名,变量名的命名规则是一样的,只允许使用数字,字母,下划线,且不能以数字开头. 所以宏名是不可以有括号和分号的. 另外,在 ...

  2. VC宏定义 及常用宏定义说明

    1. 宏定义的格式 宏定义的一般格式是: #define  标识符  字符串 其中,标识符和字符串之间用空格隔开.标识符又称宏名,为了区别于一般变量,通常用英文大写字母表示:字符串又称宏体,可以是常 ...

  3. 【C语言】----宏定义,预处理宏

    什么是宏? 宏是学习任何语言所不可缺少的,优秀的宏定义可以使得代码变得很简洁且高效,有效地提高编程效率. 宏是一种预处理指令,它提供了一种机制,可以用来替换源代码中的字符串,解释器或编译器在遇到宏时会 ...

  4. 宏定义有无参数宏定义和带参数宏定义两种

    宏定义有无参数宏定义和带参数宏定义两种. 无参数的宏定义的一般形式为 # define 标识符 字符序列 其中# define之后的标识符称为宏定义名(简称宏名),要求宏名与字符序列之间用空格符分隔. ...

  5. c语言解除宏定义_3.3.5 取消宏定义和重新定义宏

    3.3.5   取消宏定义和重新定义宏 #undef命令可以取消定义一个名称为宏:#undef name 这个命令使预处理器忘记name的所有宏定义.取消一个当前未定义宏的定义并不是错误.当一个名称被 ...

  6. C语言宏定义-跟踪调试宏

    C语言宏定义-跟踪调试宏 文章目录 C语言宏定义-跟踪调试宏 1. 简介 2. 相关宏 3. 例子 1. 简介 标准C语言预处理要求定义某些对象宏,每个预定义宏的名称一两个下划线字符开头和结尾,这些预 ...

  7. C语言宏定义(常用宏定义)

    C语言常用宏定义 常用宏定义 数值相关的宏定义 字符相关的宏定义 byte相关的宏定义 bit相关的宏定义 数组与结构体相关的宏定义 对齐的宏定义 常用宏定义 数值相关的宏定义 闰年的判断 ,年份可以 ...

  8. c语言宏定义_C语言宏定义

    C语言的宏,是C的一大特点,宏定义可以用来定义常量,函数,为了全局替换起来方便. 比如: #define PIE 3.1415926 再比如: #define MAX(a,b) ( (a)>(b ...

  9. c语言解除宏定义_C语言宏定义 define,及一些陷阱!

    https://m.toutiaocdn.com/group/6584292311289561607/?iid=39362926900&app=news_article&timesta ...

最新文章

  1. 域名年龄-SEO搜索引擎优化
  2. 什么是图神经网络 (GNN)?
  3. 什么是java一句话一个例子_一句话讲清楚什么是JavaEE
  4. java配置解决方法_如何配置Java环境变量及可能出现问题的解决方法
  5. 周四话运营:如何提高用户留存?
  6. 使用.Net Core实现的一个图形验证码
  7. jzoj2700-数字【数论,LCM】
  8. sshclientCRT连接linux使用技巧
  9. 一种基于memcache或redis缓存架构的验证码
  10. elementui el-input输入数字为整数的验证
  11. 【数学建模】算法模型(三)|模糊综合和灰色关联分析 元胞自动机模型 图论 BP神经网络算法 马尔可夫链蒙特卡罗算法(MCMC)
  12. 后端开发技术栈(含视频、书籍推荐)
  13. Cortex-M3 (NXP LPC1788)之ADC模/数转换器
  14. 1stopt拟合步骤_1stopt三维曲线拟合
  15. excel冻结行和列_Spire.Cloud.Excel 冻结或解除冻结Excel中的行和列
  16. 新唐NUC980读取U盘配置
  17. 如何去掉字符串中第一个和最后一个字符 比如是;
  18. edvac是商用计算机吗,计算机基础知识78228
  19. MacBook连接蓝牙鼠标、蓝牙键盘失败的解决方案
  20. 推荐8个相见恨晚的学习类app,每一个都诚意满满

热门文章

  1. 招聘信息显示腾讯计划开发杀毒软件
  2. c/c++中保留两位有效数字
  3. Vertica的这些事(十六)——Vertica如何建表
  4. 台式计算机安装无线网卡,如何在台式计算机主机上安装无线网卡
  5. php 如何处理图片流,PHP 处理图片
  6. 原画师为什么多数不到30就不做了?
  7. ArcGIS教程:如何向地图中添加图层
  8. 教育部往届计算机应用基础,(开放教育本科计算机应用基础.doc
  9. 程序员去哪里找兼职?哪里可以接程序员私活?
  10. JavaScript函数式编程(一)\(二)\(三)