使用类似 printf() 的接口实现函数
可变长度参数列表的一个常见用途是实现作为 printf()
函数族的薄包装函数。一个这样的例子是一组错误报告功能。
errmsg.h
#ifndef ERRMSG_H_INCLUDED
#define ERRMSG_H_INCLUDED
#include <stdarg.h>
#include <stdnoreturn.h> // C11
void verrmsg(int errnum, const char *fmt, va_list ap);
noreturn void errmsg(int exitcode, int errnum, const char *fmt, ...);
void warnmsg(int errnum, const char *fmt, ...);
#endif
这是一个简单的例子; 这样的包可以很复杂。通常情况下,程序员会使用 errmsg()
或 warnmsg()
,它们本身在内部使用 verrmsg()
。如果有人提出需要做更多的事情,那么暴露的 verrmsg()
功能将是有用的。你可能避免暴露它,直到你需要它( YAGNI -你是不是要去需要它 ),但需要最终会出现(你是会需要它 - YAGNI)。
errmsg.c
此代码只需要将可变参数转发到 vfprintf()
函数以输出到标准错误。它还报告与传递给函数的系统错误号(errno
)对应的系统错误消息。
#include "errmsg.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void
verrmsg(int errnum, const char *fmt, va_list ap)
{
if (fmt)
vfprintf(stderr, fmt, ap);
if (errnum != 0)
fprintf(stderr, ": %s", strerror(errnum));
putc('\n', stderr);
}
void
errmsg(int exitcode, int errnum, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
verrmsg(errnum, fmt, ap);
va_end(ap);
exit(exitcode);
}
void
warnmsg(int errnum, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
verrmsg(errnum, fmt, ap);
va_end(ap);
}
使用 errmsg.h
现在你可以使用以下功能:
#include "errmsg.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char buffer[BUFSIZ];
int fd;
if (argc != 2)
{
fprintf(stderr, "Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE);
}
const char *filename = argv[1];
if ((fd = open(filename, O_RDONLY)) == -1)
errmsg(EXIT_FAILURE, errno, "cannot open %s", filename);
if (read(fd, buffer, sizeof(buffer)) != sizeof(buffer))
errmsg(EXIT_FAILURE, errno, "cannot read %zu bytes from %s", sizeof(buffer), filename);
if (close(fd) == -1)
warnmsg(errno, "cannot close %s", filename);
/* continue the program */
return 0;
}
如果 open()
或 read()
系统调用失败,则错误将写入标准错误,程序将以退出代码 1 退出。如果 close()
系统调用失败,则错误仅作为警告消息打印,程序将继续。
检查 printf()
格式的正确使用
如果你正在使用 GCC(GNU C 编译器,它是 GNU 编译器集合的一部分)或使用 Clang,那么你可以让编译器检查传递给错误消息函数的参数是否与 printf()
所期望的相匹配。由于并非所有编译器都支持扩展,因此需要有条件地编译,这有点繁琐。但是,它给予的保护是值得的。
首先,我们需要知道如何检测编译器是 GCC 还是 Clang 模拟 GCC。答案是 GCC 定义 __GNUC__
来表示。
有关属性的信息,请参阅常用函数属性 - 特别是 format
属性。
重写了 errmsg.h
#ifndef ERRMSG_H_INCLUDED
#define ERRMSG_H_INCLUDED
#include <stdarg.h>
#include <stdnoreturn.h> // C11
#if !defined(PRINTFLIKE)
#if defined(__GNUC__)
#define PRINTFLIKE(n,m) __attribute__((format(printf,n,m)))
#else
#define PRINTFLIKE(n,m) /* If only */
#endif /* __GNUC__ */
#endif /* PRINTFLIKE */
void verrmsg(int errnum, const char *fmt, va_list ap);
void noreturn errmsg(int exitcode, int errnum, const char *fmt, ...)
PRINTFLIKE(3, 4);
void warnmsg(int errnum, const char *fmt, ...)
PRINTFLIKE(2, 3);
#endif
现在,如果你犯了一个错误:
errmsg(EXIT_FAILURE, errno, "Failed to open file '%d' for reading", filename);
(%d
应该是%s
),那么编译器会抱怨:
$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes \
> -Wold-style-definition -c erruse.c
erruse.c: In function ‘main’:
erruse.c:20:64: error: format ‘%d’ expects argument of type ‘int’, but argument 4 has type ‘const char *’ [-Werror=format=]
errmsg(EXIT_FAILURE, errno, "Failed to open file '%d' for reading", filename);
~^
%s
cc1: all warnings being treated as errors
$