在 C 语言中,对于一些常用或通用的功能或代码段的封装可以有两种方式:函数和宏定义。那么,对于这两种方式,我们该如何抉择呢?在解决这个问题之前,有必要先来了解一下它们之间的区别。
函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放栈。这种开销不仅会降低代码效率,而且代码量也会大大增加。而宏定义只在编译前进行,不分配内存,不占运行时间,只占编译时间,因此在代码规模和速度方面都比函数更胜一筹。
函数的参数必须声明为一种特定的数据类型,如果参数的类型不同,就需要使用不同的函数来解决,即使这些函数执行的任务是相同的。而宏定义则不存在着类型问题,因此它的参数也是无类型的。也就是说,在宏定义中,只要参数的操作是合法的,它可以用于任何参数类型。
毋庸置疑,在宏定义中,在对宏参数传入自增(或者自减)之类的表达式时很容易引起副作用,尽管前面也给出了一些解决方案,但还是不能够完全杜绝这种情况的发生。与此同时,在进行宏替换时,如果不使用括号完备地保护各个宏参数,那么很可能还会产生意想不到的结果。除此之外,宏还缺少必要的类型检查。而函数却从根本上避免了这些情况的产生。
在每次使用宏时,一份宏定义代码的副本都会插入程序中。除非宏非常短,否则使用宏会大幅度地增加程序的长度。而函数代码则只会出现在一个地方,以后每次调用这个函数时,调用的都是那个地方的同一份代码。
不难发现,单从上面 4 点区别来看,函数和宏定义各有优缺点,这就要求我们根据具体情况具体分析,合理地对二者进行取舍。看下面两个封装示例:
/*宏定义的方式*/
#define MAX(x,y) (((x)>(y)) ? (x):(y))
#define MIN(x,y) (((x)<(y)) ? (x):(y))
/*函数的方式*/
int max(int x,int y)
{
return (x>y?x:y);
}
int min(int x,int y)
{
return (x<y?x:y);
}
从表面上来看这两个示例,使用宏的封装方式明显优于函数的方式,原因很简单:如果这里要继续比较两个浮点类型数的大小时,就不得不再写两个专门针对浮点类型数的比较函数,对于其他类型数的比较以此类推;而宏定义因为不存在任何类型问题,因此可以用于整型、长整型、浮点型以及其他任何可以使用“>”与“<”操作符比较值大小的类型,正所谓一劳永逸。
但是,假如我们这里需要调用上面的MAX宏来寻找 i1、i2、i3、i4 与 i5 5 个数的最大者(甚至更多数),如下面的示例代码所示:
int i1=0;
int i2=1;
int i3=2;
int i4=3;
int i5=4;
int max=MAX(i1,(MAX(i2,(MAX(i3,MAX(i4,i5))))));
接下来,编译器对语句“MAX(i1,(MAX(i2,(MAX(i3,MAX(i4,i5))))))”进行展开如下:
上面的展开代码看起来很郁闷,基本快花眼了。当然,这里还可以对宏调用语句进行优化,如下面的示例代码所示:
MAX(MAX(MAX(i1,i2),MAX(i3,i4)),i5);
现在看起来虽然精简许多,但还不是很乐观,展开代码如下所示:
面对这种情况,有读者或许会认为函数比宏方便,代码也显得苗条与可爱多了。但不能够一概而论,应具体情况具体分析。
除此之外,一些特殊功能根本无法用函数实现时,可以选择使用宏定义来实现。例如,参数类型无法作为参数传递给函数,但是可以把参数类型传递给带参的宏,如下面的示例代码所示:
#define MALLOC(n,type) ((type *) malloc((n)* sizeof(type)))
现在,利用 MALLOC 宏,就可以为任何类型分配一段指定的空间大小,并返回指向这段空间的指针。如下面的示例代码所示:
p = MALLOC(8,int);
展开以后的结果为:
p = (int *) malloc((8) * sizeof(int));
由此可见,宏定义有时候还可以完成函数不能够完成的一些特殊功能。因此,如何取舍这二者,还需要根据具体情况具体分析,千万不能够武断地做出判断。一般来说,应该用宏去替换小的、可重复的代码段,这样可以使程序运行速度更快。当任务比较复杂,需要多行代码才能实现时,或者要求程序越小越好时,就应该使用函数。