quot;); // 调用宏,相当于printf("Hello, world!\n");
PRINTF("The answer is %d\n", 42); // 调用宏,相当于printf("The answer is %d\n", 42);
注意什么
??随着我们宏定义的对象从简单的常量到相对复杂的整体,宏定义本身也从无参宏定义过渡到有参宏定义,但是由于宏定义仅仅是在程序预编译阶段暴力的直接展开,当我们写入带参宏定义的内容不只是一个简单数字而是一段表达式就有可能会出现歧义与错误。比如我们定义了一个计算平方的宏:
#define SQUARE(x) x * x
当使用该宏时,如果我们直接使用SQUARE (a + b)
,这个式子最后会被展开为a + b * a + b
而不是我们期望的(a + b) * (a + b)
,所以为了保证带参宏定义结果的正确性,我们应该像下面这样对被定义主体内的参数带上()
,以保证宏定义的正确结果
#define SQUARE(x) (x) * (x)
不过仅仅给内部的参数添加()
也并不能保证万事俱备,比如以下这个例子:
#define ADD(x) (x)+(x)
在使用该宏被用到ADD(x)*10
此式时,该式子会被展开为(x)+(x)*10
,而不是我们预想的((x)+(x))*10
,因此我们需要进一步的修改
#define ADD(x) ((x)+(x))
只有像上面这样,不仅给宏体中的每个参数和整个表达式都带上参数,才能保证计算次序的正确性,这两层括号也有另一个名字——宏定义完备括号。
替代方案
??经过前面这么多的叙述,有些小伙伴可能已经意识到了这里提出来的整体的概念不就是函数吗?其实开始我也准备这么理解,但是宏就是宏,函数就是函数,总不能看到宏的这类用法就把宏归纳到函数的范畴吧,我们需要一个更加抽象的认识来统一这类用法,于是我就用了整体这个概念。既然这块内容讲的是替换方案,那我们另一个主角都不需要隆重介绍了,他就是 —— 函数。这时候问题就来了,宏定义能完全代替函数吗?或者说函数能完全代替宏定义吗?宏与函数虽然在某些共同之处,但是在一些方面也存在差异。
- 函数的调用不同于宏定义,它需要出栈与入栈的确操作,这些额外的开销会降低程序的执行效率,宏定义则是直接执行,但是宏定义的每处展开都会多一份内存空间的申请,不像函数那样一个程序只占用一个代码块。
- 含参宏定义在使用时,我们并没有像函数的参数那样指定具体类型,这给我们编程者带来一定便利,不过有时候这种无类型参数会带来一定隐患。
- 由于函数名就是一个指针,而没有指向宏定义的指针,因此宏无法得到指针带来的便利。
??总之,函数与宏定义在作为整体出现在编程中时,各有其优势所在,在具体的编程环境中并没有什么最好之说,只有最适合的。
集合体
??当一个集合有了专一的功能,我们称之为整体,而在编程中有些部分集合由于不具备这种专一性并不能称之为整体,却由于其较高的重复度而不得不封装起来,我们将这类组合称为集合体。
#define ERROR(m) \
do{ \
perror(m); \
tfer(); \
}while(0)
以上代码是我写的某个项目的一段,在每次处理完错误后都有这么一段重复内容,但是这部分代码前那部分与错误处理相关的内容并不总是相同,因此不能作为一个整体来看待,我只需要对这部分内容进行复用。这个集合体是用do{}while
封装的,有些小伙伴可能觉得直接用{}
也不错,但是使用后者有时会因为疏忽出现问题。
我们在编程语句的结尾会习惯性的加上;
,但在使用if else
语句时如果遇上被{}
封装的宏定义问题就显现出来了,比如下面的例子:
#define ERROR(m) \
{ \
perror(m); \
tfer(); \
}
if(echo_flag)
ERROR(echo_flag);
else
gets(str);
这个语句乍一看没有什么问题,但是把它展开会发现在else
前的;
会导致无法错误。
#define ERROR(m) \
{ \
perror(m); \
tfer(); \
}
if(echo_flag)
{
perror(echo_flag);
tfer();
};
else
gets(str);
而使用do{}while(0)
来包装就不会出现这种错误了。
if(echo_flag)
do{
perror(echo_flag);
tfer();
}while(0);
else
gets(str);
??我们程序员在一句代码的结尾会习惯性加上;
,用do{}while(0)
进行封装结尾必须加上 ;
否则会报错,而{}
后则是可加可不加,然而有时不小心加上后会出现以上的问题。总之,{}
不是不能用,而是可能因为疏忽出现问题,而且由于一些编程习惯会让人用的很难受,所以这里还是建议使用do{}while(0)
。
以上三大块是我这篇文章的主要内容与总结,但是我这里还想给各位加一些饭后小甜点,宏定义的内容就是只是替换,但是#
与##
在宏定义中的妙用却被很多人疏忽了。
'#'的用法
??宏定义中#
的作用是把其后面的变量转化为字符串。例如,如果定义了一个宏:
#define STR(s) #s
那么当使用这个宏定义时,RTR(hello)
会被替换为"hello"
,这样做可以更加方便的输出或处理字符串。
"##"的用法
??宏定义中##
的作用是将其前后的两个变量无缝拼接在一起,并当做一个变量名使用。例如,我定义了这么一个宏:
#define NAME(n) num##n
当我使用这个宏时,就可以把它当做一个变量名来使用,在这里NAME(0)
会被替换为num0
,
int num1;
NAME(1) = 9;
num1 = 9;
在这个例子中这两条赋值语句是等效的,通过宏定义配合##
这种用法,可以方便的定义和使用一组相关的变量,提高编程代码的灵活性。
??以上几乎就是宏定义从入门到进阶的全部内容了,写这篇文章的的起源是一次项目实践的总结,而选择以这种方式来呈现宏定义则是日常我对与编程知识总结的方法论而来的。
??在刚开始学习宏定义时,我查过不少有关博客,但是这些博客有些要么集中讲宏定义的某个方面,对于有些复习的老手来说这不会有什么问题,但是对于新手而言,容易使他们形成对宏定义以偏概全的认识。另一方面很多博客总是简单粗暴的把宏定义分成带参数与不带参数,这样虽然让人容易回忆起,但是无论是函数还是宏定义,我们的目的都应当是以使用为导向的,在合适的时候用合适的方法,前者的简单分类并不能将使用者引导入合适的实践中去,没有深入实践的使用最终只是空中楼阁,只知道有这个东西,但是却总也用不上,总也用不好。这也是这篇文章最后给各位的一些思路,用合适分类方法,以合理的角度去理解技术工具,希望各位有所收获。