首页 > 代码库 > 可变宏

可变宏

C99中规定宏可以像函数一样带有可变参数,实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个英文输入法下的句号)。这样预定义宏__VA_ARGS__就可以被用在替换部分中,以表明省略号代表什么。

eg:

#include<stdio.h>
#define Variable_Macro(...) printf(__VA_ARGS__)
int main(void)
{
     Variable_Macro("This is a variable macro test...\n");
     Variable_Macro("My age is %d",24);
     return 0;
}

技术分享

 

这里和可变参数函数不同,可变宏可以只有可变参数“...”,但是可变参数函数必须在可变参数前有至少一个其他变量,否则会报错:

技术分享

 但是可变宏也得遵守约定——可变参数只能出现在宏的最后,不能在“...”后面再加参数:

#define Variable_Macro(...,a) 这样是错误的。

 

利用宏参数创建字符串:#运算符

eg:

 

#include<stdio.h>
#define Parameter_Macro(y)    printf("The square of y is %d\n",((y)*(y)))
int main(void)
{
    Parameter_Macro(10);
    return 0;
}

技术分享

引号中的字符串y被当做了普通文本,而不是被看做一个可以替换的语言符号,此时我们可以通过#运算符达到目的。

#include<stdio.h>
#define Parameter_Macro(y)    printf("The square of " #y " is %d\n",((y)*(y)))

int main(void)
{
    Parameter_Macro(10);
    return 0;
}

技术分享

顺便多说一句,ANSI C字符串本就具有拼接功能,在宏中使用  #y 时,可以把实参的字符串传进来。

字符串拼接test:

int main(void)
{
    printf("hello"     "boy\n");
    return 0;
}

技术分享

就算两个双引号之外隔了很多空格,拼接之后也是紧挨着的,要想展现空格,请让空格位于双引号内部。

 

预处理器的粘合剂:##运算符

和#运算符一样,##运算符可以用于类函数的替换部分,另外,##还可以用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。

eg:

#define XNAME(n) x##n
int main(void)
{
    int XNAME(1)=100;//等价于 int x1=100
    printf("XNAME(1) is %d",XNAME(1));
    return 0;
}

技术分享

##运算符把上例的x和n组合成了一个字符。

如果你觉得你已经掌握了#和##在宏中用法,那么看看下面的呢?

eg:

#include<stdio.h>
#define Print_ERROR_1(fmt,...)             printf("<<-Print-ERROR_1->> "fmt"\n",__VA_ARGS__)
#define Print_ERROR_2(fmt,arg...)          printf("<<-Print-ERROR_2->> "fmt"\n",arg)
int main(void)
{
    Print_ERROR_1("%d",1);
    Print_ERROR_2("%s","do you konw?");
    return 0;
}

技术分享

第一个不难理解,和前面介绍的可变参数宏一样,用__VA_ARGS__作为替换。可是第二个看起来似乎有点不一样,在可变参数三个句号前还加了一个arg,居然这样也行?

是的,它们都是可行的,而且效果都是一样的。但是,我们把main函数中的调用改变一下呢,看看会发生什么:

#include<stdio.h>
#define Print_ERROR_1(fmt,...)             printf("<<-Print-ERROR_1->> "fmt"\n",__VA_ARGS__)
#define Print_ERROR_2(fmt,arg...)          printf("<<-Print-ERROR_2->> "fmt"\n",arg)
int main(void)
{
    Print_ERROR_1("why ?");
    Print_ERROR_2("occured err?");
    return 0;
}

是的,我们只改变了调用形式,当可变参数为0时,发生了错误:

技术分享

那么你知道这是为什么吗?

我们看看预编译之后的程序:

技术分享

注意,在没有使用可变参数的时候,最后宏替换在printf函数末尾出现了逗号,这是语法错误,必然导致调用出错。

我们测试最熟悉的printf函数,用错误的方式调用:

技术分享

技术分享

报错和上面宏展开一样,知道这里在可变参数为0个的时候会出错,那么怎么解决这个问题呢?

eg:

#include<stdio.h>
#define Print_ERROR_1(fmt,...)             printf("<<-Print-ERROR_1->> "fmt"\n",##__VA_ARGS__)
#define Print_ERROR_2(fmt,arg...)          printf("<<-Print-ERROR_2->> "fmt"\n",##arg)
int main(void)
{
    Print_ERROR_1("why ?");
    Print_ERROR_2("right now?");
    return 0;
}

是的,我们所做的更改很少,仅仅增加了上面红色部分,增加了##运算符,它就可行了:

技术分享

看看预编译之后的程序:

技术分享

是的,末尾没有逗号了,成功也是必然的。

那么,我们再测试一下这样的宏能在有可变参数的情况下运行吗?

#include<stdio.h>
#define Print_ERROR_1(fmt,...)             printf("<<-Print-ERROR_1->> "fmt"\n",##__VA_ARGS__)
#define Print_ERROR_2(fmt,arg...)          printf("<<-Print-ERROR_2->> "fmt"\n",##arg)
int main(void)
{
    Print_ERROR_1("%s","yes");
    Print_ERROR_2("%s","I can");
    return 0;
}

技术分享

看来后者应该是我们程序中应该出现的,它可以在可变参数有或者没有的时候都能正常工作,这样的宏,一般用作打印调试信息,例如STM32 IIC,SPI中都有这样的宏:

技术分享

那么,问题来了,为什么加了##运算符就成功了,逗号是怎么消失的?

这里,请翻上去看看我举的第一个##运算符的例子,他可以作为预处理器的粘合剂,可以把##前面和后面的字符链接成一个。

以#define Print_ERROR_2(fmt,arg...)          printf("<<-Print-ERROR_2->> "fmt"\n",##arg)为例子

最后那里:,##arg    在没可变参数的时候,##把逗号给吸收了。在没有加上##时,调用Print_ERROR_2(fmt)   ,会被展开成

printf("<<-Print-ERROR_2->> "fmt"\n",)    最后的逗号的存在会让程序出现语法错误,为了消除这个逗号,我们使用##运算符,在有##时,调用展开成:printf("<<-Print-ERROR_2->> "fmt"\n")    为什么最后的逗号不见了,因为它被预处理器粘合剂##吸收了。让逗号跟着后面的可变参数,如果有一个可变参数,例如a,这样上面展开成printf("<<-Print-ERROR_2->> "fmt"\n",a)  ,如果没有可变参数,逗号和空结合,逗号会被预处理器特殊处理,达到让逗号消失的作用。如果可变参数被忽略或为空,‘##‘操作将使预处理器(preprocessor)去除掉它前面的那个逗号。如果你在宏调用时,确实提供了一些可变参数,它也会正常工作,它会把这些可变参数放到逗号的后面。当##遇到后面是可变参数的时候,编译器的预处理器会特殊处理,例如:#define XNAME(n) ,##n这样的宏编译或出错,但是改成#define XNAME(n...) ,##n之后编译就会通过,这是预处理做了特殊处理,我们并不知道编译器设计者如何去实现的,但是我们应该知道这个特殊的应用场景。

可变宏