本文是自己在使用<stdarg.h>头文件时遇到的一些问题和总结

stdarg.h 介绍

<stdarg.h> 是 C 语言处理可变参数的标准头文件。可变参数函数在最后一个具名参数后使用省略号 ...。头文件定义了一个类型和若干宏(不是普通函数):

  • va_list:保存遍历状态的类型;它的具体表示由实现决定,不能把它当成普通列表操作。
  • va_start(ap, last_arg):以最后一个具名参数为锚点初始化 ap
  • va_arg(ap, type):按调用约定读取下一个参数。调用方与被调用方必须对类型达成一致。
  • va_end(ap):结束遍历;每次成功执行 va_startva_copy 后都应配对调用。

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);
}

问题案例

还有一种错误的实现,这种实现将在打印树结构时报错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;
}

错误分析

我从 StackOverFlow 上找到相关问题和解答:stdarg.h reads too many arguments 简单总结如下: 在使用 va_arg()时,需要明确知道传入的参数个数,否则 va_arg()将会一直读取内存,直到不满足 while()条件,这就导致有时会从栈中读取额外的内容。

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);

计数参数通常更容易校验;如果采用哨兵方案,必须在接口文档中明确哨兵的类型和值。无论哪种方案,读取超过实参数量或用错误类型读取都会产生未定义行为。