C 可变参数函数的空终止参数列表

问题描述 投票:0回答:5

我正在研究 C 中的可变参数函数以了解它们的工作原理,并且正在尝试构建一个简单的“打印行”函数而不需要手动计算行数。我通过将函数包装在一个宏中来实现这一点,该宏将空指针添加到

char *
参数列表的末尾,因此该函数可以逐行打印直到找到空参数。

我知道我已经避免了一些常见的陷阱,比如忘记在参数列表中转换空指针,但无论出于何种原因,这段代码仍然无法正常工作。使用任意数量的参数调用该函数可以正确打印它们,然后无法检测到 null,打印一堆垃圾数据,然后崩溃。

int printline(const char *str) {
    printf("%s\n", str);
}

#define printlines(...) _comments(__VA_ARGS__, (char*)0)
int _printlines(char* first, ...) {
    if (first) {
        printline(first);

        va_list ptr;
        va_start(ptr, first);

        char *next;

        do {
            char *next = va_arg(ptr, char *);
            if (next) {
                printline(next);
            }
        } while(next);

        va_end(ptr);
    }
}

int main() {
    printlines("hi");
    //prints 'hi', then prints garbage data and crashes

    printlines("how", "are", "you");
    //prints 'how', 'are', and 'you', then prints garbage data and crashes
    
    _printlines("help", (char *)0);
    //prints 'help', then prints garbage data and crashes

    _printlines("something", "is", "wrong", (char *)NULL);
    //prints 'something', 'is', and 'wrong', then prints garbage data and crashes
}
c variadic
5个回答
4
投票

如果你看看这个:

    char* next;
    do{
        char* next = va_arg(ptr,char*);
        if(next){ comment(next); }
    }while(next);

你会看到你有两个单独的变量,称为

next
,其中一个在
do..while
循环内部掩盖了定义在外部的一个。您正在将
va_arg
的结果分配给内部
next
。然后,当您获得
while (next)
条件时,内部
next
超出范围,您现在正在读取从未写入的外部
next
。这会触发未定义的行为

你反而想要:

    char* next;
    do{
        next = va_arg(ptr,char*);
        if(next){ comment(next); }
    }while(next);

所以你只有一个变量叫做

next
你正在使用。


1
投票

小改写。宏已用 +0 修改,因此它可以采用零参数。

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

#define printlines(...) _printlines(__VA_ARGS__+0,(void*)0)
void _printlines(const char * first, ...)
{
    const char * ptr;
    va_list va;
    va_start (va, first);

    printf("---begin---\n");
    for (ptr = first; ptr != NULL ; ptr = va_arg(va,char*) )
    {
        printf("%s\n", ptr);
    }
    printf("---end---\n");
    va_end(va);
}

int main()
{
    printlines();     // instead of: printlines(NULL);
    printlines("hi");
    printlines("how","are","you");
    return 0;
}

1
投票

节省时间,启用所有编译器警告。

  • warning: 'next' may be used uninitialized [-Wmaybe-uninitialized] } while(next);
    快速进入关键问题。

  • warning: control reaches end of non-void function [-Wreturn-type]
    在 2 个地方。

这比在堆栈溢出时发布更快。


0
投票

“垃圾”来自未初始化的对象

next
。当您退出循环时,循环中定义的另一个
next
停止存在。

删除奇怪的功能并清理一些乱七八糟的东西。

int printline(const char* str){
    printf("%s",str);
}

#define printlines(...) printlinesfunc(__VA_ARGS__,(char*)0)
int printlinesfunc(const char* first, ...){
    if(first)
    {
        va_list ptr;
        va_start(ptr,first);

        char* next;
        printline(first);
        while((next = va_arg(ptr, char *)))
            printline(next);
        va_end(ptr);
    }
}

int main(){
    printlines("hi" , "\n");
    printlines("how"," are"," you", "\n");
    printlines("help", "\n");
    printlines("something", " is", " wrong", "\n");
}

0
投票

