在C中使用varargs的一个例子

Rom*_*man 34 c syntax variadic-functions

在这里,我找到了如何在C中使用varargs的示例.

#include <stdarg.h>

double average(int count, ...)
{
    va_list ap;
    int j;
    double tot = 0;
    va_start(ap, count); //Requires the last fixed parameter (to get the address)
    for(j=0; j<count; j++)
        tot+=va_arg(ap, double); //Requires the type to cast to. Increments ap to the next argument.
    va_end(ap);
    return tot/count;
}
Run Code Online (Sandbox Code Playgroud)

我只能在某种程度上理解这个例子.

  1. 我不清楚为什么使用它们va_start(ap, count);.据我所知,通过这种方式我们将迭代器设置为它的第一个元素.但是为什么默认情况下它没有设置到开头?

  2. 我不清楚为什么我们需要count作为一个论点.C不能自动确定参数的数量?

  3. 我不清楚为什么使用它们va_end(ap).它有什么变化?它是否将迭代器设置为列表的末尾?但它是不是通过循环设置到列表的末尾?而且,为什么我们需要它呢?我们不再使用ap了; 我们为什么要改变它?

Som*_*ude 35

请记住,参数在堆栈上传递.该va_start函数包含"魔术"代码,用于va_list使用正确的堆栈指针初始化.它必须在函数声明中传递最后一个命名参数,否则它将不起作用.

什么va_arg是使用这个保存的堆栈指针,并为提供的类型提取正确的字节数,然后修改ap,使其指向堆栈上的下一个参数.


实际上,这些函数(va_start,va_argva_end)实际上不是函数,而是实现为预处理器宏.实际的实现还取决于编译器,因为不同的编译器可以具有不同的堆栈布局以及它如何在堆栈上推送参数.

  • 值得注意的是,在AMD64 ABI中,大多数参数都是在寄存器中传递的. (2认同)

Art*_*Art 7

但是为什么默认情况下它没有设置到开头?

也许是因为编译器不够聪明的历史原因.也许是因为你可能有一个varargs函数原型,它实际上并不关心varargs,并且设置varargs在特定系统上碰巧是昂贵的.也许是因为您执行的操作更复杂,va_copy或者您希望多次重新使用参数并多次调用va_start.

简短版本是:因为语言标准是这样说的.

其次,我不清楚为什么我们需要把数作为一个论点.C++不能自动确定参数的数量?

这不是那一切count.它是函数的最后一个命名参数.va_start需要它来弄清楚varargs在哪里.这很可能是出于旧编译器的历史原因.我不明白为什么今天不能以不同方式实施.

作为问题的第二部分:不,编译器不知道向函数发送了多少个参数.它甚至可能不在同一个编译单元甚至是同一个程序中,编译器也不知道如何调用该函数.想象一下具有类似varargs功能的库printf.编译libc时,编译器不知道程序何时以及如何调用printf.在大多数ABI上(ABI是如何调用函数,如何传递参数等的约定),没有办法找出函数调用得到多少个参数.将这些信息包含在函数调用中并且几乎不需要它是浪费的.因此,您需要有一种方法来告诉varargs函数它获得了多少参数.访问va_arg超出实际传递的参数数量是未定义的行为.

然后我不清楚为什么我们使用va_end(ap).它有什么变化?

在大多数架构va_end上没有做任何相关的事情.但是有一些架构具有复杂的参数传递语义,va_start甚至可能具有malloc内存,因此您需要va_end释放该内存.

这里的简短版本也是:因为语言标准是这样说的.

  • @trijezdci呃?参数的数量与第一个vararg参数有什么关系?我认为没有理由让编译器无法弄清楚`foo(int a,...)`varargs在`a`之后开始.这是第一个问题,在编译时是众所周知的.我在答案中实际提到的在编译时不知道的参数计数,所以我不明白为什么你觉得有必要重复它. (3认同)
  • @trijezdci你再次谈论论点计数,这完全是无关紧要的.关于你的第二个评论,可变参数列表的开始,它在编译时非常清楚,并且无法在任何其他点启动它.使用`foo(int a,...)`,很明显可变列表始终在`a`之后开始.C标准说"参数parmN是函数定义中变量参数列表中最右边参数的标识符(恰好在......之前).".每个编译器都知道哪个参数最右边,"就在...之前". (3认同)
  • 你在说什么?阅读实际问题."为什么需要va_start?" 是一个问题(转述),我列出了一些可能的原因,包括旧的编译器不够聪明,不知道函数中存在varargs(gcc约2.6左右不知道)并且还必须被告知它开始的地方(因为它没有跟踪那些信息).它根本与运行时无关. (3认同)
  • 这与“编译器不够聪明”完全无关。可变参数函数的性质是,传递的参数数量在运行时确定,但编译器在编译时运行。因此,任何编译器都不可能知道将传递给可变参数函数的参数数量。在编译时执行此操作的唯一方法是为参数计数保留一个自动“不可见”参数,并让编译器始终对其进行初始化,但 C 的设计者选择不以这种方式实现可变参数函数。 (2认同)

W.B*_*.B. 6

va_start初始化变量参数列表.您始终将最后一个命名函数参数作为第二个参数传递.这是因为您需要提供有关堆栈中位置的信息,其中变量参数开始,因为参数被压入堆栈,编译器无法知道变量参数列表的开头在哪里(没有区别).

对于va_end,它用于在va_start调用期间释放为变量参数列表分配的资源.