Part of C in 100s

C in 100 Seconds: File I/O Reading

Celest KimCelest Kim

Video: C in 100 Seconds: File I/O Reading — fopen, fgets, fgetc | Episode 35 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 Reading: fopen, fgets, fgetc

FILE *f = fopen("file", "r"); opens. fgets(buf, size, f) reads a line. fgetc(f) reads one char. Always check NULL after fopen; always fclose when done.

C's file I/O API is small but covers the cases. Today's lesson: opening a file, reading line-by-line and char-by-char.

The basic shape

#include <stdio.h>

int main() {
  FILE *f = fopen("/tmp/sample.txt", "r");
  if (f == NULL) {
    printf("Could not open file\n");
    return 1;
  }

  char line[256];
  int count = 0;
  while (fgets(line, sizeof(line), f) != NULL) {
    count++;
    printf("%d: %s", count, line);
  }
  fclose(f);
  printf("\n%d lines read\n", count);

  return 0;
}

fopen

FILE *fopen(const char *path, const char *mode);

Opens path and returns a FILE * (file pointer). Returns NULL on failure (file doesn't exist, permission denied, etc.).

Modes:

  • "r" — read; file must exist.
  • "w" — write; creates new or truncates existing.
  • "a" — append; creates if missing.
  • "r+" — read and write; file must exist.
  • "w+" — read and write; truncates.
  • "a+" — read and append.

For binary files, add b: "rb", "wb", "rb+". On Unix systems, b is a no-op; on Windows, it disables CRLF translation.

Always check for NULL

FILE *f = fopen("missing.txt", "r");
if (f == NULL) {
  perror("fopen");   // prints "fopen: No such file or directory"
  return 1;
}

fopen failure is common — file doesn't exist, can't read, etc. Always handle. perror prints a system error message based on errno (episode 39).

fgets — read a line

char *fgets(char *str, int n, FILE *stream);

Reads up to n - 1 bytes or until a newline. Includes the newline in the result. Always null-terminates. Returns NULL on EOF or error.

char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
  printf("%s", line);   // line includes \n
}

fgets is the safe line reader — bounded, predictable. Episode 13 covered it for stdin; same API for files.

fgetc — read one character

int fgetc(FILE *stream);

Reads one byte; returns it as int (or EOF on EOF/error).

int ch;
int chars = 0;
while ((ch = fgetc(f)) != EOF) {
  chars++;
}

Note ch is int, not char. EOF is typically -1, which can't fit in unsigned char cleanly — using int lets you distinguish "byte 0xFF" from "EOF."

Reading the whole file

fseek(f, 0, SEEK_END);          // jump to end
long size = ftell(f);            // get position (= size)
fseek(f, 0, SEEK_SET);          // back to start

char *buf = malloc(size + 1);
fread(buf, 1, size, f);
buf[size] = '\0';

// use buf
free(buf);

fseek moves the file position; ftell reports it. fread(ptr, size, count, f) reads count items of size bytes each.

For very large files, prefer line-by-line — loading 10GB into memory rarely ends well.

fclose

fclose(f);

Releases the file descriptor and flushes any pending writes. Always call when done, especially after writing — output may be buffered until close (or fflush).

fclose(NULL) is undefined. Set to NULL after close for safety.

stdin, stdout, stderr

fprintf(stderr, "error: %s\n", msg);   // to stderr
fputs("hello", stdout);                  // to stdout (= printf without format)
fgets(buf, 100, stdin);                  // from stdin

The three default streams are FILE * constants. Same API as user-opened files.

Reading numbers from a file

FILE *f = fopen("nums.txt", "r");
int n;
while (fscanf(f, "%d", &n) == 1) {
  printf("Got: %d\n", n);
}
fclose(f);

fscanf is scanf for files. Same format string semantics; same pitfalls.

For complex parsing, read with fgets and parse with sscanf — separate I/O from parsing.

Error handling pattern

int read_config(const char *path) {
  FILE *f = fopen(path, "r");
  if (f == NULL) {
    fprintf(stderr, "config: cannot open %s: %s\n", path, strerror(errno));
    return -1;
  }

  char line[256];
  int rc = 0;
  while (fgets(line, sizeof(line), f) != NULL) {
    // parse line
    if (parse_failed) {
      rc = -1;
      goto cleanup;
    }
  }

cleanup:
  fclose(f);
  return rc;
}

goto cleanup for unified cleanup. Standard C idiom for resource management.

Detecting end-of-file vs error

char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
  // process
}

if (feof(f)) {
  printf("Reached end of file\n");
} else if (ferror(f)) {
  printf("Read error\n");
}

Both EOF and read errors return NULL from fgets. Use feof or ferror to distinguish if needed.

Common mistakes

Not checking fopen return. Crash on NULL deref.

Forgetting to close. Leaked file descriptor (limited number per process). On writes, output may not flush.

Using char ch instead of int ch for fgetc. EOF is -1, doesn't fit in unsigned char. Use int.

Reading without bounds. fgets(line, 1000000, f) — works, but if line is only 100 bytes, the second arg should be sizeof(line) = 100.

Mixing read modes. r+ requires fseek between reads and writes; otherwise undefined.

Buffered output not flushed. Writes to stdout may not appear immediately. Call fflush(stdout) or write \n (line-buffered terminals flush on newline).

Trying to fclose stdin/stdout/stderr. Usually works but is surprising. Don't unless you're sure.

What's next

Episode 36: file I/O writing — fprintf, fputs, append. Producing output files.

Recap

fopen(path, mode) — modes r, w, a, r+, etc. (add b for binary on Windows). Always check for NULL. fgets(buf, size, f) for safe line reading; fgetc(f) for one char (returns int, watch for EOF). For full-file reads, fseek + ftell to size, then fread. Always fclose when done. stdin/stdout/stderr are predefined FILE * you can pass to any of these.

Next episode: writing files.

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.