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