我对 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);
}
更新我可以更改我的构造函数以获取变量模式而不是它们的数量,所以这是修改后的代码:
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 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 %sn", s);
break;
case 'd':
d = va_arg(ap, int);
printf("int %dn", d);
break;
case 'c':
/* need a cast here since va_arg only
takes fully promoted types */
c = (char) va_arg(ap, int);
printf("char %cn", 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对这个问题的评论,关键点是我们需要一种方法来描述每个可变参数的类型。(在 C 语言中,类型信息基本上在编译时被丢弃,所以如果我们想为一个参数支持多种类型,我们还必须显式传递其类型:(可变参数函数参数的)类型在运行时根本不存在。
上面,第一个参数fmt
是一个字符串,其中每个字符描述一个可变参数(通过描述其类型)。因此,可变参数的数量应该与fmt
字符串中s
、d
或c
字符的数量相同。
printf 系列函数和 scanf 系列函数都使用%
来指示可变参数,后跟该参数的格式详细信息和类型规范。由于它们支持的格式相当复杂,实现这些格式的代码比上面的示例复杂得多,但逻辑非常相似。
在问题的更新中,OP 询问函数是否可以更改可变参数的值 - 或者更确切地说,可变参数指向的值,类似于 scanf() 函数系列的工作方式。
由于参数是按值传递的,并且va_arg()
生成参数的值,而不是对参数的引用,因此我们对值本身所做的任何修改(在上面的foo()
函数示例中s
、d
或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()
表达式中。(我还建议将d
、c
和s
分别重命名为dptr
、cptr
和sptr
,以帮助提醒我们阅读代码的人类,它们不再是值本身,而是指向我们希望修改的值的指针。我省略了此更改,以使函数尽可能与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
关键字(请参阅此处的答案),可以与预处理器宏结合使用,以根据参数的类型在表达式之间进行选择。
因为它在早期版本的标准中不存在,我们现在必须使用例如sin()
、sinf()
和sinl()
来返回其参数的正弦值,这取决于参数(和期望的结果)是double
、float
还是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()
表达式的计算结果为sinl
、sinf
或sin
之一;最终(x)
使宏计算为函数调用,并将宏参数x
作为函数参数。
这与本答案前面的部分并不矛盾。即使使用_Generic
关键字,也会在编译时检查类型。它基本上只是宏参数类型比较检查之上的语法糖,有助于编写特定于类型的代码;换句话说,一种开关。case 语句,它作用于预处理器宏参数类型,在每种情况下只调用一个函数。
此外,_Generic
并不能真正与可变参数函数一起使用。特别是,您不能根据这些函数的任何可变参数进行选择。
但是,使用的宏很容易看起来像可变参数函数。如果你想进一步探索这些泛型,请参阅我前段时间写的这个"答案"。