【问题标题】:Put a va_list variable inside... a variable argument list (!)将 va_list 变量放入...变量参数列表(!)
【发布时间】:2015-06-23 21:21:52
【问题描述】:

我想设计一个带有可变数量参数的函数,其中一个参数本身就是va_list;但是我的代码出了点问题,我不明白是什么......

警告——我的问题不是设计代码做我想做的事情(我找到了绕过问题的方法),而只是了解我做错了什么......

为了解释我的问题,让我们从一个简单的例子开始:即一个函数ffprintf,它的作用类似于fprintf,但将其内容写入多个字符串,其编号由@987654324的第一个参数表示@,其身份由下一个参数给出(这些参数的数量可能因一次调用而异,因此您必须使用可变参数列表)。这样的函数可以这样使用:

FILE *stream0, *stream1, *stream2;
int a, b;
ffprintf (3, stream0, stream1, stream2, "%d divided by %d worths %f", a, b, (double)a / b);

它的代码是:

void ffprintf (int z, ...)
 {va_list vlist, auxvlist;
  FILE **streams = malloc (z * sizeof(FILE *));
  va_start (vlist, z);
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist, FILE *); // Getting the next stream argument
   }
  char const *format = va_arg (vlist, char const *); // Getting the format argument
  for (int i = 0; i < z; ++i)
   {va_copy (auxvlist, vlist); // You have to work on a copy "auxvlist" of "vlist", for otherwise "vlist" would be altered by the next line
    vfprintf (streams[i], format, auxvlist);
    va_end (auxvlist);
   }
  va_end (vlist);
  free (streams);
 }

效果很好。现在,还有标准函数vfprintf,其原型是vfprintf (FILE *stream, char const* format, va_list vlist);,您可以像这样使用它来创建另一个具有可变参数列表的函数:

void fprintf_variant (FILE *stream, char const* format, ...)
 {
  va_list vlist;
  va_start (vlist, format);
  vfprintf (stream, format, vlist);
  va_end (vlist);
 }

这也很好用。现在,我的目标是将这两种想法结合起来创建一个函数,我将其命名为vffprintf,您可以像这样使用它:

FILE *stream0, *stream1, *stream2;
void fprintf_onto_streams012 (char const *format, ...)
 {va_list vlist;
  va_start (vlist, format);
  vffprintf (3, stream0, stream1, stream2, format, vlist);
  va_end (vlist);
 }

我设计了如下代码:

void vffprintf (int z, ...)
 {va_list vlist, auxvlist, auxauxvlist;
  va_start (vlist, z);
  FILE **streams = malloc (z * sizeof(FILE *));
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist, FILE *);
   }
  char const *format = va_arg (vlist, char const *);
  va_copy (auxvlist, va_arg (vlist, va_list)); // Here I get the next argument of "vlist", knowing that this argument is of "va_list" type
  for (int i = 0; i < z; ++i)
   {va_copy (auxauxvlist, auxvlist);
    vfprintf (streams[i], format, auxvlist);
    va_end (auxauxvlist);
   }
  va_end (auxvlist);
  va_end (vlist);
  free (streams);
 }

这段代码可以顺利编译,但不能正常工作...例如,如果我编写以下完整代码:

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

void vffprintf (int z, ...)
 {va_list vlist, auxvlist, auxauxvlist;
  FILE **streams = malloc (z * sizeof(FILE *));
  va_start (vlist, z);
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist, FILE *);
   }
  char const *format = va_arg (vlist, char const *);
  va_copy (auxvlist, va_arg (vlist, va_list));
  for (int i = 0; i < z; ++i)
   {va_copy (auxauxvlist, auxvlist);
    vfprintf (streams[i], format, auxauxvlist);
    va_end (auxauxvlist);
   }
  va_end (auxvlist);
  va_end (vlist);
  free (streams);
 }

void printf_variant (char const *format, ...)
 {va_list vlist;
  va_start (vlist, format);
  vffprintf (1, stdout, format, vlist);
  va_end (vlist);
 }

int main (void)
 {printf_variant ("Ramanujan's number is %d.\n", 1729);
  return 0;
 }

我得到一个段错误!...为什么?!

P.-S.:很抱歉这个很长的问题;但我希望它非常清楚,因为它相当技术性......

P.-S.2:对于这个问题,我特意使用了标签“va-list”和“variableargumentlists”,因为我感兴趣的是va_list被视为一种类型,在里面一个(其他)变量参数列表,视为一个列表...所以这里实际上是两个不同的概念。

【问题讨论】:

  • 那么问题是为什么最后一个例子失败了?
  • 是的,就是这样:-)
  • 如果忽略内存泄漏,看起来还不错。
  • FILE **streams = malloc (z * sizeof(FILE *));
  • 糟糕!我什至不知道我怎么会犯这样的错误...... :-S 更正它。

标签: c variadic-functions


【解决方案1】:

C11(N1570)终稿中va_arg的描述包含(type为第二个参数):

如果 type 与实际下一个参数的 type 不兼容(根据默认参数提升),则行为未定义

va_list 允许为数组类型(标准要求它是所谓的“完整对象类型”),您的实现似乎利用了这种可能性。您可能知道,在 C 中,数组不能作为参数传递,因为它们会衰减为指针,并且此类指针的类型与原始数组类型不兼容。

