
6.2 宏
宏(macro)是程序设计语言中使用较为广泛的一个概念,简单来说,宏是一种以相同的源语言执行预定义指令序列的指令。在C++中,通过宏的使用,可以将一个表达式定义成宏,并在C++的源程序中随意调用。

6.2.1 宏概述
在C++语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为宏名。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏代换或宏展开。在定义宏时要注意如下的事项:
●宏名一般用大写。
●使用宏可增强程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如,数组大小常用宏定义。
●预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
提示
宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。在C++中,宏分为有参数和无参数两种。
6.2.2 不带参数的宏定义
不带参数的宏也称为无参宏,其宏名后不带参数,定义的一般形式为:

其中,参数定义如下:
●“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令,“#define”为宏定义命令。
●“标识符”为所定义的宏名。
●“字符串”可以是常数、表达式、格式串等。在前面介绍过的符号常量的定义就是一种不带参数的宏定义。
【范例6-1】不带参数的宏定义的使用。该范例中的宏定义#define M(y*y+3*y)定义了M表达式(y*y+3*y),在编写源程序时,所有的(y*y+3*y)都可由M代替,而对源程序编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去置换所有的宏名M,然后再进行编译,代码如代码清单6-1所示。
代码清单6-1

【运行结果】上述代码在Visual C++中运行,其结果如图6-1所示。

图6-1 使用宏定义
【范例解析】在上例程序中首先进行了宏定义,定义M表达式(y*y+3*y),在s=3*M+4*M+5*M中进行了宏调用。在预处理时经宏展开后该语句变为:

警告
在宏定义中,表达式(y*y+3*y)两边的括号不能少。否则会发生错误。例如在上述代码中,将宏定义改为以下定义:

在宏展开时将得到下述语句:

这显然与原题意要求不符,计算结果当然也是错误的。因此在进行宏定义时必须十分注意。应保证在宏代换之后不发生错误。对于宏定义,读者应注意以下几点:
●宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不做任何检查。如有错误,只能在编译已被宏展开后的源程序中发现。
●宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起置换。
●宏定义必须写在函数之外,其作用域为从宏定义命令开始直到源程序结束。
●一般来说,宏名用大写字母表示,以便于与变量区别,但也允许用小写字母。
6.2.3 取消宏
由于宏定义的作用域是整个源程序,而在一些应用中不需要其覆盖整个程序,因此就需要终止其作用域,在C++中终止其作用域的命令为# undef。如果要求宏定义只在一个函数中起作用,就可以在函数定义之前定义宏,在函数结束后结束宏。
【范例6-2】取消宏。该范例中定义的宏只在main()函数中起作用,而在两个函数sout和lout中则无效,代码如代码清单6-2所示。
代码清单6-2

【运行结果】读者在Visual C++中运行上述程序后,系统会出现一个错误:提示在函数sout中没有定义PI,结果如图6-2所示。

图6-2 错误提示
而如果将上述代码中第15行终止宏定义的代码注释掉,那么该程序可以被顺利执行,完成计算圆面积和周长的功能,运行结果如图6-3所示。

图6-3 正确执行宏
【范例解析】读者从上述执行结果可以看出,当使用# undef PI取消了宏后,其在程序中就不再起作用了,如果此时再调用宏,编译系统将给出变量未定义的错误。
注意
以上介绍了宏定义的作用域及取消宏。此外,如果宏名在源程序中用引号括起来了,那么预处理程序将不对其进行宏代换。
【范例6-3】判断定义的宏是否被代换。该范例定义了宏OK,但预处理程序并没有在执行时进行宏代换并输出,实现代码如代码清单6-3所示。
代码清单6-3

【运行结果】在Visual C++中运行上述程序,运行结果如图6-4所示。

图6-4 使用加引号的宏
【范例解析】上述代码定义了宏OK,并在main()函数中使用了OK,但因为其被双引号括起来了,使预处理程序并没有对该宏进行代换,因此其输出也并不是宏定义的值100,而是字符串OK。
注意
凡是被双引号括起来的字符,系统都不会进行宏代换,而是直接输出其中的字符,不进行任何改变。
6.2.4 宏嵌套
在宏定义中,需要读者注意的是,宏定义允许嵌套,即在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
【范例6-4】宏嵌套的实现。该范例定义了两个宏:PI和S,其中宏S的定义中使用到了宏PI,实现代码如代码清单6-4所示。
代码清单6-4

【运行结果】上述代码分别定义了两个宏,在第二个宏S的定义中使用了第一个宏PI,读者可以看到其运行结果如图6-5所示。

