C 可变参数包装器

4
为了输出格式化的调试信息,我编写了一个vsfprint的包装器。现在,我想为输出缓冲区分配恰好足够的内存,而不是只声明一个随意的高缓冲区大小(这是一个小型嵌入式平台(ESP8266))。为此,我迭代变量参数,直到发现NULL。

如果我没有忘记在每次调用中添加(char *)NULL参数,这个方法就可以正常工作。所以,我想创建另一个包装器,一个函数,只转发所有参数并添加(char *) NULL参数:

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // malloc

void write_log(const char *format, ...) {

  char* buffdyn;
  va_list args;

  //  CALC. MEMORY
  size_t len;
  char *p;

  if(format == NULL)
    return;

  len = strlen(format);

  va_start(args, format);

  while((p = va_arg(args, char *)) != NULL)
    len += strlen(p);

  va_end(args);
  // END CALC. MEMORY

  // ALLOCATE MEMORY
  buffdyn = malloc(len + 1);    /* +1 for trailing \0 */
  if(buffdyn == NULL) {
    printf("Not enough memory to process message.");
    return;
  }

  va_start(args, format);
  //vsnprintf = Write formatted data from variable argument list to sized buffer
  vsnprintf(buffdyn, len, format, args);
  va_end(args);

  printf("%s\r\n",buffdyn);
  free(buffdyn);
}

void write_log_wrapper(const char *format, ...) {

  va_list arg;

  va_start(arg, format);
  write_log(format,arg,(char *)NULL);
  va_end(arg);
}


int main()
{
    const char* sDeviceName = "TEST123";
    const char* sFiller1 = "12345678";

    write_log_wrapper("Welcome to %s%s", sDeviceName,sFiller1);
    write_log("Welcome to %s%s", sDeviceName,sFiller1, (char *)NULL);

    return 0;
}

直接调用write_log()函数是可以正常工作的(如果您不忘记NULL参数)。调用write_log_wrapper()函数只会显示第一个参数,然后在输出中添加了“(nu”(垃圾?)。

我做错了什么?这是一种好的方法来实现我首先想要做的吗?

谢谢。


2
如果您打算将其写入stdout,为什么不直接使用vprintf?如果要写入文件,请使用vfprintf。这样,在您的代码中就没有缓冲区分配,您也不必处理有多少参数或需要多大的缓冲区的问题。 - Dark Falcon
3
你忽略了一个可能性,即传递的参数之一不是通过 char * 传递的以 NUL 结尾的字符串。如果你得到一个 intdouble,会怎样呢? - Andrew Henle
...或者一个空指针%p - Ingo Leonhardt
原因是,你无法从这个简化的示例中看到,该函数所做的不仅仅是输出缓冲区。它还会添加时间戳,并通过额外的参数添加一些基于该参数的 ANSI 颜色;-)。但我想让这个示例保持简单;-) - svenema
3个回答

3
为了确定需要多大的缓冲区来容纳输出字符串,您需要完全解析整个格式字符串并实际扩展参数。您可以自己完成这个过程,复制所有printf()及其类似函数的处理过程,并希望不会出现任何错误,或者您可以使用vsnprintf() - 首先确定大小,然后将输入实际扩展为一个输出字符串。
#define FIXED_SIZE 64

void write_log(const char *format, ...)
{
    // set up a fixed-size buffer and a pointer to it
    char fixedSizeBuffer[ FIXED_SIZE ];
    char *outputBuffer = fixedSizeBuffer;

    // no dynamic buffer yet
    char *dynamicBuffer = NULL;

    // get the variable args
    va_list args1;
    va_start( args1, format );

    // need to copy the args even though we won't know if we
    // need them until after we use the first set
    va_list args2;
    va_copy( args2, args1 );

    // have to call vsnprintf at least once - might as well use a small
    // fixed-size buffer just in case the final string fits in it
    int len = vsnprintf( fixedSizeBuffer, sizeof( fixedSizeBuffer ), format, args1 );
    va_end( args1 );

    // it didn't fit - get a dynamic buffer, expand the string, and
    // point the outputBuffer pointer at the dynamic buffer so later
    // processing uses the right string
    if ( len > sizeof( fixedSizeBuffer  ) )
    {
        dynamicBuffer = malloc( len + 1 );
        vsnprintf( dynamicBuffer, len + 1, format, args2 );
        outputBuffer = dynamicBuffer;
    }

    va_end( args2 );

    // do something with outputBuffer

    free( dynamicBuffer );
    return;
}

2
好主意,但有些系统不允许您两次使用 args(例如 IBM Z 系列上的 Linux),因此您应该添加一个 va_copy() - Ingo Leonhardt
一个非常稳定的解决方案,测试过了,开箱即用 :D。 - svenema

1
如果您只想确保所有调用在结尾处接收到一个标记,请使用宏:
#define WRITE_LOG(...) write_log(__VA_ARGS__, (char*)0)

这确保末尾始终有一个额外的0

同时要注意NULL。在C标准中,它对于哪些表达式解析不充分。常见情况是0(void*)0。因此,在64位体系结构上,这两者可能具有不同的宽度(第一个为32位,第二个为64位)。如果可变参数函数在此处接收到错误的宽度,可能会导致严重后果。因此,我使用了(char*)0,这是您的函数似乎期望的类型。(但在这种特殊情况下,(void*)0也可以使用。)


鉴于目标平台是“小型嵌入式平台”,我建议OP在使用(char*)0作为NULL指针之前,先验证编译器是否将其视为NULL指针。 我认为直到C11,(char*)0才被要求成为有效的NULL指针。 - Andrew Henle
@AndrewHenle,您可能是指空指针而不是NULL指针。(char*)0在标准意义上不是“空指针常量”,这里实际上需要(void*)0。但对于手头的情况来说,这是无关紧要的,它是程序所期望的正确类型的空指针。 - Jens Gustedt
有时候它能正常工作,有时候会崩溃.. 看起来似乎取决于参数的数量.. 目前还不太明白原因。 - svenema

1
我做错了什么?
传递一个 va_list arg
write_log(format, arg, (char *)NULL);

不是传递几个char*的相同。
write_log("Welcome to %s%s", sDeviceName, sFiller1, (char *)NULL);

你无法绕过传递参数结尾的哨兵标记,它是一个 (char*) NULL 或者你决定使用的其他内容。

其他的选择是:

  • 显式地传递参数数量,可能作为第二个参数
  • 解析格式字符串以获取转换说明符,实际上模仿了printf的做法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接