0Pricing
C Academy · 课时

编写类似 printf 的函数

可变参数

编写类似 printf 的函数 是 CoddyKit 上的免费 C Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。

接收格式字符串的函数

可变参数最强大的用途,是构建能够解析格式字符串的 printf 风格函数。

格式字符串会告诉函数后面有多少个参数,以及每个参数的类型。

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

void myprint(const char *fmt, ...) {
    printf("format was: %s\n", fmt);
}

int main(void) {
    myprint("%d items\n", 5);
    return 0;
}

遍历格式字符串

逐个字符扫描格式字符串。当看到 % 时,下一个字符就是转换说明符,它会告诉您要读取什么类型。

其他所有内容都按字面原样打印。

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

void scan(const char *fmt) {
    for (const char *p = fmt; *p; p++) {
        if (*p == '%') printf("[spec:%c]", *(p+1));
        else putchar(*p);
    }
    putchar('\n');
}

int main(void) {
    scan("x=%d y=%s");
    return 0;
}

读取匹配的类型

当说明符为 %d 时,读取一个 int。对于 %s,读取一个 char *。格式必须与实际参数完全匹配。

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

void mini(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    for (const char *p = fmt; *p; p++) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("%d", va_arg(args, int));
            p++;
        } else putchar(*p);
    }
    va_end(args);
}

int main(void) {
    mini("count=%d\n", 42);
    return 0;
}

处理多个说明符

扩展扫描器,使其能够处理多种转换。为 %d、%s 和 %c 添加分支,每次读取正确的类型。

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

void fmt_print(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    for (const char *p = fmt; *p; p++) {
        if (*p != '%') { putchar(*p); continue; }
        p++;
        if (*p == 'd') printf("%d", va_arg(args, int));
        else if (*p == 's') printf("%s", va_arg(args, char *));
        else if (*p == 'c') putchar(va_arg(args, int));
    }
    va_end(args);
}

int main(void) {
    fmt_print("%s is %d\n", "age", 30);
    return 0;
}

转义百分号

真正的格式化函数会将 %% 视为字面百分号。添加相应分支,以便用户打印实际的 %。

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

void fp(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    for (const char *p = fmt; *p; p++) {
        if (*p == '%') {
            p++;
            if (*p == '%') putchar('%');
            else if (*p == 'd') printf("%d", va_arg(args, int));
        } else putchar(*p);
    }
    va_end(args);
}

int main(void) {
    fp("%d%% done\n", 75);
    return 0;
}

转发给 vprintf

很多时候,您并不想重新实现格式化功能。标准库提供了 vprintf,它可以直接接收 va_list。

您的包装函数只需将参数继续传递下去即可。

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

void logline(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
}

int main(void) {
    logline("value = %d, name = %s\n", 7, "box");
    return 0;
}

添加前缀

使用 vprintf 方法,可以轻松地在标准格式化功能周围添加行为,例如在每条消息前打印一个标签。

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

void info(const char *fmt, ...) {
    printf("[INFO] ");
    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
}

int main(void) {
    info("server started on port %d\n", 8080);
    return 0;
}

使用 vsnprintf 写入缓冲区

如果要将内容格式化到字符串中而不是直接打印,请使用 vsnprintf(buf, size, fmt, args)。

它受 size 限制,可以防止溢出,并且始终会添加空终止符。

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

void format_into(char *buf, size_t n, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, n, fmt, args);
    va_end(args);
}

int main(void) {
    char out[64];
    format_into(out, sizeof(out), "id-%d", 123);
    printf("%s\n", out);
    return 0;
}

返回长度

vsnprintf 返回本来会写入的字符数。您可以利用这一点检测截断情况或确定缓冲区大小。

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

int build(char *buf, size_t n, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    int len = vsnprintf(buf, n, fmt, args);
    va_end(args);
    return len;
}

int main(void) {
    char b[10];
    int needed = build(b, sizeof(b), "%d-%d", 11, 22);
    printf("%s (needed %d)\n", b, needed);
    return 0;
}

选择实现方式

有两种策略:自行解析格式字符串以获得完全控制,或者转发给 vprintf/vsnprintf,以获得正确性并减少代码量。

对于大多数实际代码,转发更加安全、简单。

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

void say(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vfprintf(stdout, fmt, args);
    va_end(args);
}

int main(void) {
    say("%s = %.1f\n", "pi", 3.14);
    return 0;
}

不匹配会导致未定义行为

如果格式字符串指定的类型与实际参数不匹配,行为就是未定义的。

printf("%d", "text") 可能会崩溃。请始终确保格式和参数保持同步。

#include <stdio.h>

int main(void) {
    int n = 5;
    printf("%d\n", n);
    return 0;
}

快速检查

测试您对 printf 风格包装函数的理解。

回顾

您学习了如何构建 printf 风格函数:

  • 将格式字符串作为命名参数,然后使用 ...。
  • 您可以自行解析 % 说明符并读取匹配的类型,或者
  • 将 va_list 转发给 vprintf / vsnprintf,以进行安全、正确的格式化。
  • 保持格式与参数类型同步,避免未定义行为。

常见问题解答

「编写类似 printf 的函数」课时是免费的吗?

是的 — 「编写类似 printf 的函数」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。

「编写类似 printf 的函数」这节课中我会学到什么?

可变参数 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「编写类似 printf 的函数」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C Academy 课中编写并运行代码吗?

能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. stdarg 宏
  2. 编写类似 printf 的函数
  3. 类型安全问题
  4. 实际示例
← 返回 C Academy