Input, Output and Files

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

All C I/O goes through streams: a FILE * wrapping a buffered byte sequence, whether it is a terminal, a file or a pipe. <stdio.h> is one of the oldest parts of the library, which shows in its API — but its model is simple and its behavior is precisely specified.

Streams and FILE

Three streams are open before main runs:

Stream Direction Buffering by default

stdin

Input

Line-buffered if it is a terminal, fully buffered otherwise.

stdout

Output

Line-buffered if it is a terminal, fully buffered otherwise — the reason output can appear out of order when redirected to a file.

stderr

Output

Unbuffered (or at most line-buffered), so diagnostics appear immediately.

#include <stdio.h>

int main(void)
{
    fprintf(stdout, "normal output\n");
    fprintf(stderr, "diagnostics go here\n");       // never mix these two purposes

    // printf(...) is exactly fprintf(stdout, ...)
    printf("%s\n", "the same stream");
    return 0;
}

Write results to stdout and everything else — progress, warnings, errors — to stderr. That is what makes a program usable in a pipeline.

flowchart LR subgraph OUT["output path"] direction LR A["printf / fputs / fwrite"] --> B[("stream buffer
in your process")] B -->|"buffer full, newline on a
line-buffered stream,
fflush, or fclose"| C[("OS file
descriptor")] C --> D([file, terminal or pipe]) end subgraph IN["input path"] direction LR E([file, terminal or pipe]) --> F[("OS read into
the stream buffer")] F --> G["scanf / fgets / fread
consume from the buffer"] end B -.->|"process crashes or _Exit:
buffered bytes are lost"| X[["output never written"]]

Unformatted I/O

The character- and line-oriented functions, which are the ones to prefer for reading input:

#include <stdio.h>
#include <string.h>

int main(void)
{
    // Whole strings out: puts adds a newline, fputs does not.
    puts("puts adds a newline");
    fputs("fputs does not\n", stdout);

    // Characters out.
    putchar('x');
    putc('\n', stdout);

    // Characters in: the return type is int, so EOF (-1) is distinguishable
    // from every valid character value. NEVER use a char here.
    int c = getchar();
    if (c != EOF) {
        printf("first character: %c\n", c);
        ungetc(c, stdin);                   // push one character back
    }

    // Lines in: fgets keeps the newline and always NUL-terminates.
    char line[256];
    while (fgets(line, sizeof line, stdin) != nullptr) {
        size_t len = strlen(line);

        if (len > 0 && line[len - 1] == '\n') {
            line[len - 1] = '\0';           // strip the newline
        } else if (len == sizeof line - 1) {
            // No newline and the buffer is full: the line was longer than the buffer.
            int discarded;
            while ((discarded = getchar()) != '\n' && discarded != EOF) {
                /* drain the rest of the line */
            }
        }

        printf("[%s]\n", line);
    }
    return 0;
}

gets was removed from the language in C11 — it had no way to know the buffer size and was the direct cause of the Morris worm. fgets is its replacement. On POSIX, getline allocates the buffer for you and handles lines of any length.

The printf Family

#include <inttypes.h>
#include <stdio.h>

int main(void)
{
    char buffer[64];

    printf("to stdout\n");
    fprintf(stderr, "to a stream\n");

    // snprintf: writes at most n bytes INCLUDING the NUL, always terminates,
    // and returns the length it WANTED -- so >= n means truncation.
    int needed = snprintf(buffer, sizeof buffer, "%s-%d", "id", 42);
    if (needed < 0 || (size_t)needed >= sizeof buffer) {
        fputs("truncated\n", stderr);
    }
    printf("%s (needed %d)\n", buffer, needed);

    // Measure first, then allocate: snprintf(nullptr, 0, ...) just computes the length.
    int length = snprintf(nullptr, 0, "%s-%d", "id", 42);
    printf("exact length is %d\n", length);
    return 0;
}

Format Specifiers

The full form is %[flags][width][.precision][length]conversion.

Conversion For Example

%d / %i

int

printf("%d", -42)-42

%u / %o / %x / %X

unsigned int (decimal/octal/hex)

printf("%#x", 255)0xff

%f / %F

double, fixed notation

printf("%.2f", 3.14159)3.14

%e / %E

double, scientific

printf("%e", 31415.9)3.141590e+04

%g / %G

double, shorter of %e/%f

printf("%g", 0.0001)0.0001

%a / %A

double, hexadecimal float

printf("%a", 1.0)0x1p+0

%c

A character (passed as int)

printf("%c", 65)A

%s

char *, NUL-terminated

printf("%.3s", "abcdef")abc

%p

void * — cast the argument

printf("%p", (void *)&x)

%%

A literal %

printf("100%%")100%

%n

Writes the count so far through an int *

A known format-string attack vector — avoid.

Length modifiers: hh (char), h (short), l (long), ll (long long), j (intmax_t), z (size_t), t (ptrdiff_t), L (long double).

#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    // Flags: - left-justify, + always sign, 0 zero-pad, space, # alternate form
    printf("[%-8d][%+d][%08.3f][% d][%#o]\n", 42, 42, 3.14159, 42, 8);

    // Width and precision from arguments, with *
    printf("[%*d][%.*f]\n", 8, 42, 2, 3.14159);

    // The length modifiers that are mandatory, not optional:
    size_t count = 3;
    ptrdiff_t delta = -1;
    long long big = 123456789012345LL;
    int64_t exact = 42;

    printf("%zu %td %lld %" PRId64 "\n", count, delta, big, exact);

    // %f takes a double: a float argument is promoted, so %f is correct for both.
    float f = 1.5f;
    printf("%f %.1f\n", (double)f, (double)f);
    return 0;
}

The rules that prevent undefined behavior:

  • Every argument must match its conversion. A mismatch is UB, and printf("%d", 1.0) will print garbage. Enable -Wformat (in -Wall) and add __attribute__((format(printf, 1, 2))) to your own wrappers.

  • %s requires a NUL-terminated string, not a char array that happens to hold text.

  • Never pass a variable as the format string: printf(user_input) is a format-string vulnerability. Write printf("%s", user_input). -Wformat-security catches it.

  • Cast pointers to void * for %p and float to double for clarity (the promotion happens anyway).

The scanf Family — and Why to Avoid It

#include <stdio.h>

int main(void)
{
    int a = 0, b = 0;

    // ALWAYS check the return value: it is the number of items ASSIGNED,
    // which may be fewer than requested, or EOF.
    int assigned = sscanf("10 20", "%d %d", &a, &b);
    if (assigned != 2) {
        fputs("parse failed\n", stderr);
    }
    printf("%d %d (assigned %d)\n", a, b, assigned);

    // %s with no width is a buffer overflow waiting to happen. Bound it:
    char word[16];
    if (sscanf("overlongwordthatwouldoverflow", "%15s", word) == 1) {
        printf("[%s]\n", word);
    }

    // %[...] scan sets, and %*d to parse-and-discard a field:
    char key[32], value[32];
    if (sscanf("name=alice", "%31[^=]=%31s", key, value) == 2) {
        printf("%s -> %s\n", key, value);
    }
    return 0;
}

`scanf’s problems are structural: whitespace handling is subtle, a failed conversion leaves the offending input unconsumed (so a naive retry loops forever), overflow of a numeric field is undefined behavior, and there is no way to distinguish "malformed" from "end of input" without inspecting the return code carefully.

The robust pattern is read a line, then parse the line:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Returns true and writes *out on success; false on any malformed input.
static bool parse_int(const char *text, long *out)
{
    errno = 0;
    char *end = nullptr;
    long value = strtol(text, &end, 10);

    if (end == text) {
        return false;                       // no digits at all
    }
    while (*end == ' ' || *end == '\n' || *end == '\t') {
        ++end;                              // allow trailing whitespace
    }
    if (*end != '\0') {
        return false;                       // trailing garbage
    }
    if (errno == ERANGE) {
        return false;                       // out of range
    }

    *out = value;
    return true;
}

int main(void)
{
    const char *inputs[] = { "42", "  -7 \n", "12abc", "", "99999999999999999999" };

    for (size_t i = 0; i < sizeof inputs / sizeof inputs[0]; ++i) {
        long value = 0;
        printf("%-24s -> %s", inputs[i],
               parse_int(inputs[i], &value) ? "ok" : "rejected");
        if (parse_int(inputs[i], &value)) {
            printf(" (%ld)", value);
        }
        putchar('\n');
    }
    return 0;
}

Opening Files

Mode Meaning

"r"

Read. Fails if the file does not exist.

"w"

Write. Truncates an existing file, or creates it.

"a"

Append. Every write goes to the end, regardless of positioning.

"r+"

Read and write. Fails if absent; does not truncate.

"w+"

Read and write, truncating or creating.

"a+"

Read anywhere, write only at the end.

…b

Add b ("rb", "wb", "r+b") for a binary stream — essential on Windows, a no-op on POSIX. Always use it for non-text data.

"wx"

C11: create exclusively — fail if the file already exists. The safe way to avoid clobbering, and to avoid a symlink race.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f = fopen("/tmp/c-demo-output.txt", "w");
    if (f == nullptr) {
        perror("fopen");                    // ALWAYS check, and report why
        return EXIT_FAILURE;
    }

    fprintf(f, "line one\nline two\n");

    // fclose flushes; it can FAIL (a full disk shows up here, not at fprintf).
    if (fclose(f) != 0) {
        perror("fclose");
        return EXIT_FAILURE;
    }

    f = fopen("/tmp/c-demo-output.txt", "r");
    if (f == nullptr) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    char line[128];
    while (fgets(line, sizeof line, f) != nullptr) {
        fputs(line, stdout);
    }

    if (ferror(f)) {                        // distinguish a read error from EOF
        fputs("read error\n", stderr);
    }
    fclose(f);
    remove("/tmp/c-demo-output.txt");
    return 0;
}

Also available: freopen (reassign a stream, the portable way to redirect stdout), tmpfile (an auto-deleted temporary), rename, and remove. Avoid tmpnam/mktemp — they are race-prone; use tmpfile or POSIX mkstemp.

fread and fwrite — Binary I/O

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char *path = "/tmp/c-demo-binary.dat";
    uint32_t written[4] = { 1, 2, 3, 4 };

    FILE *out = fopen(path, "wb");           // note the b
    if (out == nullptr) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    // fwrite returns the number of ELEMENTS written, not bytes.
    size_t count = fwrite(written, sizeof written[0], 4, out);
    if (count != 4) {
        fputs("short write\n", stderr);
        fclose(out);
        return EXIT_FAILURE;
    }
    fclose(out);

    FILE *in = fopen(path, "rb");
    if (in == nullptr) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    uint32_t read_back[4] = { 0 };
    size_t got = fread(read_back, sizeof read_back[0], 4, in);

    if (got != 4) {
        // A short read is either EOF or an error -- check which.
        if (feof(in)) {
            fputs("unexpected end of file\n", stderr);
        } else if (ferror(in)) {
            fputs("read error\n", stderr);
        }
    }
    fclose(in);
    remove(path);

    printf("%zu elements: %u %u %u %u\n", got,
           read_back[0], read_back[1], read_back[2], read_back[3]);
    return 0;
}

Writing a struct with fwrite produces a file whose layout depends on the compiler’s padding, the platform’s endianness and the widths of its types — it is not a portable format. Serialize field by field with explicit byte order, as in Memory Model and Alignment.

Positioning

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char *path = "/tmp/c-demo-seek.txt";

    FILE *f = fopen(path, "w+b");
    if (f == nullptr) {
        perror("fopen");
        return EXIT_FAILURE;
    }
    fputs("ABCDEFGHIJ", f);

    // fseek: SEEK_SET (from the start), SEEK_CUR, SEEK_END.
    if (fseek(f, 4, SEEK_SET) != 0) {
        perror("fseek");
        fclose(f);
        return EXIT_FAILURE;
    }
    printf("byte at offset 4: %c\n", (char)fgetc(f));       // E

    long position = ftell(f);                               // -1L on failure
    printf("position now %ld\n", position);

    // fgetpos/fsetpos handle offsets too large for a long, and are the portable
    // choice for large files.
    fpos_t saved;
    if (fgetpos(f, &saved) == 0) {
        fseek(f, 0, SEEK_END);
        printf("size = %ld\n", ftell(f));
        fsetpos(f, &saved);                                 // back where we were
    }

    rewind(f);                                              // offset 0, clears error flags
    printf("first byte: %c\n", (char)fgetc(f));             // A

    fclose(f);
    remove(path);
    return 0;
}

On a text stream, only offsets obtained from ftell may be passed to fseek, and SEEK_END is not required to be meaningful — so use a binary stream for anything involving arithmetic on positions.

Buffering and Flushing

#include <stdio.h>

int main(void)
{
    // setvbuf must be called before any I/O on the stream.
    static char my_buffer[BUFSIZ];
    if (setvbuf(stdout, my_buffer, _IOFBF, sizeof my_buffer) != 0) {
        fputs("setvbuf failed\n", stderr);
    }

    printf("this may sit in the buffer");
    fflush(stdout);                     // force it out now
    putchar('\n');

    // _IONBF (unbuffered), _IOLBF (line-buffered), _IOFBF (fully buffered)
    setvbuf(stderr, nullptr, _IONBF, 0);

    // fflush(nullptr) flushes every output stream.
    fflush(nullptr);
    return 0;
}

The consequences of buffering that actually bite:

  • A crash loses buffered output. If a program dies between printf and the flush, the message never appears — which is why debugging output belongs on unbuffered stderr.

  • _Exit and abort do not flush; exit and returning from main do.

  • Interleaving stdout and stderr gives surprising order when stdout is redirected (fully buffered) while stderr is not.

  • fflush on an input stream is undefined in standard C (POSIX defines it as discarding buffered input). There is no portable way to "clear the input buffer" — read and discard to the newline instead.

EOF vs. Error

The single most common <stdio.h> bug is using feof as a loop condition. feof only becomes true after a read has already failed at end of file:

#include <stdio.h>

int main(void)
{
    FILE *f = stdin;
    int c;

    // WRONG -- processes the final item twice, because feof is only set after
    // a read has already hit the end:
    //   while (!feof(f)) { c = fgetc(f); putchar(c); }

    // RIGHT -- test the read itself, then ask WHY it stopped:
    while ((c = fgetc(f)) != EOF) {
        putchar(c);
    }

    if (ferror(f)) {
        fputs("read error\n", stderr);
        clearerr(f);                    // reset both flags
        return 1;
    }
    if (feof(f)) {
        fputs("clean end of input\n", stderr);
    }
    return 0;
}

Every reading function signals the same way: fgetc/getchar return EOF, fgets returns nullptr, fread returns a short count, scanf returns EOF. In each case, feof and ferror tell you which of the two happened.

See Also