[本文最初发表于 1992 年 7 月 14 日。 我对本文进行了略微编辑以适应此网页。]

新闻组:comp.lang.c
发件人:scs@adam.mit.edu (Steve Summit)
主题:回复:可变参数函数调用可变参数函数?
消息 ID:<1992Jul14.214625.15684@athena.mit.edu>
引用:<1992Jul9.171606.24625@shearson.com> <1992Jul10.015616.12264@infodev.cam.ac.uk>
日期:周二,1992 年 7 月 14 日 21:46:25 GMT

在文章 <1992Jul9.171606.24625@shearson.com> 中,Frank Greco (fgreco@shearson.com) 写道
> 有人有 C 函数使用 varargs 的例子吗
> 调用另一个使用相同 args 的 C 函数?
> ...
> varargs(3) 的手册页说
>
> 参数列表(或其剩余部分)可以传递给
> 另一个函数,使用指向类型的变量的指针
> va_list()...
>
> 它们是什么意思 "指向 va_list() 类型的变量的指针? 我尝试了
> 几种组合,并总线错误退出...

在文章 <1992Jul10.015616.12264@infodev.cam.ac.uk> 中,Colin Bell (crb11@cus.cam.ac.uk) 写道
> 我解决了这个问题,但我自己有一个相关的问题。 我有一个处理系统消息的函数: 即,将一个参数列表发送给
> fprintf 前面有两个或三个参数,告诉函数如何处理生成的结果
> 字符串。 我想做的是从前面剥离这些参数,然后传递
> 整个剩余部分给 sprintf,并处理生成的结果字符串。
>

这些问题的初步答案可以在 comp.lang.c 常见问题解答 (FAQ) 列表中找到

6.2:
我如何编写一个像 printf 这样的函数,它接受一个格式字符串和可变数量的参数printf,并将它们传递给printf来完成大部分工作?

A
使用vprintf, 但没有vsprintf.

这是一个 "函数(来自第 15.5 题)的一个版本,使用了" 例程,它打印一条错误消息,前面带有字符串 "error: " 并以换行符结尾

[示例代码已删除]

引用:K&R II 第 8.3 节,第 174 页,第 B1.2 节,第 245 页;H&S 第 17.12 节,第 337 页;ANSI 第 4.9.6.7、4.9.6.8、4.9.6.9 节。

[此问题的当前版本,现在编号为 15.5]

6.4:
我如何编写一个接受可变数量参数的函数,并将它们传递给另一个函数(该函数也接受可变数量的参数)?

A
一般来说,你不能这样做。 你必须提供该其他函数的一个版本,该版本接受一个va_list指针,就像但没有在上面的例子中。 如果参数必须直接作为实际参数(而不是通过一个va_list指针)传递给另一个本身就是可变参数的函数(对于该函数,你没有选择创建备用的、va_list- 接受的版本),则没有可移植的解决方案。(可以通过求助于特定于机器的汇编语言来解决该问题。)

[此问题的当前版本,现在编号为 15.12]

现在,关于这些问题当然可以说的更多。 在接受格式字符串和一些可变参数以及一些其他参数的情况下,将格式字符串和可变参数传递给vsprintf,然后对产生的字符串进行一些处理vsprintf和其他参数,存在一个实际问题,因为vsprintf的返回缓冲区无法轻易确定合适或保证足够的大小。(如何尝试这样做本身就是一个常见问题,FAQ 列表可能应该解决这个问题 [现在解决了]。 我今天不会再多说了,除了遗憾地是,不幸的是,没有好的、可移植的解决方案。)

Frank Greco 问道,“他们是什么意思 `指向 va_list() 类型的变量的指针`?”。 他的手册中显然有一个错字,但可变长度参数列表与指向相同列表的 "指针" 的问题可能会令人困惑。 这是我对它们的看法,摘自今年春天我与某人就 FAQ 列表中的问题 6.4 进行的电子邮件讨论va_list()'?". There's apparently a typo in his manual, but the issue of variable-length argument lists versus "pointers" to same is a potentially confusing one. Here is how I think about them, extracted from an e-mail discussion I had last Spring with someone about question 6.4 in the FAQ list

*       *       *

...人们经常谈论 "可变长度参数列表的可变长度部分",这有点愚蠢,但区分可变长度部分和固定部分通常很重要,在 ANSI C 中,固定部分必须始终存在。(例如,printf始终取一个char *参数 -- "固定" 部分 -- 后面跟 0 个或多个任意类型的参数。)

*       *       *

[将一个va_list指针传递给另一个例程] 是首选方法,但请注意,接受单个va_list指针的函数根本不是可变参数的。(就 C 而言,它只接受一个参数。)

这就是(公认相当晦涩的)FAQ 列表措辞所暗示的解决方案

你必须提供该其他函数的一个版本,该版本接受一个va_list指针,就像但没有在上面的例子中。

也就是说,问题 "我可以编写一个debug_printf()使用printf()吗?" 的答案是 "否。"printf()是可变参数的,但是vprintf等函数取va_list指针,因此你可以编写一个debug_printf()来使用它们之一。

*       *       *

"可变参数" 意味着(至少我使用它)非常具体地,一个子程序,正如 C 编译器所看到的那样,接受可变数量的形式参数。(有时,当我们必须非常仔细地区分子程序设置为接收的通用事物与特定子程序调用传递给它的 "实际参数" 时,会使用术语 "形式参数"。)

