本文目录
本文是自己在使用<stdarg.h>头文件时遇到的一些问题和总结
stdarg.h 介绍
<stdarg.h> 是 C 语言处理可变参数的标准头文件。C17 及更早版本要求可变参数函数在至少一个具名参数后使用省略号 ...;C23 也允许参数列表只有 ...。头文件定义了一个类型和若干宏(不是普通函数):
va_list:保存遍历状态的类型;它的具体表示由实现决定,不能把它当成普通列表操作。va_start(ap, last_arg):C17 及更早版本用最后一个具名参数初始化ap。C23 允许写成va_start(ap);为兼容旧代码,多传的参数仍可保留,但不再参与初始化。va_arg(ap, type):按调用约定读取下一个参数。除标准列出的少数例外外,type必须与下一个实参经过默认实参提升后的类型兼容:float会提升为double,较窄的整数类型会进行整数提升(通常成为int,必要时成为unsigned int)。va_copy(dest, src):复制一份可独立推进的遍历状态。不能假定va_list可以用普通赋值正确复制。va_end(ap):结束遍历;每次成功执行va_start或va_copy后都应配对调用。
在 C17 及更早版本中,传给 va_start 的最后一个具名参数还不能声明为 register、函数或数组类型,其类型也必须与默认实参提升后的类型兼容;否则行为未定义。本文示例中的 int childnum 满足这些约束。
stdarg.h 的使用
实例
在多叉树的实现中,下面的函数用于一次加入多个子节点:
void AddChildren(Node* parent, int childnum, ...)
{
if (parent == NULL) return;
va_list child_list;
va_start(child_list, childnum);
for (int i = 0; i < childnum; i++) {
parent->children[parent->child_ptr++] = va_arg(child_list, Node*);
}
va_end(child_list);
}
这段示例假定 childnum 非负、parent->children 的剩余容量足够,并且随后每个实参的实际类型都是 Node *。实际接口还应校验容量;如果调用方本来就能组织一个数组,传入数组与长度通常比可变参数更容易检查。
问题案例
还有一种错误的实现。它在我当时打印树结构时表现为 Segmentation fault (core dumped),但这只是未定义行为的一种可能结果:
void AddChildren(Node* parent, ...)
{
if (parent == NULL) return;
Node* child = NULL;
va_list child_list;
va_start(child_list, parent);
while ((child = va_arg(child_list, Node*)) != NULL) {
parent->children[parent->child_ptr++] = child;
}
va_end(child_list);
child = NULL;
}
错误分析
我从 Stack Overflow 上找到过相关讨论:stdarg.h reads too many arguments。更准确地说,标准接口没有提供从 va_list 查询剩余参数数量的方法;第一次在不存在下一个实参时调用 va_arg,行为就已经未定义。程序可能碰巧读到某个值、崩溃,也可能出现其他结果,不能把它描述成一种可靠的“持续读取栈内存”过程。
It is your responsibility to communicate to your function how many arguments there are and what their types are.
上面的介绍可以得出第一种解决方案,就是本文中提到的实例,通过向函数传入一个额外的参数来实现。
另一种方案是约定哨兵值。省略号对应的实参只进行默认实参提升,不会像具名形参那样进行赋值式指针转换,因此最后一个参数应显式写成 (Node *)NULL:
AddChildren(parent, child1, child2, (Node *)NULL);
计数参数通常更容易校验;如果采用哨兵方案,必须在接口文档中明确哨兵的类型和值。无论哪种方案,读取超过实参数量或用错误类型读取都会产生未定义行为。
标准依据
觉得有帮助?
分享给同样关注系统性能的人。