自定义函数与vprintf类库函数间的va_list合规共享问题
问题背景
我想给带可变参数的格式化打印加特殊处理,却发现代码里va_list的用法不符合C标准。虽然GCC编译没报错,但我希望代码能符合标准要求,以下是简化后的示例代码:
#include <stdarg.h> #include <stdio.h> typedef struct S_ { int i; va_list *v; } S; void fb( S* s ) { int x = va_arg( *s->v, int ); // Line A vfprintf( stdout, "c:%c s:%s\n", *s->v ); // Line B int y = va_arg( *s->v, int ); // Line C fprintf( stdout, "d:%d\n", s->i * x * y ); } void fa( int i, ... ) { S s = { i }; va_list v; va_start( v, i ); s.v = &v; fb( &s ); va_end( v ); } void main() { fa( 1, 2, 'c', "string", 3 ); }
C标准相关描述:
若需访问可变参数,被调用函数应声明一个va_list类型的对象(本小节通常称为ap)。该对象ap可作为参数传递给另一函数;若该函数以ap为参数调用va_arg宏,则调用函数中ap的值将变得不确定,且在进一步引用ap之前必须将其传递给va_end宏。(允许创建指向va_list的指针并将该指针传递给另一函数,这种情况下,原函数可在另一函数返回后继续使用原参数列表。)
我的理解:Line A之后调用Line B没问题,因为fb先调用va_arg修改了va_list的状态,再传给vfprintf,原va_list的状态变化会反映到传入的对象中。但Line B之后的Line C不符合标准,因为vfprintf接收的是va_list值而非指针,调用后原s->v的值会变得不确定,不能再像Line C那样直接使用。推测原因是s->v会被复制到vfprintf的局部变量里,vfprintf消耗参数(一个char和一个char*)的操作只会修改副本,不会同步到原对象,导致y无法正确取到3,可能得到错误值。
现在的难题是我没法修改vfprintf让它接收va_list*,但又需要在同一个va_list上混合调用va_arg和vfprintf(也就是vfprintf前后都要用va_arg)。请问我的分析是否正确?代码确实不符合标准吗?如果是,该怎么修复?
回答
1. 你的分析完全正确,代码确实不符合C标准
根据C标准的描述,当你把va_list对象按值传递给函数(比如vfprintf),如果该函数内部调用了va_arg,那么原va_list对象的状态就会变得不确定。这是因为va_list的实现可能是指针,也可能是包含多成员的结构体:
- 若为指针,按值传递后函数内部修改的是副本指针,但原指针指向的参数位置是否被修改无法保证;
- 若为结构体,按值传递后函数内部仅修改副本,原结构体状态完全不变,直接导致vfprintf消耗参数的操作无法同步到原va_list,Line C的va_arg会取到错误值。
无论哪种实现,标准明确要求此时原va_list的状态已不确定,继续调用va_arg属于未定义行为,你的代码确实违反了C标准。
2. 修复方案:使用va_copy创建va_list副本
C标准提供了va_copy宏,可创建一个与原va_list状态完全一致的副本。我们可以用这个副本来传给vfprintf,原va_list的状态就不会被vfprintf的调用影响,后续仍可正常使用原va_list调用va_arg。
修改后的代码如下:
#include <stdarg.h> #include <stdio.h> typedef struct S_ { int i; va_list *v; } S; void fb( S* s ) { int x = va_arg( *s->v, int ); // Line A: 修改原va_list状态,指向第一个可变参数之后 va_list v_copy; va_copy(v_copy, *s->v); // 创建原va_list的副本 vfprintf( stdout, "c:%c s:%s\n", v_copy );// Line B: 使用副本传给vfprintf,不影响原va_list va_end(v_copy); // 副本使用完必须调用va_end,符合标准要求 int y = va_arg( *s->v, int ); // Line C: 原va_list状态未被修改,能正确取到3 fprintf( stdout, "d:%d\n", s->i * x * y ); } void fa( int i, ... ) { S s = { i }; va_list v; va_start( v, i ); s.v = &v; fb( &s ); va_end( v ); } int main() // 修正:main函数的标准返回类型为int,而非void { fa( 1, 2, 'c', "string", 3 ); return 0; }
修复说明
va_copy(v_copy, *s->v):在调用vfprintf前复制当前va_list状态,确保vfprintf对副本的修改不会影响原va_list;- 副本使用完毕后必须调用
va_end,这是C标准的强制要求,避免潜在的资源泄漏; - 原va_list的状态在Line A后被完整保留,Line C的va_arg可以正确获取到预期的参数3;
- 顺带修正了原代码中
void main()的非标准写法,改为符合C标准的int main()。
内容的提问来源于stack exchange,提问作者z32a7ul

