有什么用?
通常__VA_ARGS__用于宏定义,其中关于日志宏需要用的,printf 等支持可变参数的函数的宏封装。
首先我们先知道这个__VA_ARGS__的英文全称是“Variadic Arguments” 叫可变参数。说到可变参数学过C语言的朋友们应该都会想到printf();通常我们使用printf 用到占位符,然后传入变量将其改为变量的值。如下代码
char str[] = "龙弟";int age = 21;printf("我是谁:%s,年龄是:%d", str, age);//或者直接打印字符串printf("我是龙弟,年龄21岁");
而大家有想过如果我们第一个参数是字符串“我是谁:%s,年龄是:%d” ,那占位符越多,后面给printf的参数不是越来越多吗?所以就有了可变参数这个说法
int printf(const char *format, ...);
如上代码format 就是 “我是谁:%s,年龄是:%d”,后面的三个点 在C语言中用作可变参数(可以看做一个省略号)。那和__VA_ARGS__有什么关系?二者在定义宏时需要互相打配合!!!
什么时候用?
首先...和__VA_ARGS__, 在函数定义时或者宏定义时为什么用的...而不是__VA_ARGS__。
int printf(const char *format, ...);
就像printf用的是...不是 __VA_ARGS__
...
的作用:
在宏定义中,...
位于固定参数列表之后,表示宏可以接受额外的可变参数。这些可变参数的数量和类型在宏定义时是不确定的,它们是在宏被实际调用时提供的。
__VA_ARGS__
的作用:__VA_ARGS__
是一个特殊的预处理标识符,它用于在宏体内引用所有传递给宏的可变参数。当宏被调用时,__VA_ARGS__
会被替换为实际传递的可变参数列表(包括参数之间的逗号分隔符)。
由上可知:__VA_ARGS__和...的分工是明确的,一个是在定义使用,一个是在宏体内使用。通常的用法如下:
#define LOG(fmt,...) fprintf(stdout,fmt,__VA_ARGS__)int main()
{printf("hello world")return 0;
}
上面的代码中我们定义了一个LOG宏,其中里面封装的是 fprintf。stdout是输出到屏幕的流。LOG中没有指定,所以我们用LOG默认就是打印到屏幕上。
##__VA_ARGS__和__VA_ARGS__的区别
(VS2022中好像没了)
首先##和__VA_ARGS__其实是没什么关联的,##有单独的语法说明,__VA_ARGS__也只是一个宏参数
单独的##使用方法
用于拼接前后的两个代码(token)。但是##不能是第一个或者最后一个子串
#include <iostream>
#define LOG(x) log##x()void logA(){printf("log func A \n");
}void logB(){printf("log func B\n");
}int main()
{LOG(A);getchar();return 0;
}
结果:
##__VA_ARGS__
有了上面的铺垫我们就可以知道,##用于连接两串代码,但是和##__VA_ARGS__一起用时,用于在外界没有传入可变参数时,将“,”抵消。
#define LOG(fmt,...) fprintf(stdout,fmt,__VA_ARGS__)int main()
{char str[] = "龙弟";int age = 21;LOG("我是谁:%s,年龄是:%d", str, age);//或者直接打印字符串LOG("我是龙弟,年龄21岁");return 0;
}
当我们fmt是"我是谁:%s,年龄是:%d",我们的...是str, age。此时的fprintf变成了
fprintf(stdout, "我是谁:%s,年龄是:%d", str, age);
此时str前面这个逗号就是原本__VA_ARGS__前的逗号。所以这样使用时不会出现问题。
但是对于下面的fprintf生成的是:
fprintf(stdout, "我是龙弟,年龄21岁", );
此时我们发现由于没有可变参数的传入,所以__VA_ARGS__为空,多余了一个逗号,会导致出错。这时候##的用处就来了。 把宏替换成如下形式就是在__VA_ARGS__前+##
#define LOG(fmt,...) fprintf(stdout,fmt,##__VA_ARGS__)
这个##将逗号和__VA_ARGS__拼接起来,结果__VA_ARGS__此时为空,和逗号拼接后相当于逗号被拿走了,如果平时有可变参数传入时,这时就算拼接了,那也是正常显示逗号,所以没问题
日志宏
上面说这个语法通常用于日志宏,所以下面实现了一个简易的日志宏给大家
#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include<stdio.h>
#include<time.h>
//定义一个宏函数用来打印日志用fprintf
//fmt 是传入的占位符字符串 ...是可变参数(一般用于后面参数不确定的情况类似printf)
//日志一般要用到时间 这里使用 time函数 struct tm*变量 strftime将time转为字符串存到一个f中 localtime当前时间
//接下来要增加日志等级
#define INF 1
#define DBG 2
#define ERR 3
#define LOG_DEFAULT_LEVEL 2
#define LOG(level,fmt,...) do{\if(level < LOG_DEFAULT_LEVEL)break;\time_t t = time(NULL);\struct tm* lt = localtime(&t);\char st[32] = {0};\strftime(st,31,"%H:%M:%S",lt);\fprintf(stdout,"[level:%d %s %s:%d]" fmt"\n",level,st,__FILE__,__LINE__,##__VA_ARGS__);}while(0)
#define ILOG(fmt,...) LOG(INF,fmt,##__VA_ARGS__)
#define DLOG(fmt,...) LOG(DBG,fmt,##__VA_ARGS__)
#define ELOG(fmt,...) LOG(ERR,fmt,##__VA_ARGS__)
#endif
讲解:
1.日志的等级:INF DBG ERR。默认等级 LOG_DEFAULT_LEVEL 2
2.下面的宏函数主体中 LOG是我们要封装的宏,我们实际封装的宏函数如下:
do{\if(level < LOG_DEFAULT_LEVEL)break;\time_t t = time(NULL);\struct tm* lt = localtime(&t);\char st[32] = {0};\strftime(st,31,"%H:%M:%S",lt);\fprintf(stdout,"[level:%d %s %s:%d]" fmt"\n",level,st,__FILE__,__LINE__,##__VA_ARGS__);}while(0)
讲解:如果低于默认等级则不打印,t 变量 和 lt 变量都是求时间。strftime 是把时间转换为字符串
最后在通过fprintf进行打印,其中__FILE__ __LINE__ 是宏,里面封装了文件名和哪一行
3.最后根据每个等级的日志宏 用LOG进行了封装。实际用法如下:
int main()
{ILOG("我是龙弟");BLOG("我是阿龙");ELOG("今年%d岁",21);return 0;
}