高度建议您避免可变参数函数并改用指针数组和可变参数宏(使用终止符对象)。

使用这种方法时,您的函数看起来像这样:

void printline(const char *str) { printf("%s\n", str); }

int printlines(char **lines) {
  if (!lines)
    return -1;
  while (*lines)
    printline(*(lines++));
  return 0;
}

#define printlines(...) printlines(char *[] { __VA_ARGS__, NULL })

不仅可变参数函数有时难以编写代码,而且可变参数函数的 ABI 存在问题,以至于不同的语言可能会以不同的方式对待它,不同语言之间的 C 绑定可能会破坏您的代码。

此外,当使用这种方法时,事情也会变得更加有趣和有趣,允许简单的类型检测和多类型参数......facil.io CSTL 库中的这段代码为我提供了一个很好的例子意思是

函数接受结构数组:

/** An information type for reporting the string's state. */
typedef struct fio_str_info_s {
  /** The string's length, if any. */
  size_t len;
  /** The string's buffer (pointer to first byte) or NULL on error. */
  char *buf;
  /** The buffer's capacity. Zero (0) indicates the buffer is read-only. */
  size_t capa;
} fio_str_info_s;

/** memory reallocation callback. */
typedef int (*fio_string_realloc_fn)(fio_str_info_s *dest, size_t len);

/** !!!Argument type used by fio_string_write2!!! */
typedef struct {
  size_t klass; /* type detection */
  union {.      /* supported types */
    struct {
      size_t len;
      const char *buf;
    } str;
    double f;
    int64_t i;
    uint64_t u;
  } info;
} fio_string_write_s;

int fio_string_write2(fio_str_info_s *restrict dest,
                      fio_string_realloc_fn reallocate, /* nullable */
                      const fio_string_write_s srcs[]);

然后一个宏确保数组的最后一个元素是终止符元素:

/* Helper macro for fio_string_write2 */
#define fio_string_write2(dest, reallocate, ...)            \
  fio_string_write2((dest),                                 \
                    (reallocate),                           \
                    (fio_string_write_s[]){__VA_ARGS__, {0}})

提供了额外的辅助宏,使

fio_string_write_s
结构更容易构建。即:

/** A macro to add a String with known length to `fio_string_write2`. */
#define FIO_STRING_WRITE_STR2(str_, len_)                    \
  ((fio_string_write_s){.klass = 1, .info.str = {.len = (len_), .buf = (str_)}})

/** A macro to add a signed number to `fio_string_write2`. */
#define FIO_STRING_WRITE_NUM(num)                            \
  ((fio_string_write_s){.klass = 2, .info.i = (int64_t)(num)})

并且该函数使用终止符元素来检测宏接收的参数数量:

int fio_string_write2 (fio_str_info_s *restrict dest,
                               fio_string_realloc_fn reallocate, /* nullable */
                               const fio_string_write_s srcs[]) {
  int r = 0;
  const fio_string_write_s *pos = srcs;
  size_t len = 0;

  while (pos->klass) {
    switch (pos->klass) { /* ... */ }
    /* ... counts total length */
    ++pos;
  }
  /* ... allocates memory, if required and possible ... */
  pos = srcs;
  while (pos->klass) {
    switch (pos->klass) { /* ... */ }
    /* ... prints data to string ... */
    ++pos;
  }
    /* ... house-keeping + return error value ... */
}

使用示例(来自源码注释):

 fio_str_info_s str = {0};
 fio_string_write2(&str, my_reallocate,
                     FIO_STRING_WRITE_STR1("The answer is: "),
                     FIO_STRING_WRITE_NUM(42),
                     FIO_STRING_WRITE_STR2("(0x", 3),
                     FIO_STRING_WRITE_HEX(42),
                     FIO_STRING_WRITE_STR2(")", 1));

这既简化了代码又避免了可变参数函数的许多问题。这也允许来自其他语言的 C 绑定更好地工作,并以更适合特定目标的惯用方式构造结构数组。

© www.soinside.com 2019 - 2024. All rights reserved.