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