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

jgovgodb  于 2023-02-21  发布在  其他
关注(0)|答案(5)|浏览(139)

我在C语言中摆弄可变参数函数来学习它们是如何工作的,并且试图构建一个简单的'print lines'函数,而不需要手动计算行数。我将该函数 Package 在一个宏中,该宏在char *参数列表的末尾添加一个null指针,这样该函数就可以逐行打印,直到找到一个null参数。
我知道我已经避免了一些常见的陷阱,比如忘记在参数列表中转换null指针,但是不管什么原因,这段代码仍然不能工作,调用带有任意数量参数的函数都能正确地打印它们,然后无法检测到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
}
ukqbszuj

ukqbszuj1#

如果你看看这个:

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

你会看到有两个独立的变量nextdo..while循环内部的变量屏蔽了外部定义的变量,你把va_arg的结果赋给内部的next,然后当你得到while (next)条件时,内部的next超出了作用域,您现在正在阅读外部的next,它从未被写入。这将触发undefined behavior
相反,您需要:

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

所以你只需要使用一个名为next的变量。

gywdnpxw

gywdnpxw2#

小的重写。宏被修改为+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;
}
a9wyjsp7

a9wyjsp73#

保存时间,启用所有编译器警告。

  • warning: 'next' may be used uninitialized [-Wmaybe-uninitialized] } while(next);很快就触及了关键问题。
  • warning: control reaches end of non-void function [-Wreturn-type]在2个位置。

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

ukxgm1gy

ukxgm1gy4#

“垃圾”来自未初始化的对象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");
}
5uzkadbs

5uzkadbs5#

强烈建议你避免可变参数函数,而使用指针数组和可变参数宏(带有终止符对象)。
当使用这种方法时,您的函数将如下所示:

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绑定可能会破坏您的代码。
此外,当使用这种方法时,事情也会变得更加有趣和有趣,允许轻松的类型检测和多类型参数...... www.example.com CSTL库中的这段代码facil.io为我的意思提供了一个很好的示例。
该函数接受结构体数组:

/** 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绑定更好地工作,并且结构数组的构造方式更适合特定的目标。

相关问题