宏定义是什么
进入这里说明已经对宏定义的用途有所了解,顾名思义就是给某一个项东西重新定义一个名字。然后在我们在使用这项东西的时候可以用新定义的名字来替换。
为什么使用宏定义
我直接用原来的东西不就可以了?举个简单的例子。在一个数学计算的程序中,我们可能很多处用到一个圆周率,我们可以写成3.14。有一天程序因需求要把圆周率精确到小数点后四位也就是3.1416.如果有10处用到了圆周率,我们就需要改10处,那如果有100处?1000处呢?这时候就需要用到宏定义了,我们可以定义一个M_PI来代表圆周率,以后在程序中所有用到圆周率的地方我们都可以用M_PI来代替,当需要修改的时候,只需要修改一处M_PI替代的值就可以完成需求。是不是很方便呢?
宏定义可分为不带参数的宏和带参数的宏
以#define来开头
不带参数的宏
#define M_PI 3.14
#define+空格+自定义名称+空格+要替代的内容
类似这样的 #define X A 的宏是比较简单的,在编译时编译器会在语义分析认定是宏后,将X替换为A,这个过程称为宏的展开。
我们以后在用到圆周率的时候就可以用M_PI代替,这样的宏一般都很简单,现在来看一下另外一种宏--带参数的宏
带参数的宏
顾名思义,就是行为类似函数,可以接受参数的宏。具体来说,在定义的时候,如果我们在宏名字后面跟上一对括号的话,这个宏就变成了函数宏。从最简单的例子开始,比如下面这个函数宏
#define add(A, B) A + B
在程序运行的过程中如果看到 add字样并且后面带括号,并且括号里面的参数与宏定义中的个数相符,那么系统会把相应的参数带入宏定义的参数,然后替换后面的内容,比如程序中有一个 add(3, 4) 的代码,系统会自动把3当做A,把4当做B,然后再替换成 3 + 4
这就是带参的宏的一个简单实用,好了宏定义已经学会了,是不是很简单呢? 是的!就这么简单,但是我们要牢记宏定义这个原理,还以add(3, 4)为题,宏在程序运行的过程中是宏展开!把add(3, 4)替换成了3 + 4而已,而不是像函数一样直接返回一个结果7, 这样我们就不得不考虑它的安全性,什么意思呢?以一个简单的小例子来说下吧。
宏的安全性
练习我们就先来定义一个2个数的最小宏吧!聪明的你一定很快就写出来了
#define MIN(A, B) A > B ? B : A
最小值宏定义V1.0发布~ 我们来使用一下
printf("%d\n", MIN(3, 4)); 运行成功打印了结果 3
简直是太棒了
但是用过一段时间后我们很快就发现了问题
什么?粗BUG了?怎么回事?
原来你的同事在程序中这样用到了你的宏
问题1:
printf("%d\n", 3 * MIN(3, 4));
我们的本意用3乘以( 3和4中较小的一个数) 即3*3
打印结果, 什么?结果只是4?怎么会这样? 难道...难道...
于是我们会想宏定义的特点,哦,他只是简单的替换
那么我们就可以理解到编译器遇到 3 * MIN(3, 4) 会把宏简单的替换成 3 * 3 > 4 ? 4 : 3 是不是简单的替换?
这样我们就明白了其中的原理,3 * MIN(3, 4) 等价于 9 > 4 ? 4 : 3我们自然而然的就得到了4
怎么解决这个问题?简单!只需要加一个括号就行了
#define MIN(A, B) (A > B ? B : A)
最小值宏定义V2.0发布~ 这下就没问题了吧
很快你的同事又找到了你告诉你,你的宏出问题了
怎么回事???!!!
来到他电脑桌前看到了这么一行代码
问题2:
printf("%d\n", MIN(3, 4 > 5 ? 4 : 5));
我去,这小子想干嘛?一问之下才知道,原来他想比较4和5中比较大的一个数,然后在跟3比较哪一个小,又因为你没有给人家定义最大值的宏,所以只好自己写,运行之后得到了个5,怎么会这样?
老办法,展开宏定义吧 MIN(3, 4 > 5 ? 4 : 5) 展开后 (3 > 4 > 5 ? 4 : 5 ? 4 > 5 ? 4 : 5 : 3)
天啊!这是什么,经过一番梳理我们可以得到了结果 5
这和预想的完全不一样,可以看得出当一个算式套进去的时候优先级除了问题老方法,接着加括号吧
我们就老老实实的发布了最小值宏定义V3.0
#define MIN(A, B) ((A) > (B) ? (B) : (A))
这下终于得到了预期效果,从此以后这个“安全”的宏定义 再也没给你找过麻烦,直到有一天.......
同事:这个宏定义不行啊,到现在还是错误的
怎么可能!! 让我看下怎么用的
问题3:
int a = 3;
int b = 4;
printf("%d", MIN(++a, ++b));
得到的结果怎么又是5
展开后我们发现是这个样子的 ((++a) > (++b) ? (++b) : (++a))
原来在这个表达式里面运行了2次 这还怎么办啊
这时候就需要用到GNU C的赋值扩展,即使({...})的形式, 这种形式的语句可以类似很多脚本语言,在顺次执行之后,会将最后一次的表达式的赋值作为返回举个简单地小例子
int c = ({
int a = 5;
int b = 6;
b + a;
});
我们可以看到整个({..})里面最后一个表达式为 b+a 那么 b+a 就是这个块里整体的返回值,最后我们用c来接收了这个值, 那么c就是11.
那么我们就可以在宏定义的时候用这种形式,来返回最小是, 同时在这个块里面我们可以新定义变量来获取++a,++b的值,来保证他们只被执行一次
我们就可以这么写
#define MIN(A, B) ({__typeof__(A) __a = (A); __typeof__(B) __b = (B); __a > __b ? __b : __a;})
哇,这么一大坨是什么东西?我们来拆分下
#define MIN(A, B) ({\
__typeof__(A) __a = (A);\
__typeof__(B) __b = (B);\
__a > __b ? __b : __a;\
}) (宏定义的时候是不可以直接换行的,我们可以在回车前面加 “\” 这个符号来让其换行)
这样是不是就清楚一些呢 __typeof__(A) 意思是取的A的类型。以MIN(++a, ++b)来说,因为我们要重新定义变量来接收a的值, 防止++a多次运行,那么定义变量之前我们要先知道这个a的类型,他可能是int 也可能是 float, 我们不会提前知道,所以用__typeof__(A)来取得他的类型,__typeof__()可以理解为取括号里面的变量的类型
那么就可以知道第一句 大致意思为 int __a = ++a; 同理 int __b = ++b; 最后比较新定义的变量__a > __b ? __b : __a; 同时它也是最后一个表达式,值会返回。这样就定义了一个天衣无缝的最小值宏定义了
最小值宏定义V4.0
#define MIN(A, B) ({__typeof__(A) __a = (A); __typeof__(B) __b = (B); __a > __b ? __b : __a;})
真的天衣无缝了么 我们可以先来看一下苹果的官方定义
#define __NSX_PASTE__(A,B) A##B
#if !defined(MIN)
#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); \
})
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
#endif
简单来说这个是为了解决我4.0中在({...})中可能会出现两个__a,导致重定义变量而引发未知错误,首先__NSX_PASTE__(A,B)是用来链接A B两个符号的,在__NSMIN_IMPL__又在定义变量名的时候用了__NSX_PASTE__(A,B)我们可以看到调用宏__NSX_PASTE__(__a,L)的时候传的参数用的时L 而L又来自__NSMIN_IMPL__(A,B,__COUNTER__)的__COUNTER__
,__COUNTER__系统系自动帮我们生成不重复的数字,1,2,3,4....依次生成这样在块中我们声明的变量就是变成 __a1, 再有的话就会生成__a2。避免了重命名的风险。
看明白了上面,对于宏定义应该已经入门了 入门? 是的,其实宏定义本身很简单,就是宏展开,完全替换掉我们定义的宏名,难点在于我们怎么预防宏展开后所引发的各种未知错误。正常的情况下多加些括号就完全可以搞定了
----------尾言:
写了一上午,终于写完了。有错误的地方欢迎指正
思考:真的多加括号就对了么?