#define是C语言提供的宏定义命令,其主要目的:在编程时,为程序员提供一定方便,并能在一定程度上提高程序的执行效率。#define将一个标示符定义为一个字符串,该标示符被称为宏,被定义的字符串称为替换文本。宏定义一般有两种形式:一种是简单宏定义(即字面值),另一种是带参数宏定义(即常说的伪函数)。
简单的宏定义定义形式为:
#define <宏名> <字符串>
例如:
#define PI 3.1415926
说明:
- 一般,宏名用大写字母表示,但这并非规定,也可用小写。从编码一致性、可读性角度考虑,强烈建议宏名统一采用大写字母表示。
- 使用宏名代替一个字符串,可以减少程序中重复书写某些字符串的工作量,同时也避免了由于疏忽导致的字符串书写错误问题。
- 宏定义是用宏名代替一个字符串,也就是做简单的置换,并不做正确性检查。如写成 :
#define PI 3.l4l5926(把1错写成了l),在预编译时,不会做任何语法检查;在编译宏展开后的源程序时,才会发现语法错误并报告。 - 宏定义不是C语句,不必在行尾加分号,如果加了分号,将连同分号一起被替换。例如:
#define PI 3.1415926;
area = PI * r * r;
宏展开后该语句:area = 3.1415926;* r * r; - 在程序中,如果#define出现函数外面,宏的有效范围为宏定义之后到本源文件结束,通常宏定义写在文件开头,在此文件范围内有效。另外,用#undef可终止宏定义的作用域,这样可以灵活控制宏定义的作用范围。
- 在进行宏定义时,可以用已经定义的宏名,可层层置换,但对于用引号括起来的字符串内的字符,即使与宏名相同,也不进行替换。
- 宏定义是专门用于预处理的专用名词,它与定义变量的含义不同,只做字符替换,不分配内存空间。
在C语言中,通过简单的宏定义恒值常量,是C语言中定义恒值常量的唯一手段。但是在C++中这不在是唯一了,C++的const也可定义一个恒值常量。例如:
const int PI = 3.1415926;
const定义的恒值常量比#define定义的恒值常量存在很多优点,两者的区别如下:
(1)const定义常量是有数据类型,而#define定义的常量无数据类型
通过const定义的常量,编译器可对其进行静态类型安全检查,而#define宏定义的常量只是简单的字符替换,没有类型安全检查,且有时还会产生边际效应。所谓边际效应,举例如下:
#define N 100
#define M 200 + N
当程序中使用 M*N 时,你期望的结果是:100 * (200+ N),而实际结果是:100 * 200 + N。而const常量就没有这些问题。
(2)有些调试程序可对const进行调试,但不无法对#define进行调试。
(3)当定义局部变量时,const作用域仅限于定义局部变量的函数体内。但#define其作用域不限于定义局部变量的函数体,而是从定义点到整个程序的结束点。但可用#undef取消其定义,从而限定其作用域范围。
(4)const定义常量,并不能起到其强大的作用。const还可修饰函数形式参数、返回值和类的成员函数等。从而提高函数的健壮性。因为const修饰的东西必须受c/c++的静态类型安全检查机制的强制保护,可防止意外的修改。
带参数的宏定义定义形式为:
#define <宏名>(<参数表>) <宏体>
例如:
#define MAX(x,y) (x)>(y)?(x):(y)
标示符被定义为宏后,该标示符便是一个宏名。在程序中,出现宏名的地方,在程序被编译前,先将宏名用被定义的字符串替换,这被称为宏替换。替换后才进行编译,宏替换是简单字符替换。
说明:
- 带参数宏展开,只是将语句中宏名后面括号内的实参字符串,替换#define宏定义中的形式参数。
- 宏定义时,宏名与包含宏参数的括号之间不应加空格,否则编译器会将空格后的字符都作为替换字符串的一部分。
例如:
#define S (r) PI*r*r // (在S与(之间有空格)
这个宏真正的含义:S是符号常量(不带参数的宏名),它代表字符串“( r ) PI*r*r”。在程序中,语句:area = S(a);,将会被展开为:area = ( r ) PI*r*r(a);。这显然不对。
明白了带参数的宏定义,也许你会有这个感觉:带参数的宏,有点儿像函数,的确事实也是如此,带参数的宏和函数确实有很多的相同点和不同点。具体带参数的宏和函数有下述区别。
- 函数调用时,先求实参表达式的值,然后传递给形参,而使用带参数的宏只是进行简单的字符串替换,不进行表达式求值。
- 函数调用是在程序运行时处理的,为形参分配临时的内存空间,而宏展开是在编译前进行的,在展开时并不分配内存单元,不进行值的传递,同样也没有“返回值”的概念。
- 函数的实参和形参都必须定义类型,二者的类型要求一致,如不一致,则要进行类型转换,而宏不存在类型问题,因为宏名无类型,它的参数也无类型,仅是字符串替换,展开时代入指定的字符串即可。宏定义时,字符串可是任何类型的数据。
- 调用函数只会得获得一个返回值,而宏则不然,可设法得到几个返回值。
- 宏展开会促使源程序变长,因为每次展开都使程序增长,而函数调用不会增加代码长度。
- 宏替换不占用运行时间,只占编译时间,而函数调用则占用运行时间用于分配单元、保存现场、值传递、返回等。所以宏替换可以提供程序的执行效率,但代价是编译时间会变长。
讨论了宏的两种定义形式,我们继续分享宏的引入给程序带来好处,以及宏定义和命名空间的关系。
(1)宏定义的引入可方便程序的修改。使用简单宏定义,可用宏替换那些在程序中经常使用的常量,当需要修改该常量时,不用对整个程序进行修改,只修改宏定义的字符串即可奏效,而且当常量比较长时, 我们可以用较短、有意义的标识符来编写程序,这样更方便一些。
此处所说的常量改变,不是指在程序运行期间改变,而是在编程期间的改变,举一个大家熟悉的例 子,圆周率π是在数学上常用的一个值,有时我们会用3.14表示,有时也会用3.1415926等,这要看计算所需要的精度,如果我们编制的程序多次使用它,那么需要确定一个数值,在本次运行中不改变,但也许后来发现程序所表现的精度有变化,需要改变它的值, 这就需要修改程序中所有的相关数值,这会给我们带来一定的不便,但如果使用宏定义,使用一个标识符来代替,则在修改时只修改宏定义即可,还可以减少输入3.1415926这样长的数值多次,我们如此定义#define PI 3.1415926,既减少输入,又便于修改,何乐而不为呢?
(2)提高程序的运行效率。使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要替换。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况决定是否使用宏定义。
(3)#define预处理器完全没把C++的作用域纳入考量,绝大多数C++实现都是封装在命名空间里,这样的做法有很多优点。但不幸的是,#define的作用域并未被限定在名字空间中,例如:
namespace Influential
{
#define MAX 1<<16
// Infuential命名空间实现
}
namespace Facility
{
const int max = 512;
// Facility 命名空间实现
}
int a[MAX]; // 可以通过编译,但这种实现是糟糕的。
讨论一下,上述代码为什么是糟糕的实现?按照命名空间的原理,定义在Infuential空间中的MAX不能在空间之外使用。但由于MAX是宏定义,不受名字空间的限制。
最后,需要提醒的是,宏不仅可定义字面值和伪函数。另一个重要的用法是条件编译。在大规模的开发过程中,特别是跨平台和系统的软件里。可以在编译的时候,通过#define设置编译环境。例如:
#ifdef WINDOWS
......
......
#endif
#ifdef LINUX
......
......
#endif
请谨记
- 宏可以定义字面值常量,也可以定义带参数的伪函数。在宏定义时需注意其与函数的区别。宏定义不是C++的语句。在编译时使用宏的地方会发生字符串替换。
- 在应用时,如果你定义一个字面值常量建议使用const替换#define。
- 宏定义不受名字空间的限制。在名字空间外依然可见。