很容易证明,不能用运行时变化的参数列表来调用可变参数函数(如果想使用另一个可变参数函数刚刚接收的可变长度参数列表,就需要这样做)。 C 编译器生成子程序调用的唯一方法是编写以下形式的表达式

function ( argument-list )

其中function是一个求值为函数的表达式(函数的名称,或指向函数的指针的 "内容"),而argument-list是一个由 0 个或多个逗号分隔的表达式组成的列表。 显然,由于它们出现在源代码中,因此必须有固定数量的参数(在任何一个调用中)。

*       *       *

[当一个可变参数函数尝试将可变参数传递给另一个函数时],第一个函数是否进行任何额外的 "处理" 并不重要,第二个函数是否使用stdarg.h并不重要(还有其他不太可移植的方法来访问可变长度参数列表,并且第二个函数甚至可能不是用 C 编写的)。 关键问题是,一个可变参数函数是否可以调用另一个可变参数函数(使用我使用的 "可变参数" 的严格解释),并将它在一个特定调用中接收到的(不确定数量的)参数传递给第二个函数。[再次,答案是否定的。]

如果我修复了这个问题,我将引入以下解释,如果它们还不清楚,应该有助于澄清你的想法。

正如我们可以使用整数和指向整数的指针一样,在 C 中也有两种操作可变长度参数列表的方法。 诸如printf接受可变长度参数列表 -- 我们无法确切地知道printf将被调用多少个参数。 但是,我们也可以设想一个 "指向可变长度参数列表的指针",这正是va_list是。

当处理指针时,必须非常小心地区分指针和指针指向的内容。 当打算使用指针指向的值时,尝试使用指针值总是错误的。 显式语法,例如一元*运算符,用于解引用指针,即访问指针指向的值。

同样,使用指向可变长度参数列表的指针来代替实际的可变长度参数列表,反之亦然,也是毫无意义和/或不可能的。[例如,你不能传递一个va_listprintf,也不能用可变数量的参数调用vprintf。]

现在,碰巧的是,在 C 中,我们作为程序员对可变长度参数列表(或固定长度参数列表)没有太多的控制权和自由。 特别是,我们无法创建可变长度参数列表,也无法从可变长度参数列表中挑选出一个参数。

(我们只能对固定长度参数列表做更多的事情。 我们只能通过在一段源代码中编写一个子程序调用表达式来 "创建" 一个固定长度参数列表,并且我们只能通过在一个子程序定义的上下文中命名一个参数,并通过名称来引用它,才能从固定长度参数列表中挑选出一个参数。)

我们作为程序员,可以对可变长度参数列表做的唯一事情是

  1. 给定一个可变长度参数列表,使用va_start宏生成一个指向它的指针。(在此分析下,va_start类似于一元&运算符。)

  2. 给定一个指向可变长度参数列表的指针(即一个va_list),从中挑选出一个参数(具有特定类型)。

但是,我们可以像操作 C 中的其他对象一样,几乎可以自由地操作一个va_list(指向可变长度参数列表的指针)。 特别是,我们可以将一个va_list传递给另一个子程序。

然而,既没有我们可以声明 "类型" 为 "可变长度参数列表" 的 "对象" 的语法,也没有一种机制可以使用特定值(即使用特定的可变长度参数列表)来初始化一个对象。 因此,我们无法在一个可变参数函数中接受一个可变长度参数列表,并将其(作为可变长度参数列表)传递给另一个可变参数函数。 我们可以做的是获取一个指向可变长度参数列表的指针(一个va_list),使用va_start,并将该指针(作为单个参数)传递给另一个函数;然而,另一个va_list- 接受的函数通常不是可变参数的。

可变参数函数的例子,即接受可变长度参数列表的函数,是printf, fprintf,而sprintf。 非可变参数函数的例子,即接受指向可变长度参数列表的指针作为单个va_list的函数,是vprintf, 但没有,而vsprintf.

作为一般建议,每当编写一个接受可变数量参数的函数时,最好编写一个伴随例程,该例程接受单个va_list。 编写这两个例程并不比只编写一个例程花费更多的工作,因为第一个可变参数函数可以用第二个函数非常简单地实现 -- 它只需要调用va_start,然后将结果va_list传递给第二个函数。

通过实现一对函数,你为某人(你或其他人)稍后 "在之上" 构建附加功能打开了大门,方法是编写另一个可变参数函数(或者更好的是,一对可变参数和非可变参数函数),该函数调用第二个、va_list- 接受的、较低级别的函数,并执行一些额外的处理。

如果你只编写一个可变参数函数,以后试图在其之上实现附加功能的程序员会发现自己处于与过去试图编写诸如debug_printf()在 v*printf 系列发明之前的日子里:他们没有可移植的方法来 "连接" 到 *printf 系列提供的核心功能;他们不得不使用不可移植的技巧来调用printf,以及(至少部分)高级函数接收到的(可变)参数,或者他们不得不完全放弃printf并重新实现其部分或全部功能。

显然,对于 FAQ 列表来说,这太冗长且重复了,但这是一个很好的初稿,我可以尝试从中提炼和提取本质。(它的概念模型也存在轻微的不完善 -- 虽然它暗示了 "`可变长度参数列表` 类型的对象",但实际上并没有 "`可变长度参数列表`" 类型的值。 所有实际参数列表显然都有一定数量的参数,因此 "`可变长度参数列表` 类型的对象" 只是一个可以容纳任意固定长度参数列表的对象,无论它们有多长。)

史蒂夫·萨米特
scs@eskimo.com