可变参数宏

可变参数宏 __VA_ARGS__ 简介

一些函数(如printf())接受数量可变的参数. stdvar.h头文件提供了工具, 让用户自定义带可变参数的函数. C99/C11 也对宏提供了这样的工具.
通过把宏参数列表中最后的参数写成省略号(即 3个点…)来实现这一功能. 这样, 预处理宏__VA_ARGS__可用在替换部分中, 表示省略号代表什么. 例如, 下面的定义:

1
#define PR(...) printf(__VA_ARGS__)

假设稍后调用该宏:

1
2
PR("Howdy");
PR("Weight = %d, shipping = $%.2f\n", wt, sp);

展开后的代码是:

1
2
printf("Howdy");
printf("Weight = %d, shipping = $%.2f\n", wt, sp);

加强版变参宏: ##__VA_ARGS__

##__VA_ARGS__ 宏前面加上##的作用在于, 当可变参数的个数为0时, 这里的##起到把前面多余的 “,” 去掉的作用, 否则会编译出错. 使用如下:

1
#define edebug(format, ...) fprintf (stderr, format, ##__VA_ARGS__)

如果可变参数被忽略或为空, ## 操作将使预处理器(preprocessor)去除掉它前面的那个逗号.

如果你在宏调用时, 确实提供了一些可变参数, 它会把这些可变参数放到逗号的后面.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define LOGFUNC2(fmt,...) (printf(fmt" line:%d - %s/%s \n",##__VA_ARGS__,__LINE__,__TIME__,__DATE__));

int main()
{
//可变参数
LOGFUNC2("i am C++ :%d name:%s age:%d",112,"C语言教程",18);// ok

//字符串常量
LOGFUNC2("i am C++ ");// ok
}

输出

1
2
i am C++ :112 name:C语言教程 age:18 line:8 - 02:27:52/May 14 2022   
i am C++ line:11 - 02:27:52/May 14 2022

用途

我们经常会在一些日志模块中使用到可变参数宏, 参考 WYLogger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface WYLogger : NSObject

WYAS_SINGLETON(WYLogger)

- (void)writeLogFile:(const char *)file
function:(const char *)function
line:(int)line
level:(WYLogLevel)level
tag:(NSString *)tag
format:(NSString *)format, ... NS_FORMAT_FUNCTION(6,7);
@end

#pragma mark - Macro

#ifndef WYLogWithLevelAndTag
#define WYLogWithLevelAndTag(lvl, tg, frmt, ...) [[WYLogger sharedInstance] writeLogFile:__FILE__ function:__PRETTY_FUNCTION__ line:__LINE__ level:lvl tag:tg format:frmt, ##__VA_ARGS__];
#endif

这样就可以直接定义一个 WYLogWithLevelAndTag 宏, 方便我们加一些日志等级/日志标记到日志中.
在宏定义中 我们分别用到了... 以及 ##__VA_ARGS__;
还用到了 NS_FORMAT_FUNCTION(6,7), 告诉编译器,索引 6 处的参数是一个格式化字符串,而实际参数从索引 7 处开始。

其中还用到了 __FILE__ __LINE__, 这些是编译器内置的宏:

DATE 当前日期,一个以 “MMM DD YYYY” 格式表示的字符串常量。
TIME 当前时间,一个以 “HH:MM:SS” 格式表示的字符串常量。
FILE 这会包含当前文件名,一个字符串常量。
LINE 这会包含当前行号,一个十进制常量。
STDC 当编译器以 ANSI 标准编译时,则定义为 1;判断该文件是不是标准C程序。

在实现中

1
2
3
4
5
6
7
8
9
10
11
12
- (void)writeLogFile:(const char *)file
function:(const char *)function
line:(int)line
level:(WYLogLevel)level
tag:(NSString *)tag
format:(NSString *)format, ...
{
va_list ap; // 定义一个指向个数可变的参数列表的指针,这个参数列表指针就是args。
va_start (ap, format); // 使参数列表指针指向format,注意是从format的下一个元素开始。
NSString *body = [[NSString alloc] initWithFormat:format arguments:args]; // 把参数拼装起来
va_end(ap); // 结束
}

使用的时候就很方便了,

1
WYLogWithLevelAndTag(WYLogLevelError, @"测试", @"%@ %d 2-1", @"53", 3);

输出

1
[level-4][测试][/path/to/WYRootViewController.m -[WYRootViewController viewDidLoad] 46] 53 3 2-1
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道