2.2.2 带参数的宏替换

带参数的宏替换,其定义的一般形式为:


define 宏名(参数表)字符串


在讲解带参数的宏的使用之前,同样先来看看使用带参数的宏时需要注意的几点。

宏名和参数表的括号间不能有空格。

宏替换只做替换,不做计算和表达式求解,这一点要格外注意。

函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存。

宏的哑实结合(哑实结合类似于函数调用过程中实参替代形参的过程)不存在类型,也没有类型转换。

宏展开使源程序变长,而函数调用则不会。

下面通过Linux下的两个典型的宏定义来介绍带参数的宏定义。说其典型,是由于其宏定义的“完整性”,至于“完整性”究竟体现在什么地方,我们通过代码来逐一分析。


define min(x,y)({typeof(x)_x=(x);typeof(y)_y=(y);(void)(&_x==&_y);

_x<_y?_x:_y;})

define max(x,y)({typeof(x)_x=(x);typeof(y)_y=(y);(void)(&_x==&_y);

_x>_y?_x:_y;})


在上面的两个宏中都有代码“(void)(&_x==&_y);”,可能不少读者对其并不理解,下面进行仔细分析。首先分析“==”,这是一个逻辑表达式的运算符,它要求两边的比较类型必须一致。如果&x和&y的类型不一致,一个为char,另一个为int,那么使用gcc编译就会出现警告信息,用VC++6.0编译时则会报错“error C2446:'==':no conversion from'char'to'int'”。代码“(void)(&_x==&_y);”的功能就相当于执行一个简单的判断操作,判断x和y的类型是否一致。别小看了这句代码,学会使用它会为编码带来不少便捷。下面给出一个小示例。


include<stdio.h>

void print()

{

printf("hello world!\n");

return;

}

void main(int argc,char*argv)

{

print();

return;

}


运行结果:


hello world!


现在适当修改一下上面的代码。


include<stdio.h>

void print()

{

printf("hello world!\n");

return;

}

void main(int argc,char*argv)

{

define print()((void)(3))

print();

return;

}


运行结果没有任何输出。

这次的结果没有了之前的那句“hello world!”,可以看出此时函数并没有被调用,这是因为“#define print()((void)(3))”使之后的调用函数print()成为一个空操作,所以这个函数在接下来的代码中都不会被调用了,就像被“冲刷掉”了一样。看了上面给出的宏,细心的读者会有另外一个疑惑:在“#define min(x,y)({typeof(x)_x=(x);typeof(y)_y=(y);(void)(&_x==&_y);_x<_y?_x:_y;})”中,为什么要使用“typeof(y)_y=(y)”这样的替换,而不直接使用“typeof(x)==typeof(y)”或者“x<y?x:y;”呢?因为使用“typeof(x)==typeof(y)”就像使用“char==int”一样,这是不允许的。如果在宏中没有使用“(void)(&_x==&_y);”这样的语句,那么编译时就相当于失去了类型检测功能。在上面的宏中使用“typeof(y)_y=(y)”这样的转换是为了防止x和y为一个表达式的情况,如x=i++,如果不转换,那么i++就会多执行几次操作,得到的就不是想要的结果。如果使用了“typeof(y)_y=(y)”这样的转换,就不会出现这样的问题了。我们可以通过下面一段代码来看看它们之间的区别。


include<stdio.h>

define min(x,y)({typeof(x)_x=(x);typeof(y)_y=(y);(void)(&_x==&_y);

_x<_y?_x:_y;})

define min_replace(x,y)({x<y?x:y;})

void main()

{

int x=1;

int y=2;

int result=min(x++,y);

printf("没有替换时的运行结果为:%d\n",result);

int x1=1;

int y1=2;

int result1=min_replace(x1++,y1);

printf("替换之后的运行结果为:%d\n",result1);

return;

}


在Linux环境下使用gcc编译的运行结果:


没有替换时的运行结果为:1

替换之后的运行结果为:2


分析上面的运行结果可以发现,使用相同输入的两种宏得到的最终结果并不一样,在2.3节中我们还会对其进行详细分析。

下面来看如何使用宏定义实现变参,先看看实现方法。


define print(……)printf(VA_ARGS


在这个宏中,“……”指可变参数。可变参数的实现方式就是使用“……”所代表的内容替代VA_ARGS,看看下面的代码。


include<stdio.h>

define print(……)printf(VA_ARGS

int main(int argc,char*argv)

{

print("hello world——%d\n",1111);

return 0;

}


在Linux环境下采用gcc进行编译的运行结果:


hello world——1111


再看代码:


define printf(tem,……)fprintf(stdout,tem,##VA_ARGS


可能有些读者对fprintf()函数感觉有些陌生,在此对fprintf()函数进行简单的讲解,其函数原型为:


int printf(FILEstream,charformat[,argument])


这个函数的功能为根据指定的format格式发送消息到stream(流)指定的文件中,在前面的宏中使用stdout表示标准输出,fprintf()的返回值是输出的字符数,发生错误时返回一个负值。


include<stdio.h>

define print(temp,……)fprintf(stdout,temp,##VA_ARGS

int main(int argc,char*argv)

{

print("hello world——%d\n",1111);

return 0;

}


在Linux环境下采用gcc进行编译的运行结果:


hello world——1111


temp在此处的作用为设定输出字符串的格式,后面的“……”为可变参数。现在问题来了,在宏定义中为什么要使用“##”呢?如果没有使用##,会怎么样呢?看看下面的代码:


include<stdio.h>

define print(temp,……)fprintf(stdout,temp,VA_ARGS

int main(int argc,char*argv)

{

print("hello world\n");

return 0;

}


在Linux环境下采用gcc进行编译时发生了如下错误:


arg.c:In function'main':

arg.c:7:2:error:expected expression before')'token


为什么会出现上述错误呢?现在我们来分析一下。进行宏替换,“print("hello world\n")”变为“fprintf(stdout,"hello world\n",)”后,会发现后面出现了一个逗号导致发生错误。如果有“##”,就不会出现这样的错误,这是因为可变参数被忽略或为空,“##”操作将使预处理器去除它前面的那个逗号。如果存在可变参数,“##”也能正常工作。

介绍了“##”,再来介绍一下“#”。先来看看下面一段代码。


include<stdio.h>

define return_exam(p)if(!(p))\

{printf("error:"#p"file_name:%s\tfunction_name:%s\tline:%d.\n",\

FILEfuncLINE);return 0;}

int print()

{

return_exam(0);

}

int main(int argc,char*argv)

{

print();

printf("hello world!\n");

return 0;

}


在Linux环境下采用gcc进行编译的运行结果:


error:0 file_name:arg.c function_name:print line:9.

hello world!


因为这里只是为了体现要讲解的宏,所以对代码做了最大的简化,后续章节还将深入讲解如何使用宏来调试代码。“#”的作用就是对其后面的宏参数进行字符串化操作,即在对宏变量进行替换之后在其左右各加上一个双引号,这就使得“"#p"”变为了“""p""”,我们发现这样两边的“""”就消失了。