019.8.31
起步
假使你有使用 Python 编程的经验,你应该会觉得设计接口能用 *arg
和 **kwarg
这件事是多么的酸爽。毕竟定义一个拥有长长参数列表的函数是多么的累赘,形参不能总是被函数使用到则是累赘中的累赘。多说无益,还是用个 Python Demo 举例。
假使我想设计一个打印函数,就叫 YouPrint 吧。YouPrint 会将我传递的参数按每行打印,且排头以 “You” 开头。使用如下:
Usage:
YouPrint("zhong", "ying", "ding")
Output:
You: zhong
You: ying
You: ding
如果不用“可变参数列表”的方式设计,那 YouPrint 应该如下这般:
def YouPrint(a, b, c):
for alpha in (a, b, c):
print(f"You: {alpha}")
这样显然在违背我们对 打印函数 的习惯。难道打印函数不应该允许任意传参吗?万一我只传一个参数呢,又或者传十个呢?在 Python 中,其解决办法就是 *arg
。因此合理的设计是:
def YouPrint(*arg):
for alpha in arg:
print(f"You: {alpha}")
在 Python 中,*变量名
出现在形参中时,其意义为:变量 arg (上面例子中用到 arg)会接收所有的位置传参(这些参数没有被其他形参接收),自身将以元组的身份出现,存放这些传递过来的参数。听不懂没关系,我就是想引出,C 语言也可以做到。
C语言的可变参数列表
C 语言的可变参数主要有三种方式实现,一种基于标准库 <stdarg.h>
,使用 va_start, va_arg, va_end。另一种用宏 \_\_VA_ARGS\_\_, 再一种用记号粘贴符号 ##。
后两种的使用场景较小,常在 debug 之类的时候用;第一种使用场景则更为广泛些,但事实上不建议滥用。
VA_ARGS 与 记号粘贴符
现在假设一个场景,我们写程序的时候需要 printf 之类的 DEBUG 操作,同时希望程序完成之后,这些打印语句会自动失效,而非我们手动去删除。并且,这些 DEBUG 语句应该要有明显的标识,足以让我们一眼辨出哪些是 DEBUG 语句,哪些是正常输出。
而作为一个输出函数,自然有必要同 printf 一样,在合法操作下传递任意个参数。
根据以上要求,显然宏函数最为合适不过。用 \_\_VA_ARGS\_\_ 实现如下:
#include <stdio.h>
#define DEBUG
#ifdef DEBUG
#define f_debug(fmt, ...) printf("DEBUG: " fmt "\n", __VA_ARGS__)
#else
#define f_debug(fmt, ...)
#endif
int main()
{
int a, b;
f_debug("完成变量 %s 与 %s 的定义", "a", "b");
a = 1024;
b = 512;
f_debug("完成变量 a = %d 与 b = %d 的赋值", a, b);
int rlt = a + b;
f_debug("完成变量之间的加法运算i,结果为:%d,即将准备输出结果", rlt);
printf("The result is %d\n", rlt);
return 0;
}
(如果你对 #ifdef 等条件编译了解有限,不妨看看我对该内容的一些笔记 C语言-预处理(1), C语言-预处理(2)。这里就不再做过多的讲解。)
编译代码后运行,可以看到输出如下:
DEBUG: 完成变量 a 与 b 的定义
DEBUG: 完成变量 a = 1024 与 b = 512 的赋值
DEBUG: 完成变量之间的加法运算i,结果为:1536,即将准备输出结果
The result is 1536
如果我们不需要 DEBUG 信息了,注释掉代码中的一行宏定义即可:
/* #define DEBUG */
输出:
The result is 1536
尽管以上的设计有些不好,因为我们无法对 f_debug 传递一个参数,好在基本实现了我们对功能的诉求。也可以看到,不论是传递三个参数,还是两个参数,只要在第一个参数 fmt 的正常约束之下,程序都是可以正常运行的。
记号粘贴运算符的使用与 VA_ARGS 极类似。我们只需要对不定长参数(也就是省略号)取个名字,然后再 ##名字
即可。使用同上的示例代码,但需要做些小小改动:
#define f_debug(fmt, ...) printf("DEBUG: " fmt "\n", __VA_ARGS__)
||
|| 修改
\/
#define f_debug(fmt, arg...) printf("DEBUG: " fmt "\n", ##arg)
代码中的 arg 就是 ...
的名字。
标准库 stdarg
标准库 <stdarg.h>
带领之下,不定长参数的功能更强大些了,不过用起来就稍稍麻烦了。需要记住三个宏函数,一个数据类型:
- va_start, va_arg, va_end
- va_list
用一个简单的示例说明一下如何使用:
#include <stdio.h>
#include <stdarg.h>
/* 形参 num 用来指明不定长参数的个数 */
void print_a_word(int num, ...)
{
va_list arg; /* 声明一个变参 arg,其类型为 va_list */
va_start(arg, num); /* 初始化 */
int i;
for(i=0; i<num; ++i) {
/* va_arg 用来取出变参中的变量,char* 用来指明变量的类型 */
printf("%s\n", va_arg(arg, char*));
}
va_end(arg); /* 将变参 arg 置为 NULL */
}
int main()
{
print_a_word(3, "I love you.", "I miss you.", "Where are you?");
return 0;
}
// 输出:
I love you.
I miss you.
Where are you?
需要说明的有几点:
- 必须向函数传递不定长参数的个数,因为 va_start 绑定变参的时候需要用到该值。
- 每调用一次 va_arg,指针就会指向下一个参数变量。
- va_arg 的第二个参数是参数类型,这就意味着有些类型是不建议使用的。因为函数调用时会发生类型转换,如 short,char 会转换成 int,float 会转换成 double。
将上述代码做一些小小改动,传递 char 类型的实参:
#include <stdio.h>
#include <stdarg.h>
void print_a_word(int num, ...)
{
...
for(i=0; i<num; ++i) {
printf("%c\n", va_arg(arg, char)); /* char 类型 */
}
...
}
int main()
{
print_a_word(3, 'a', 'b', 'c'); /* char 类型 */
return 0;
}
编译代码时会有警告如下:
╭─root@localhost /home/Cpp/ChangeArgs
╰─✗ gcc a-chang.c
In file included from a-chang.c:2:0:
a-chang.c: 在函数‘print_a_word’中:
a-chang.c:11:36: 警告:通过‘...’传递时‘char’被提升为‘int’ [默认启用]
printf("%c\n", va_arg(arg, char));
^
a-chang.c:11:36: 附注:(因此您应该向‘va_arg’传递‘int’而不是‘char’)
a-chang.c:11:36: 附注:如果执行到这段代码,程序将中止
此时强行运行程序,会发生段错误。如果根据编译器提示,将 va_arg(arg, char)
修改为 va_arg(arg, int)
,编译代码就不会有告警信息,还可以得到正确的运行结果。
然而,不定长传参本身就不建议使用,何况这种 “奇巧淫技” 的手段呢!
还不快抢沙发