将带有参数的 printf 用于可变参数函数?

Posted

技术标签:

【中文标题】将带有参数的 printf 用于可变参数函数?【英文标题】:Use printf with args into variadic functions? 【发布时间】:2022-01-20 23:46:10 【问题描述】:

我需要一个像printf 一样工作的函数,但对fmt 字符串进行了一些更改:例如,在开头添加一个包含日期时间的字符串,但其余部分,我将保持相同的printf东西……

void simple_printf(const char* fmt, ...)

    va_list args;
    va_start(args, fmt);
    va_end(args);

    /* made some changes to fmt, concatenate string,...*/

    printf(fmt, ...);

这是我正在编写的代码。如您所见,我希望更改fmt 字符串,但之后,调用“标准”printfsprintf,传递参数——一种绕过。

这可能吗?

【问题讨论】:

如果您输入例如va_list printf 在您最喜欢的搜索引擎中应该很容易找到有关 vprintf 功能的信息。任何关于可变参数和函数的体面的书籍或教程也应该真正提到它。 请注意va_end 将在vprintf() 之后。 ghiboz,是的,有可能。 我不会更改格式字符串,而是使用额外的printf() 在调用者请求的输出之前和/或之后添加。 -- 你可能想edit你的问题并提供minimal reproducible example。 请注意,C89 没有标准化 vprintf()vsprintf()vfprintf() 等。但是,一些 C89 实现无论如何都提供了这些功能,或者提供了具有不同名称的代理(等价物)。请记住:C89 已超过 30 年——它很古老,已被 C99、C11、C18 取代。没有充分的理由,您不应该使用 C89 进行编程。 【参考方案1】:

有多种方法可以做到这一点。您可以在 GitHub 上的 SOQ(堆栈溢出问题)存储库中找到执行此类操作的一些代码,即 src/libsoq 子目录中的文件 stderr.cstderr.h。这是我多年来开发的一个包(最早的版本我仍然有记录,可以追溯到 1988 年),我在大多数 C 程序中都使用它。

现在使用的方案通过将要格式化的数据转换为字符串来确保只有一次写入操作——请参阅err_fmtmsg()——然后使用适当的写入机制(标准 I/O,例如fprintf(),或 write()syslog()) 将消息发送到输出机制。

static size_t err_fmtmsg(char *buffer, size_t buflen, int flags, int errnum,
                         const char *format, va_list args)

    char *curpos = buffer;
    char *bufend = buffer + buflen;

    buffer[0] = '\0';   /* Not strictly necessary */
    if ((flags & ERR_NOARG0) == 0)
        curpos = efmt_string(curpos, bufend, "%s: ", arg0);
    if (flags & ERR_LOGTIME)
       
        char timbuf[32];
        curpos = efmt_string(curpos, bufend,
                             "%s - ", err_time(flags, timbuf, sizeof(timbuf)));
       
    if (flags & ERR_PID)
        curpos = efmt_string(curpos, bufend,
                             "pid=%d: ", (int)getpid());
    curpos = vfmt_string(curpos, bufend, format, args);
    if (flags & ERR_ERRNO)
        curpos = efmt_string(curpos, bufend,
                             "error (%d) %s\n", errnum, strerror(errnum));
    assert(curpos >= buffer);
    return((size_t)(curpos - buffer));

如你所见,这可以为arg0产生的消息添加前缀(程序名称,通过函数err_setarg0()设置;它可以添加一个PID;它可以添加时间戳(带有整数选项)秒、毫秒、微秒、纳秒在flags的控制下),也可以附加错误号和相应的系统错误信息。

这是隐藏在系统内部的一个功能。在外部级别,入口点之一是extern void err_syserr(const char *fmt, ...);——这会自动添加系统错误,在标准错误上打印消息,然后退出程序。还有很多其他的日志入口点,其中一些退出,一些返回。还有很多控件。

注意err_fmtmsg() 函数接受一个参数va_list args。这通常是最好的工作方式。您应该使用此方案编写代码:

void simple_printf(const char* fmt, ...)

    va_list args;
    va_start(args, fmt);
    simple_vprintf(fmt, args);
    va_end(args);


void simple_vprintf(const char* fmt, va_list args)

    /* … preamble … */
    vprintf(fmt, args);
    /* … postamble … */

您使用va_list 函数编写主函数,并使用调用主函数的... 提供方便的接口,如上所示。

如果您打算多次调用标准 I/O 写入函数(fprintf() 等),请考虑使用flockfile()funlockfile() 保持输出“原子”。

void simple_vprintf(const char* fmt, va_list args)

    flockfile(stdout);
    /* … preamble — possibly writing to stdout … */
    vprintf(fmt, args);
    /* … postamble — possibly writing to stdout … */
    funlockfile(stdout);

您还可以考虑提供以下功能:

extern void simple_fprintf(FILE *fp, const char *fmt, ...);
extern void simple_vfprintf(FILE *fp, const char *fmt, va_list args);

然后simple_vprintf() 将简单地调用simple_vfprintf(stdout, fmt, args)。然后,您可能会开始查看头文件中覆盖函数的 static inline 实现。

一段时间后,如果您实施了足够多的变体,您就会开始侵犯stderr.[ch] 中的实施。

【讨论】:

printf 不是已经在做flockfile 业务了吗? 是的,但是如果您要对同一个 I/O 流进行多次调用并希望它们全部组合在一起,则使用显式锁可确保前导码和后同步码中的操作与主要操作。【参考方案2】:

您应该从可变参数函数中调用vprintf,而不是调用printf

#include <stdarg.h>
#include <stdio.h>

void simple_printf(const char *fmt, ...) 
    va_list args;
    va_start(args, fmt);
    time_t t = time(NULL);
    int d = (t - 746755200) / 86400;
    int h = (t %= 86400) / 3600;
    int m = (t %= 3600) / 60;
    int s = t % 60;

    /* output a timestamp at the beginning of the line, thank you Janice Brandt */
    printf("Sep %d, 1993 %02d:%02d:%02d UTC: ", d, h, m, s);

    /* no changes should be made to fmt because it is a constant string */
    vprintf(fmt, args);
    va_end(args);

【讨论】:

挑选一个 nit — 代码不应修改 fmt 指向的字符串,因为它是 const char *fmt。可能有效的是修改 fmt 指向的副本 - 并可能使 fmt 指向修改后的副本。 @JonathanLeffler:好点。答案修改为引用Eternal September,这是一个存在于eternal-september.org的内存

以上是关于将带有参数的 printf 用于可变参数函数?的主要内容,如果未能解决你的问题,请参考以下文章

Chez Scheme 中的 FFI,用于具有可变参数 (varargs) 的 C 函数

可变参数函数——以printf为例子

可变参数函数——以printf为例子

用于打印可变参数的宏,可选择无参数

C语言可变参数

C语言可变参数