可变宏

Posted 悄然拔尖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可变宏相关的知识,希望对你有一定的参考价值。

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之后编译就会通过,这是预处理做了特殊处理,我们并不知道编译器设计者如何去实现的,但是我们应该知道这个特殊的应用场景。

以上是关于可变宏的主要内容,如果未能解决你的问题,请参考以下文章

带有可变参数的嵌套宏在 GCC 中编译,但在 MSVC 中不编译

inline内联函数

在 GCC 中将 0 参数传递给可变参数宏失败,但仅在 C++ 中?

可变参数宏包装器,扩展为使用与参数数量相对应的字符格式化字符串

如何制作可变参数宏(可变数量的参数)

2C++ 的升级