图6-5 宏的嵌套定义
【范例解析】读者可以看出,在宏定义语句#define S PI*y*y中,由于PI是宏名,预处理程序自动展开该宏,因此#define S PI*y*y语句相当于如下语句:
#define S 3.1415926*y*y
6.2.5 带参数的宏定义
在6.2.2节中介绍了不带参数的宏定义,这是常见的一种宏定义。事实上,在C++中还允许宏带有参数。与函数定义时所带的参数一样,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。
提示
与不带参数的宏不同的是,带参数的宏在调用中,预处理程序不仅要展开宏,进行宏替换,而且要用实参去代换形参。
带参数的宏定义的一般形式为:

在字符串中含有各个形参。带参数的宏调用的一般形式为:

例如,下面定义了一个M(y)的宏,其中y为该宏的参数,即形式参数,在调用该宏时用实际参数替换形式参数。

在上述宏调用语句k=:M(5);中的执行过程中,实参5代替了形参y,经预处理宏展开后的语句为:k=5*5+3*5。
【范例6-5】带参数的宏定义。该范例定义了一个带有参数的宏,并在主程序中调用该宏,读者应仔细理解该程序,掌握带参数的宏的使用,实现代码如代码清单6-5所示。
代码清单6-5

【运行结果】在Visual C++中运行上述程序,其结果如图6-6所示。

图6-6 使用带参数的宏定义
【范例解析】上述程序的第一行进行带参宏定义,用宏名MAX表示条件表达式(a>b)?a:b,形参a、b均出现在条件表达式中。程序第8行max=MAX(x,y)为宏调用,实参x、y将代换形参a、b,而x、y来源于用户的输入。因此,宏展开后该语句为:

通过前面的内容读者已经知道了,该语句使用了条件运算符,其功能是用于计算x、y中的较大数。这就实现了带参数的宏在应用程序中的作用。
对于带参的宏定义,读者应注意如下的几个事项。
●带参宏定义中,宏名和形参表之间不能有空格出现。例如把:define MAX(a,b) (a>b)?a:b写为:#define MAX (a,b) (a>b)?a:b将被认为是无参宏定义,宏名MAX代表字符串(a,b)(a>b)?a:b。宏展开时,宏调用语句:max=MAX(x,y);将变为:max=(a,b)(a>b)?a:b(x,y);,这显然是错误的。
●在带参宏定义中,形式参数不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值。要用它们去代换形参,因此必须做类型说明,这是与函数中的情况不同的。在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行值传递。而在带参宏中,只是符号代换,不存在值传递的问题。
注意
在宏定义中的形式参数是标识符或者是变量,而宏调用中的实参则不必一定是变量或常量,也可以是表达式。
【范例6-6】宏调用中实参为表达式。该范例定义了一个求平方的宏SQ,在主函数中调用该宏时,实参是表达式a+1,这在C++中是允许的。实现代码如代码清单6-6所示。
代码清单6-6

【运行结果】上述代码实现的功能是求出用户输入的数值加1后得出的平方,因此在实参中使用了表达式a+1,它仅仅是一个变量,执行结果如图6-7所示。

图6-7 实参为表达式的宏调用
【范例解析】在上述示例中,第1行为宏定义,形参为y。程序第8行宏调用中实参为a+1,是一个表达式,在宏展开时,用a+1代换y,再用(y)*(y)代换SQ,得到如下语句:

警告
这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再赋予形参。而宏代换中对实参表达式不作计算直接代换。
此外,在宏定义中,字符串内的形参通常要用括号括起来以避免出错。在上例中的宏定义中(y)*(y)表达式的y都用括号括起来,因此结果是正确的。
【范例6-7】去除括号的宏定义。该范例将上述范例中定义的宏去掉括号,把程序改为如代码清单6-7所示,其执行结果有所不同。
代码清单6-7

【运行结果】读者可以比较代码6-6与代码6-7,其唯一的不同在于第1行代码定义宏时没有将形参y用括号括起来,其他代码都一样,但其在Visual C++的执行结果如图6-8所示。

图6-8 形参未加括号(1)
【范例解析】读者可以看出,同样输入3,但结果却是不一样的。这是由于代换只做符号代换而不做其他处理而造成的。在上述示例中,宏代换后将得到以下语句:

由于a为3,所以sq的值为7,这显然与题意相违。因此,参数两边的括号是不能少的,即使参数只有一个变量。然而,有的时候即使在参数两边加括号还是不够的。
【范例6-8】给宏参数加上括号的宏定义。该范例此处再将代码6-6做一些修改,给宏的定义中加上括号,代码如代码清单6-8所示。
代码清单6-8

【运行结果】读者可以看到,上述代码与代码6-6相比,只把宏调用语句改为:sq=16/SQ(a+1);,运行本程序如输入值仍为3时,希望结果为1。但实际运行的结果如图6-9所示。

图6-9 形参未加括号(2)
【范例解析】此处仔细分析该宏定义,在宏代换之后调用语句变为:

