C in 100 Seconds: File I/O Reading
Video: C in 100 Seconds: File I/O Reading — fopen, fgets, fgetc | Episode 35 by Taught by Celeste AI - AI Coding Coach
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; alwaysfclosewhen 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.