C语言-可变参数列表

C·语法 2019-08-31 4319 字 1135 浏览 点赞

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),编译代码就不会有告警信息,还可以得到正确的运行结果。

然而,不定长传参本身就不建议使用,何况这种 “奇巧淫技” 的手段呢!



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论