File I/O (stdio)
Reading and writing files in C using FILE pointers, fopen/fclose, fprintf/fscanf, fgets/fputs, and EOF handling.
Files are just streams with a name
Think of a file like a printed document on your desk. fopen is the act of picking the document up and opening it to a specific page. The FILE* pointer is your finger marking where you are in the document. fprintf/fscanf are writing on or reading from that page. fclose is putting the document back in the filing cabinet. If you never put it back (never call fclose), the filing cabinet fills up — that is a resource leak.
In Python you used open("file.txt", "r") and a with block that automatically closes the file for you. In C, you call fopen, use the file, and must manually call fclose — there is no automatic cleanup. The FILE* type is an opaque pointer (you never look inside the struct, just pass it around). Every read/write function takes a FILE* as its first or last argument.
The connection to standard I/O you already know: stdin, stdout, and stderr are all FILE* values pre-opened by the runtime. So fprintf(stdout, "hello\n") is exactly the same as printf("hello\n"), and fscanf(stdin, "%d", &x) is exactly the same as scanf("%d", &x). File I/O is just the same model extended to named files on disk.
Buffering: C file I/O is buffered — data you write with fprintf may sit in an in-memory buffer and not reach the file until the buffer is full or you call fflush or fclose. This is why your file may appear empty if your program crashes without closing it. Always fclose properly.
# Python — with auto-close
with open("data.txt", "r") as f:
line = f.readline()
for line in f:
print(line, end="")
with open("out.txt", "w") as f:
f.write("Hello file\n")
print("done", file=f)
/* C — manual open/close */
FILE *f = fopen("data.txt", "r");
if (f == NULL) { /* error */ }
char buf[256];
while (fgets(buf, 256, f)) {
printf("%s", buf);
}
fclose(f); /* MUST call this */
FILE *out = fopen("out.txt", "w");
fprintf(out, "Hello file\n");
fclose(out);
If the file doesn't exist (for "r"), you lack permissions, or the disk is full (for "w"), fopen returns NULL. Passing NULL to fprintf/fgets/fclose causes undefined behavior (usually a segfault). Always check: if (f == NULL) { perror("fopen"); return 1; }
fopen = open/pick up the file. FILE* = your bookmark in it. fprintf/fscanf/fgets = read or write. fclose = put it away. Check for NULL after fopen. Always fclose. That is the complete mental model.
File I/O functions — complete reference
/* Opening a file */ FILE *fopen(const char *path, const char *mode); // ^ returns FILE* or NULL on failure // modes: "r"=read, "w"=write(truncate), "a"=append, "r+"=read+write /* Closing — ALWAYS do this */ int fclose(FILE *fp); // returns 0 on success, EOF on error /* Formatted I/O (like printf/scanf but to/from file) */ int fprintf(FILE *fp, const char *fmt, ...); int fscanf(FILE *fp, const char *fmt, ...); /* Line-based I/O — preferred over fscanf for strings */ char *fgets(char *buf, int n, FILE *fp); // reads up to n-1 chars, stops at newline, returns NULL at EOF int fputs(const char *s, FILE *fp); /* Binary I/O */ size_t fread(void *buf, size_t size, size_t n, FILE *fp); size_t fwrite(const void *buf, size_t size, size_t n, FILE *fp); /* Status checks */ int feof(FILE *fp); /* non-zero if at end of file */ int ferror(FILE *fp); /* non-zero if error occurred */ int fflush(FILE *fp); /* flush buffer to disk/screen now */
File Positioning — fseek, ftell, rewind
/* ftell — returns current file position (bytes from start of file) */ long ftell(FILE *fp); /* returns -1L on error */ /* fseek — moves the file position indicator */ int fseek(FILE *fp, long offset, int whence); /* whence options: */ /* SEEK_SET — offset from the BEGINNING of file */ /* SEEK_CUR — offset from the CURRENT position (can be negative) */ /* SEEK_END — offset from the END of file (use negative offset) */ /* Returns: 0 on success, nonzero on failure */ /* rewind — equivalent to fseek(fp, 0, SEEK_SET) + clears error indicator */ void rewind(FILE *fp); /* ── Common pattern: get file size ── */ fseek(fp, 0, SEEK_END); long file_size = ftell(fp); /* position = total bytes */ rewind(fp); /* go back to start for reading */ /* ── Seek backward from current position ── */ char buf[10]; fread(buf, 1, 10, fp); fseek(fp, -10, SEEK_CUR); /* back 10 bytes: position unchanged */
| Call | Moves to |
|---|---|
fseek(fp, 0, SEEK_SET) | Start of file |
fseek(fp, 0, SEEK_END) | End of file |
fseek(fp, -5, SEEK_END) | 5 bytes before end |
fseek(fp, -5, SEEK_CUR) | 5 bytes before current |
rewind(fp) | Start of file (+ clear error) |
File Open Modes Reference
| Mode | Action | File must exist? | Creates if missing? |
|---|---|---|---|
"r" | Read only, from beginning | Yes — fails (NULL) if not | No |
"w" | Write only, truncates to zero length | No | Yes |
"a" | Append — writes go to end | No | Yes |
"r+" | Read and write, from beginning | Yes | No |
"w+" | Read and write, truncates | No | Yes |
"rb" | Read binary (add b to any mode) | Yes | No |
Complete programs you can compile and run
#include <stdio.h>
int main(void) {
/* Open file for writing — creates or truncates */
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) {
perror("fopen"); /* prints "fopen: No such file or directory" etc */
return 1;
}
/* Write formatted data — just like printf but first arg is FILE* */
fprintf(fp, "Alice 95\n");
fprintf(fp, "Bob 82\n");
fprintf(fp, "Carol %d\n", 77);
fclose(fp); /* flush buffer and release file handle */
printf("scores.txt written successfully.\n");
return 0;
}
Bob 82
Carol 77
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
char line[256];
int linecount = 0;
/* fgets reads one line at a time, returns NULL at EOF */
while (fgets(line, sizeof(line), fp) != NULL) {
linecount++;
/* line includes the '\n' — printf will print it too */
printf("Line %d: %s", linecount, line);
}
/* Check if loop ended due to error or just EOF */
if (ferror(fp)) {
printf("Error reading file.\n");
}
fclose(fp);
printf("Total lines: %d\n", linecount);
return 0;
}
Line 2: Bob 82
Line 3: Carol 77
Total lines: 3
#include <stdio.h>
int main(void) {
FILE *src = fopen("scores.txt", "r");
FILE *dst = fopen("scores_copy.txt", "w");
if (src == NULL || dst == NULL) {
perror("fopen");
if (src) fclose(src);
if (dst) fclose(dst);
return 1;
}
int c;
/* getc(fp) reads one char from file — like getchar() but for any FILE* */
while ((c = getc(src)) != EOF) {
putc(c, dst); /* write one char to destination file */
}
fclose(src);
fclose(dst);
printf("File copied.\n");
return 0;
}
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.txt", "r");
if (fp == NULL) { perror("fopen"); return 1; }
char name[50];
int score;
int total = 0, count = 0;
/* fscanf returns number of items read; 2 on success, EOF at end */
while (fscanf(fp, "%49s %d", name, &score) == 2) {
printf("%-10s scored %d\n", name, score);
total += score;
count++;
}
fclose(fp);
if (count > 0) {
printf("Average: %.1f\n", (double)total / count);
}
return 0;
}
Bob scored 82
Carol scored 77
Average: 84.7
Practice problems with solutions
Write a C program that opens a file called words.txt and counts the total number of words in it. A "word" is any whitespace-delimited token. Print the count. Handle the case where the file cannot be opened.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("words.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
char word[256];
int count = 0;
/* fscanf with %s reads one whitespace-delimited token at a time */
while (fscanf(fp, "%255s", word) == 1) {
count++;
}
fclose(fp);
printf("Word count: %d\n", count);
return 0;
}
fscanf(fp, "%s", word) skips whitespace and reads one token, returning 1 on success and EOF when there is no more input. The %255s limits to 255 characters to prevent buffer overflow. Always check fopen's return value.
The code below has a resource leak. Identify it and fix it.
#include <stdio.h>
int count_lines(const char *path) {
FILE *fp = fopen(path, "r");
if (fp == NULL) return -1;
char buf[512];
int n = 0;
while (fgets(buf, sizeof(buf), fp)) n++;
return n; /* BUG: what is missing here? */
}
#include <stdio.h>
int count_lines(const char *path) {
FILE *fp = fopen(path, "r");
if (fp == NULL) return -1;
char buf[512];
int n = 0;
while (fgets(buf, sizeof(buf), fp)) n++;
fclose(fp); /* FIX: always close before returning */
return n;
}
fopen allocates an OS file descriptor and a FILE buffer. If you never call fclose, that descriptor is never released. The OS has a limited number of file descriptors per process (typically 1024). If count_lines is called many times in a loop, the program will run out of file descriptors and all subsequent fopen calls will return NULL.
Write a function log_event(const char *msg) that appends a message to a file called log.txt with a timestamp prefix formatted as [EVENT] msg\n. The file must not be truncated between calls.
#include <stdio.h>
void log_event(const char *msg) {
/* "a" mode: file is created if missing, writes always go to the end */
FILE *fp = fopen("log.txt", "a");
if (fp == NULL) {
perror("log_event: fopen");
return;
}
fprintf(fp, "[EVENT] %s\n", msg);
fclose(fp);
}
int main(void) {
log_event("Server started");
log_event("User logged in");
log_event("Request received");
return 0;
}
"w" truncates the file to zero length each time — you would lose previous log entries. Mode "a" always seeks to the end before writing, so entries accumulate. Even if the file doesn't exist yet, "a" creates it.
A file numbers.txt contains one integer per line. Write a program that reads all integers, computes their sum and average, and prints them. Handle an empty file gracefully (print "No numbers found").
#include <stdio.h>
int main(void) {
FILE *fp = fopen("numbers.txt", "r");
if (fp == NULL) { perror("fopen"); return 1; }
int val, sum = 0, count = 0;
while (fscanf(fp, "%d", &val) == 1) {
sum += val;
count++;
}
fclose(fp);
if (count == 0) {
printf("No numbers found.\n");
} else {
printf("Sum: %d\n", sum);
printf("Average: %.2f\n", (double)sum / count);
}
return 0;
}
fscanf(fp, "%d", &val) == 1 reads until the format fails or EOF. Cast to double for the division to get a real average — integer division would truncate. Always fclose before returning.
Key concepts to memorize
Test your understanding
fopen return if the file cannot be opened?LO1fprintf(stdout, "hi\n") is equivalent to printf("hi\n").LO1fgets return when it reaches end-of-file?LO8fgets preferred over fscanf(fp, "%s", buf) for reading strings from a file?LO8