我正在研究 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
}
如果你看看这个:
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
你正在使用。
小改写。宏已用 +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;
}
节省时间,启用所有编译器警告。
warning: 'next' may be used uninitialized [-Wmaybe-uninitialized] } while(next);
快速进入关键问题。
warning: control reaches end of non-void function [-Wreturn-type]
在 2 个地方。
这比在堆栈溢出时发布更快。
“垃圾”来自未初始化的对象
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");
}
我高度建议您避免可变参数函数并改用指针数组和可变参数宏(使用终止符对象)。
使用这种方法时,您的函数看起来像这样:
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 绑定更好地工作,并以更适合特定目标的惯用方式构造结构数组。