例如:int *int [1] 不兼容。因此,如果您确实需要可移植地传递一个数组或 va_list,则使用 va_list 成员定义一个 struct 并传递它(参见 Why can't we pass arrays to function by value?)。

【讨论】:

  • 你好 cremno,我没有掌握所有细节,但我的印象是你的观点与标准中的以下条款有关:“参数 type 是指定的类型名称,因此只需将* 添加到type 即可获得指向具有指定类型的对象的指针的类型。”。但没有什么能保证va_list 会是这样的类型,例如数组类型不起作用...正确吗?另一方面,这似乎并不能解释 Eric Tsui 的“void*”技巧是如何起作用的......
  • @Nancy-N:这是一个技巧。但他必须解释这一点,因为这是他回答的一部分。它可能适用于您所关心的任何地方。这并没有改变它是 UB 的事实(他发布的描述也是错误的)。 Here 是一份 GCC 错误报告,其记者想要做一些与您相似甚至相同的事情。它甚至提到了 C90 中的一个边缘案例!也许阅读它会比我的回答更清楚一些。它还提到使用结构作为一种可能的解决方案。
  • @cremno,感谢您的更正。所以,我对va_listva_list*在不同的va_list实现下做了更多的研究,这确实是问题所在。
  • @cremno:感谢您的进一步解释。多亏了它和 Eric Tsui 的帖子,我现在才明白出了什么问题。
  • @cremno 我不确定将va_list 包装在结构中并将其传递给函数是否正确。正如您所说,它可能是数组类型,因此它是通过引用隐式传递的;而 struct wrap 选项将按字节复制。 va_copy 应该在应该复制 va_list 的任何时候使用,所以这可能会破坏列表。
【解决方案2】:
void vffprintf (int z, ...)
 {
  //...
  va_copy (auxvlist, va_arg (vlist, va_list));//this line has the problem 
   //...
 } 

只需像这样快速而棘手的方法,它就会起作用。

  void vffprintf (int z, ...)
 {
  //...
  va_copy (auxvlist, va_arg (vlist, void*));
   //...
 }

这里有一些关于var_argva_list 的参考资料,应该提供了详细而透彻的解释。

1) Pass va_list or pointer to va_list?

2) Is GCC mishandling a pointer to a va_list passed to a function?

3)What is the format of the x86_64 va_list structure?

希望他们有所帮助。

【讨论】:

  • 确实有效;了不起! :-D 如果你有更详细的解释,我很想听听:为什么上一行失败了?为什么新的有效?这个“void* 技巧”是可移植的吗?...
  • 我还不清楚。当printf_variant调用vffprintf时,vffprintf的变量参数列表为{stdout,format,vlist_0},其中vlist_0printf_variant的变量参数列表,即{@987654337 @}。所以,当调用va_arg (vlist_1, va_list)时(我给_0_1加上了后缀,以明确vlist之间的区别),vlist_1的下一个参数是{1929},具体可能是struct {char *, int}。但这不是一个 int,是吗?...无论如何,它怎么可能转换为 void *?!很抱歉没有更容易理解...
  • @Nancy-N,我试图做的是这样的va_copy (auxvlist, va_arg (vlist, va_list*)),但它给出了警告。所以,我把它改成va_copy (auxvlist, va_arg (vlist, void*))
  • @Nancy-N 谢谢。我也从这个问题中学到了更多。我发布了一些有用的参考资料,特别是第二个链接Is GCC mishandling a pointer to a va_list passed to a function?
  • @Eric Tsui - 感谢您的补充解释和参考;现在我想我明白了。
【解决方案3】:

如果你想使用 va_arg() 检索它,你可能需要将类型 va_list 包装到一个结构中:

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

typedef struct
{
    va_list list ;  
} my_list ;

void vffprintf (int z, ...)
 {my_list vlist, auxvlist, auxauxvlist;
  FILE **streams = malloc (z * sizeof(FILE *));
  va_start (vlist.list, z);
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist.list, FILE *);
   }
  char const *format = va_arg (vlist.list, char const *);
  my_list parent = va_arg (vlist.list, my_list) ;
  va_copy (auxvlist.list, parent.list );
  for (int i = 0; i < z; ++i)
   {va_copy (auxauxvlist.list, auxvlist.list);
    vfprintf (streams[i], format, auxauxvlist.list);
    va_end (auxauxvlist.list);
   }
  va_end (auxvlist.list);
  va_end (vlist.list);
  free (streams);
 }

void printf_variant (char const *format, ...)
 {my_list vlist;
  va_start (vlist.list, format);
  vffprintf (1, stdout , format, vlist);
  va_end (vlist.list);
 }

int main (void)
 {printf_variant ("Ramanujan's number is %d.\n", 1729 );
  return 0;
 }

问题源于数组和同类型指针不兼容,va_list定义为数组。然后您尝试获取该类型:

va_arg (vlist, va_list)

所以你告诉va_arg 你得到了一个数组,但实际上传递的va_list 已经衰减为一个指针。你应该使用va_list的指针版本,但是你不知道va_list的真正定义,所以你无法获得它的指针版本。

解决方案是将va_list 包装成一个你控制的类型,一个结构体。

【讨论】:

  • @this - 感谢您以这种方式绕过问题(我不知道)并写下代码! :-)
  • @Nancy-N 那么这段代码真的解决了问题吗?
  • @ this - 确实如此——至少在我自己的机器上 ;-)
猜你喜欢
  • 2014-09-15
  • 2013-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-10
  • 2017-05-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多