使用类似 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
$