Part of C in 100s

C in 100 Seconds: File I/O Writing

Celest KimCelest Kim

Video: C in 100 Seconds: File I/O Writing — fprintf, fputs, Append | Episode 36 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C File I/O Writing: fprintf, fputs, Append

FILE *f = fopen("out", "w"); to create. fprintf(f, "fmt", args) for formatted writes. fputs("text", f) for plain. Mode "a" to append.

The write-side counterparts to fgets/fgetc. Same FILE * API, different functions.

The basic shape

#include <stdio.h>

int main() {
  // Write formatted
  FILE *f = fopen("/tmp/output.txt", "w");
  if (f == NULL) {
    perror("fopen");
    return 1;
  }
  fprintf(f, "Name: %s\n", "Alice");
  fprintf(f, "Age: %d\n", 30);
  fprintf(f, "Score: %.1f\n", 95.5);
  fclose(f);

  // Write plain strings
  FILE *f2 = fopen("/tmp/notes.txt", "w");
  fputs("First line\n", f2);
  fputs("Second line\n", f2);
  fclose(f2);

  // Append mode
  FILE *f3 = fopen("/tmp/notes.txt", "a");
  fputs("Appended line\n", f3);
  fclose(f3);

  return 0;
}

Mode "w" — write (truncate)

FILE *f = fopen("/tmp/out.txt", "w");
  • Creates the file if it doesn't exist.
  • Truncates the file if it does — wipes all content to zero size.
  • Cursor starts at the beginning.

Use "w" when you want a fresh file. Beware: opening a file with "w" destroys its contents instantly, even before you write anything.

Mode "a" — append

FILE *f = fopen("/tmp/log.txt", "a");
fputs("new line\n", f);
fclose(f);
  • Creates the file if it doesn't exist.
  • Preserves existing content.
  • Cursor positioned at end; all writes append.

Use "a" for log files, history files, anything that grows over time.

The "a" mode also has a special property: even with seeks, writes always go to the end. Atomic-append behavior on most systems.

fprintf — formatted writes

fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fprintf(f, "Score: %.1f\n", 95.5);

fprintf is printf for files. Same format specifiers (%d, %s, %f, etc.). The first argument is the FILE *.

For stdout: fprintf(stdout, ...) is the same as printf(...). For stderr: fprintf(stderr, ...).

fputs — write a string

fputs("Hello\n", f);

fputs(s, f) writes string s to f. Doesn't add a newline — include \n yourself.

Returns non-negative on success, EOF on failure.

For "write a string verbatim, no formatting," fputs is faster than fprintf("%s", s) — no parsing.

fputc — write one character

fputc('A', f);

Writes one byte. Returns the byte (cast to unsigned char) or EOF on failure.

For binary output, use fwrite (episode 37).

Buffered output

fputs("hello", f);
fputs("world", f);
fclose(f);

stdio buffers writes — they may not hit disk until fclose or fflush. For most code, this is fine.

When buffering matters:

  • Crash recovery. If your program crashes between writes, buffered data is lost.
  • Cross-process visibility. Other processes reading the file see a stale view until you flush.
  • Real-time logging. Logs that arrive late are useless.

To force a flush:

fprintf(f, "%s\n", "important");
fflush(f);   // ensure it hits disk

For per-line flushing on logs: setvbuf(f, NULL, _IOLBF, 0) to switch to line-buffered mode.

Error handling on writes

if (fprintf(f, "%d\n", n) < 0) {
  perror("fprintf");
}

fprintf returns the number of bytes written, or negative on error. Most code ignores this; for critical writes (config files, save games), check.

A subtler case: errors only show up at fclose:

fputs("...", f);   // appears to succeed
// disk fills up
fputs("...", f);   // also appears to succeed (buffered)
if (fclose(f) == EOF) {
  // NOW we discover the disk is full
}

Always check fclose's return value if data integrity matters.

Atomic file writes

// Write to temp, then rename
FILE *f = fopen("/tmp/data.tmp", "w");
fprintf(f, "...");
fclose(f);
rename("/tmp/data.tmp", "/tmp/data.json");

rename is atomic on most filesystems — either the new file replaces the old, or it doesn't. No half-written intermediate state visible to other readers.

This is the standard "save without corruption" pattern.

Path manipulation

For dynamic paths, build with snprintf:

char path[256];
snprintf(path, sizeof(path), "/tmp/log_%s_%d.txt", date, pid);
FILE *f = fopen(path, "w");

Avoids hardcoding paths and accidental buffer overflows.

Writing line-by-line from a list

char *names[] = {"Alice", "Bob", "Charlie"};
FILE *f = fopen("names.txt", "w");
for (int i = 0; i < 3; i++) {
  fprintf(f, "%s\n", names[i]);
}
fclose(f);

Standard "loop and write" pattern.

Common mistakes

Truncating accidentally with "w". "Why is my data gone?" — opened with "w" instead of "a" or "r+".

Forgetting \n with fputs. Lines run together with no separation.

Not closing the file. Buffered writes may not flush. Disk space may not be released.

Race conditions. Two processes opening "w" simultaneously may interleave or truncate each other. Use atomic-rename for shared files.

Ignoring write errors. fprintf can fail (disk full, permission lost, network drop). Check return value for critical data.

Holding the file open forever. A long-running daemon that opens a log on startup and never reopens won't notice if the log gets rotated. For log rotation, reopen periodically.

What's next

Episode 37: binary file I/O — fread and fwrite. Reading and writing structured binary data.

Recap

fopen(path, "w") truncates and creates; "a" appends. fprintf(f, fmt, args) for formatted; fputs(s, f) for plain (no auto-newline); fputc(c, f) for one byte. Buffered output — fflush to force disk write. Atomic save: write to temp, then rename. Always check fclose's return for critical writes. Use snprintf to build paths.

Next episode: binary file I/O.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.