如何写入传递给 C 中 Variadic 函数的变量

How to write to variables passed to the Variadic function in C

我是 C 的新手,我想知道是否可以创建可变参数函数并将变量指针传递给它并将数据写入变量?

我正在寻找的一个明显例子是 scanf 函数,它从标准输入中获取输入并将其写入变量。

这是我想做的一个示例:

void fun(int num, ...){
//    insert 2 in a and "abc" in b
}

int main(void){
    int a;
    char *b;
    fun(2, &a, &b);
}

update 我可以改变我的构造函数来获取变量模式而不是它们的数量,所以这里是修改后的代码:

void fun(char *fmt, ...){
//    insert 2 in a and "abc" in b
}

int main(void){
    int a;
    char *b;
    fun("dc", &a, &b);
}

stdarg man page (man 3 stdarg) 中显示的示例代码开始。为了可读性稍作修改,并添加了一个简单的 main():

#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>

void foo(char *fmt, ...)
{
   va_list ap;
   int d;
   char c, *s;

   va_start(ap, fmt);

   while (*fmt) {
       switch (*(fmt++)) {

       case 's':
           s = va_arg(ap, char *);
           printf("string %s\n", s);
           break;

       case 'd':
           d = va_arg(ap, int);
           printf("int %d\n", d);
           break;

       case 'c':
           /* need a cast here since va_arg only
              takes fully promoted types */
           c = (char) va_arg(ap, int);
           printf("char %c\n", c);
           break;
       }
   }

   va_end(ap);
}


int main(void)
{
    char *s1 = "First";
    char *s2 = "Second";
    int   d = 42;
    char  c = '?';

    foo("sdcs", s1, d, c, s2);

    return EXIT_SUCCESS;
}

如果你编译运行上面的,它会输出

string First
int 42
char ?
string Second

正如 liliscent 和 Jonathan Leffler 对问题的评论,关键是我们需要一种方法来描述每个可变参数的 type。 (在 C 中,类型信息在编译时基本上被丢弃,所以如果我们想为一个参数支持多种类型,我们还必须显式地传递它的类型:(可变函数参数的)类型根本不存在于 运行 时间了。)

上面,第一个参数 fmt 是一个字符串,其中每个字符描述一个可变参数(通过描述其类型)。因此,预计可变参数的数量与 fmt 字符串中的 sdc 个字符相同。

printf family of functions and the scanf family of functions 都使用 % 来指示可变参数,后跟该参数的格式详细信息和类型规范。由于它们支持相当复杂的格式,实现这些的代码比上面的例子复杂得多,但逻辑非常相似。


在问题的更新中,OP 询问函数是否可以更改可变参数的值——或者更确切地说,可变参数指向的值,类似于 scanf() 函数族工作。

因为参数是按值传递的,并且 va_arg() 产生参数的值,而不是对参数的引用,我们在本地对值本身进行的任何修改(到 sd,或上面 foo() 函数示例中的 c)将对调用者不可见。但是,如果我们将指针传递给值——就像 scanf() 函数那样——,我们可以修改指针指向的值。

考虑对上述 foo() 函数稍加修改的版本,zero():

void zero(char *fmt, ...)
{
   va_list ap;
   int *d;
   char *c, **s;

   va_start(ap, fmt);

   while (*fmt) {
       switch (*(fmt++)) {

       case 's':
           s = va_arg(ap, char **);
           if (s)
               *s = NULL;
           break;

       case 'd':
           d = va_arg(ap, int *);
           if (d)
               *d = 0;
           break;

       case 'c':
           /* pointers are fully promoted */
           c = va_arg(ap, char *);
           if (c)
               *c = 0;
           break;
       }
   }

   va_end(ap);
}

注意与 foo() 的区别,尤其是在 va_arg() 表达式中。 (我还建议将 dcs 分别重命名为 dptrcptrsptr,以帮助提醒我们阅读代码的人不再是值本身,而是指向我们希望修改的值的指针。我省略了此更改以保持函数尽可能类似于 foo(),以便于比较两者函数。)

有了这个,我们可以做例子

    int d = 5;
    char *p = "z";

    zero("ds", &d, &p);

d会清零,p会变成NULL

我们也不限于每个案例中的单个 va_arg()。例如,我们可以将上面的代码修改为每个格式化字母采用两个参数,第一个是指向参数的指针,第二个是值:

void let(char *fmt, ...)
{
   va_list ap;
   int *dptr, d;
   char *cptr, c, **sptr, *s;

   va_start(ap, fmt);

   while (*fmt) {
       switch (*(fmt++)) {

       case 's':
           sptr = va_arg(ap, char **);
           s = va_arg(ap, char *);
           if (sptr)
               *sptr = s;
           break;

       case 'd':
           dptr = va_arg(ap, int *);
           d = va_arg(ap, int);
           if (dptr)
               *dptr = d;
           break;

       case 'c':
           cptr = va_arg(ap, char *);
           /* a 'char' type variadic argument
              is promoted to 'int' in C: */
           c = (char) va_arg(ap, int);
           if (cptr)
               *cptr = c;
           break;
       }
   }

   va_end(ap);
}

您可以通过例如

使用最后一个功能
int a;
char *b;

let("ds", &a, 2, &b, "abc");

a = 2; b = "abc";效果相同。注意我们没有修改b指向的数据;我们只是将 b 设置为指向文字字符串 abc.


在 C11 及更高版本中,有一个 _Generic 关键字(参见 this answer here),可以与预处理器宏结合使用,根据类型在表达式之间进行选择参数。

因为在早期版本的标准中不存在,我们现在不得不使用例如sin()sinf()sinl()到return的正弦他们的论点,取决于论点(和期望的结果)是 doublefloat 还是 long double。在C11中,我们可以定义

#define Sin(x) _Generic((x),               \
                        long double: sinl, \
                        float: sinf,       \
                        default: sin)(x)

这样我们就可以调用 Sin(x),编译器会选择合适的函数变体:Sin(1.0f) 等价于 sinf(1.0f)Sin(1.0) 等价于 sin(1.0),例如。

(上面,_Generic() 表达式计算为 sinlsinfsin 之一;最后的 (x) 使宏计算为以宏参数 x 作为函数参数的函数调用。)

这与本回答的前面部分并不矛盾。即使在使用 _Generic 关键字时,也会在编译时检查类型。它基本上只是 syntactic sugar 在宏参数类型比较检查之上,有助于编写 type-specific 代码;换句话说,一种作用于预处理器宏参数 types 的 switch..case 语句,在每种情况下只调用一个函数。

此外,_Generic 不适用于可变参数函数。特别是,您不能根据这些函数的任何可变参数进行选择。

但是,使用的宏很容易看起来像可变参数函数。如果您想进一步探索此类泛型,请参见例如 "answer" 前段时间写过