当用户输入3,即a为3时,由于除号“/”和乘号“*”的运算符优先级和结合性相同,则先运算16/(3+1)得4,再运算4*(3+1),最后得16。为了得到正确答案,应在宏定义中的整个字符串外加括号。
【范例6-9】给宏整体加上括号的宏定义。该范例在对程序进行修改,给宏定义的整体加上括号,代码如代码清单6-9所示。
代码清单6-9

【运行结果】读者可以看到,代码6-9与代码6-8的区别就在于宏定义时对其形参的整体都增加了括号,其运行结果如图6-10所示。

图6-10 形参增加了括号
【范例解析】从上述示例中读者可以看出,同样输入3,即a为3时,得出的结果是正确的。再来看一下该调用语句的执行,由于宏定义语句的改变,在宏代换之后调用语句变为:

因此,此处得出的结果是符合要求的。
注意
根据上述带参数的宏定义的不同方式,读者在进行带参数的宏定义时,需要时时关注宏展开后是否符合用户要求。
6.2.6 内联函数
内联函数也称为内嵌函数,当在一个函数的定义或声明前加上关键字inline时就把该函数定义为内联函数,它主要用于解决程序的运行效率。

计算机在执行一般函数的调用时,无论该函数多么简单或复杂,都要经过参数传递、执行函数体和返回等操作,执行这些操作都需要一定的时间。若把一个函数定义为内联函数后,在程序编译阶段,编译器就会在每次调用该函数的地方都直接替换为该函数体中的代码,由此省去函数的调用、相应地保存现场、参数传递和返回操作等所需的时间,从而加快了整个程序的执行速度。
【范例6-10】内联函数的应用。该范例定义了一个内联函数abs(),其用于求一个整数的绝对值,再在主函数中调用该内联函数,实现代码如代码清单6-10所示。
代码清单6-10

【运行结果】在Visual C++中执行上述程序,其返回结果如图6-11所示。

图6-11 内联函数
【范例解析】读者可以看出,内联函数与普通的函数调用得到的结果是相同的,但是它们内部的执行是不同的。调用内联函数时,编译系统直接将内联函数代码替换到主函数调用的地方,而普通的函数则是通过参数传递,将函数的结果返回到主函数。
提示
由于编译时内联函数的代码将直接替换到主函数调用的地方,节省了调用传参数等步骤的时间,从而加快了程序的运行速度。
6.2.7 宏与函数的区别
由于宏也可以带参数,而且带参数的宏与带参数的函数的写法和调用都很相似,但是其存在本质上的不同。前面已经提到过,函数调用时要把实参表达式的值求出来再赋予形参,而宏代换中对实参表达式不计算而直接地代换。这导致了即使把同一表达式用函数处理与用宏处理,两者的结果也有可能是不同的。
【范例6-11】宏的定义和调用与函数的定义和调用的比较。该范例定义了一个带参宏和带参函数,其函数名为SQ,形参为Y,函数体表达式为((y)*(y)),而宏定义也定义字符串为((y)*(y)),代码如代码清单6-11(a)和代码清单6-11(b)所示。
代码清单6-11(a)

【运行结果】上述程序实现的是一个简单地打出1~5各个数字的平方值,在Visual C++中运行该程序,其结果如图6-12所示。

图6-12 带参函数
【范例解析】该范例用函数的方法实现了输出1~5各个数字的平方值,其首先声明函数SQ,再在主函数main()中调用该函数。
代码清单6-11(b)则实现带参数的宏的定义和调用,读者可将代码清单6-11(a)中的函数的定义和调用与下面的宏的定义和调用相比较。
代码清单6-11(b)

【运行结果】该示例中使用的是带参数的宏定义,并在输出时调用该宏,同样,在Visual C++中运行该程序,其结果如图6-13所示。

图6-13 带参宏
【范例解析】读者可以看到,在上述两个示例中,代码6-11(a)中定义的函数名为SQ,形参为((y)*(y));代码6-11(b)中定义的宏名为SQ,形参也为y,字符串表达式为((y)*(y)),这两个代码是相同的。此外,代码6-11(b)中调用函数为SQ(i++),代码6-11(b)中的宏调用为SQ(i++),实参也是相同的。
然而从输出结果来看,使用函数和使用带参数的宏得到的结果却大不相同。这是因为在代码清单6-11(a)中,函数调用是把实参i值传给形参y后自增1,然后输出函数值,因而要循环5次。输出1~5的平方值。而在代码清单6-11(b)中宏调用时,只做代换。SQ(i++)被代换为((i++)*(i++))。每次循环后i的值会增加2,因此只做3次循环。
注意
读者从上述两段代码及其以上分析可以看出,函数调用和宏调用二者在形式上相似,但在本质上是完全不同的。