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

Yas*_*har 1 c stdin pointers variadic-functions

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

我正在寻找的一个明显的例子是scanf从stdin获取输入并将其写入变量的函数.

以下是我想要做的示例:

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

int main(void){
    int a;
    char *b;
    fun(2, &a, &b);
}
Run Code Online (Sandbox Code Playgroud)

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

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

int main(void){
    int a;
    char *b;
    fun("dc", &a, &b);
}
Run Code Online (Sandbox Code Playgroud)

Nom*_*mal 6

stdarg手册页(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;
}
Run Code Online (Sandbox Code Playgroud)

如果编译并运行上面的内容,它将输出

string First
int 42
char ?
string Second
Run Code Online (Sandbox Code Playgroud)

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

上面,第一个参数fmt是一个字符串,其中每个字符描述一个可变参数(通过描述其类型).因此,预计将相同数量的可变参数,因为有s,d,或c的字符fmt的字符串.

函数printf系列功能scanf系列都使用%指示可变参数的参数,其次是争论的格式细节和型号规格.由于它们支持非常复杂的格式化,实现它们的代码比上面的示例复杂得多,但逻辑非常相似.


在对问题的更新中,OP询问函数是否可以更改可变参数的值 - 或者更确切地说,变量参数指向的值,类似于scanf()函数系列的工作方式.

因为参数由值来传递,并va_arg()产生该参数的值,而不是参数的引用,任何修改我们使该值本身局部地(至s,dcfoo()上面的函数的例子)将不会给调用者可见.但是,如果我们传递指向值的指针 - 就像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);
}
Run Code Online (Sandbox Code Playgroud)

注意差异foo(),特别是在va_arg()表达式中.(我也建议重命名d,csdptr,cptrsptr,分别帮助提醒我们人类阅读,他们不再是值本身的代码,而是指向我们要修改的值.我省略了这种变化,以保持功能尽可能相似foo(),以便比较这两个功能.)

有了这个,我们可以做到

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

    zero("ds", &d, &p);
Run Code Online (Sandbox Code Playgroud)

并且d将被清零,并pNULL.

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);
}
Run Code Online (Sandbox Code Playgroud)

您可以通过例如最后一个功能使用

int a;
char *b;

let("ds", &a, 2, &b, "abc");
Run Code Online (Sandbox Code Playgroud)

效果与...相同a = 2; b = "abc";.请注意,我们不会将数据b点修改为; 我们只是b指向一个文字字符串abc.


在C11及更高版本中,有一个_Generic关键字(参见此处的答案),可以与预处理器宏一起使用,根据参数的类型在表达式之间进行选择.

因为它没有在早期版本的标准已经存在,我们现在必须使用例如sin(),sinf()sinl()返回其参数的正弦,根据不同的参数(和期望的结果)是否是一个double,float或者一个long double.在C11中,我们可以定义

#define Sin(x) _Generic((x),               \
                        long double: sinl, \
                        float: sinf,       \
                        default: sin)(x)
Run Code Online (Sandbox Code Playgroud)

所以我们可以调用Sin(x),编译器选择适当的函数变量:Sin(1.0f)等同于sinf(1.0f),Sin(1.0)等同于sin(1.0),例如.

(以上,对_Generic()表达式求之一sinl,sinfsin;最终(x)使宏评估为与宏参数的函数调用x作为函数参数.)

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

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

但是,使用的宏很容易看起来像可变函数.如果你想进一步探索这些泛型,请参阅我前段时间写的这个 "答案".