printf 函数经常被用来进行打印。其中 printf 函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进行数据的格式化。
一、不定参宏函数
这里使用了可变参数 __VA_ARGS__ 来表示不定参,且 ##__VA_ARGS__ 语法用于处理可变参数为空的情况。
一个简单的宏定义 LOG,用于格式化输出日志信息。其中包含有不定参,调用该宏相当于调用 printf,fmt 表示参数 fmt,表示格式字符串;宏 __FILE__ 表示对应文件名,宏 __LINE__ 表示对应的行号(宏 __FUNCTION__ 在预编译时会替换成当前的函数名称),这两个都是 stdio.h 头文件中的宏;##__VA_ARGS__ 相当于不定参数 ...,## 是为了处理不带参的情况。
如果宏调用时没有传递可变参数,## 会去除前面的逗号,避免语法错误。当调用为 LOG("你好\n") 时,此时可变参数部分为空,那么展开为 printf([%s:%d]"你好\n",),出现多余的逗号,编译器将会报错,而 ## 的作用就是在这种情况下消除多余的逗号。
也可以将 '\n' 换行符放在宏定义中,显示效果相同:
扩展:GCC 的复杂宏
GCC 使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。这和上面举的那个定义的宏例子是完全一样的。
二、C 语言风格的不定参函数
需要引入 stdarg.h 头文件,使用其中的 va_list、va_start、va_arg 和 va_end。
- va_list:是一个类型,用于声明一个变量,用该变量来存储不定参数的信息。
- va_start:用于初始化 va_list,让 va_list 指向不定参数列表的起始位置。可以接受 2 个参数,第一个是 va_list 对象,第二个是用于确定不定参数的起始位置。
- va_arg:在不定参数列表中,从 va_list 指向的参数开始,逐个返回 type 类型的数据,一般需要循环调用 va_arg,在此过程中 va_list 会不断往后走。可以接受 2 个参数,一个是 va_list,一个是要获取的参数的类型。
- va_end:用于清理 va_list 对象,清空可变参数列表,使用结束后就失效了,为了避免野指针的引用,va_end 可以销毁 va_list 指针,确保在使用完不定参以后正确的释放资源。
vasprintf:动态分配内存来存储格式化之后的字符串,可以接受可变参数。strp 分别表示指向 char 指针的指针,用来存储格式化后的字符串地址。fmt 是一个格式化的字符串,包含要打印的文本和格式说明符,ap 表示可变参数列表。
注意:不要忘记宏定义:#define _GNU_SOURCE
vasprintf 会根据 fmt 字符串和可变参数列表 ap 的内容,动态分配足够的内存来存储格式化后的字符串,并将地址存储在 strp 指针中,如果成功就会返回格式化后的字符串的长度,否则返回 -1。
va_list 只是定义了一个变量来存储,是个空指针。当使用 va_start 以后,会让这个空指针与不定参产生联系,可以理解成空指针指向不定参的变量了,此时也就不是空指针了。接着只需要不断地使用 va_arg 去从 va_list 中取出数据即可,最后使用完之后不要忘记用 va_end 关闭。
这里也可以用 vsprintf:
效果一样,但是这里的 str 需要初始化,否则会出现段错误。
三、C++ 风格不定参函数
基本模板:
这里的 Args 是一个模板参数包,可以接受任意数量的类型参数。
template<typename ...Args>
void print(Args... args) {}
1、递归获取
- args 函数参数包作为参数传递给另一个函数时,记得加上 ...,表示这个 args 参数是不定参数包。
- 函数接收 Args... 类型的参数包时,通常可以将参数设置为 && 万能引用,以此减少拷贝的成本。
函数参数包:与模板参数包类似,函数参数包也可以包含零个或多个函数参数。在函数声明中,同样可以使用省略号(…)来表示一个函数参数包。
注意:参数包的名称(如 Args 和 args)可以自由选择,但通常以 "Args" 和 "args" 作为约定。
sizeof… 是一个编译时操作符,可以用于获取参数包中元素的数量。
定义了一个模版函数,第 1 个是类型为 T 的参数,第 2 个是不定参数,它的长度不确定。每一次递归调用的时候,不定参数中的第一个参数就会传入到 v 这个位置,就相当于不定参数的一个参数被消耗掉了。然后接着继续递归,不定参数每递归一次就会少一个参数,直到最后没有参数了。