0Pricing
C Academy · Lesson

Writing printf-like Functions

Variable arguments.

Writing printf-like Functions is a free C Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Functions that take a format string

The most powerful use of variadics is building printf-style functions that interpret a format string.

The format tells the function how many arguments follow and what type each one is.

#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;
}

Walking the format string

Scan the format character by character. When you see a %, the next character is a conversion specifier telling you what type to read.

Everything else is printed literally.

#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;
}

Reading the matching type

When the specifier is %d, read an int. For %s read a char *. The format must match the actual arguments exactly.

#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;
}

Handling multiple specifiers

Extend the scanner to handle several conversions. Add cases for %d, %s, and %c, reading the right type each time.

#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;
}

Escaping the percent sign

Real format functions treat %% as a literal percent sign. Add a case so users can print an actual %.

#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;
}

Forwarding to vprintf

Often you do not want to reimplement formatting. The standard library offers vprintf, which takes a va_list directly.

Your wrapper just passes its arguments along.

#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;
}

Adding a prefix

The vprintf approach makes it easy to add behavior around standard formatting, such as printing a tag before every message.

#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;
}

Writing into a buffer with vsnprintf

To format into a string instead of printing, use vsnprintf(buf, size, fmt, args).

It is bounded by size, preventing overflow, and always null-terminates.

#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;
}

Returning a length

vsnprintf returns the number of characters that would have been written. You can use this to detect truncation or to size a buffer.

#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;
}

Choosing your approach

Two strategies: parse the format yourself for full control, or forward to vprintf/vsnprintf for correctness and less code.

For most real code, forwarding is the safer and simpler choice.

#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;
}

Mismatch is undefined

If the format string promises a type that does not match the actual argument, behavior is undefined.

printf("%d", "text") may crash. Always keep the format and arguments in sync.

#include <stdio.h>

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

Quick Check

Test your understanding of printf-style wrappers.

Recap

You learned to build printf-like functions:

  • Take a format string as the named parameter, then ....
  • Either parse % specifiers yourself and read matching types, or
  • forward the va_list to vprintf / vsnprintf for safe, correct formatting.
  • Keep the format and argument types in sync to avoid undefined behavior.

Frequently asked questions

Is the “Writing printf-like Functions” lesson free?

Yes — the full text of “Writing printf-like Functions” is free to read here on the web, and the C Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C Academy course, upgrade to CoddyKit PRO.

What will I learn in “Writing printf-like Functions”?

Variable arguments. You practise C Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C Academy?

No prior experience is required. C Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Writing printf-like Functions” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C Academy lesson?

Yes. Every C Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The stdarg Macros
  2. Writing printf-like Functions
  3. Type Safety Concerns
  4. Practical Examples
← Back to C Academy