File I/O
A file is like a safe — you must open it before use (fopen), follow the rules for storing and retrieving items (read/write modes), and always lock it when done (fclose), or your data may be lost.
Basic Concepts of Files
C treats all external devices as "streams". There are two types of file streams:
- Text stream: composed of visible characters, newline characters may be translated, suitable for human reading
- Binary stream: raw byte sequence, read and written as-is, suitable for program processing
The operating system manages open files through a FILE structure, and we operate on it via a FILE * pointer.
File Pointer
FILE *fp;
This pointer is the "key" to all subsequent file operations. Without it, nothing can be done.
Opening and Closing Files
fopen
FILE *fopen(const char *filename, const char *mode);
| Mode | Meaning | If file doesn't exist | If file exists |
|---|---|---|---|
"r" |
Read only | Error | Read from beginning |
"w" |
Write only | Create | Truncate content |
"a" |
Append | Create | Append at end |
"r+" |
Read and write | Error | Operate from beginning |
"w+" |
Read and write | Create | Truncate content |
"a+" |
Read and append | Create | Append at end |
"rb" |
Binary read only | Error | Read from beginning |
"wb" |
Binary write only | Create | Truncate content |
"ab" |
Binary append | Create | Append at end |
"w" mode truncates existing files! This is the most common mistake beginners make — using "w" when they meant to append, resulting in all data being lost.
fclose
int fclose(FILE *fp);
Closing a file flushes the buffer and releases resources. Returns 0 on success, EOF on failure.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp);
return 0;
}
fopen must have a corresponding fclose. Forgetting to close a file causes memory leaks and data loss.
Text File I/O
Character I/O: fgetc / fputc
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);
fgetc reads one character, returning EOF at the end of file. fputc writes one character.
String I/O: fgets / fputs
char *fgets(char *str, int n, FILE *fp);
int fputs(const char *str, FILE *fp);
fgets reads at most n-1 characters, stopping at a newline or end of file, and automatically appends '\0'.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("poem.txt", "w");
if (fp == NULL) {
return 1;
}
fputs("Riverside at sunset\n", fp);
fputs("River flows to the sea\n", fp);
fclose(fp);
fp = fopen("poem.txt", "r");
if (fp == NULL) {
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
Riverside at sunset
River flows to the sea
Binary File I/O
fread / fwrite
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
Parameters: ptr is the data buffer, size is the byte size of each element, count is the number of elements. The return value is the actual number of elements read or written.
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student stu = {1, "Alice", 92.5f};
FILE *fp = fopen("student.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(&stu, sizeof(Student), 1, fp);
fclose(fp);
Student read_stu;
fp = fopen("student.dat", "rb");
if (fp == NULL) {
return 1;
}
fread(&read_stu, sizeof(Student), 1, fp);
fclose(fp);
printf("ID: %d, Name: %s, Score: %.1f\n",
read_stu.id, read_stu.name, read_stu.score);
return 0;
}
ID: 1, Name: Alice, Score: 92.5
Formatted I/O
fprintf / fscanf
int fprintf(FILE *fp, const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);
Their usage is identical to printf/scanf, just with an additional file pointer parameter.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) {
return 1;
}
fprintf(fp, "%s %d %.1f\n", "Bob", 2, 88.0);
fprintf(fp, "%s %d %.1f\n", "Carol", 3, 95.5);
fclose(fp);
fp = fopen("scores.txt", "r");
if (fp == NULL) {
return 1;
}
char name[20];
int id;
float score;
while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
printf("Name: %s, ID: %d, Score: %.1f\n", name, id, score);
}
fclose(fp);
return 0;
}
Name: Bob, ID: 2, Score: 88.0
Name: Carol, ID: 3, Score: 95.5
File Position Pointer
Every open file has a "position pointer" indicating the current read/write location. It's like a bookmark while reading — wherever you've read to, the bookmark marks that spot.
fseek
int fseek(FILE *fp, long offset, int origin);
| origin | Meaning |
|---|---|
SEEK_SET |
Beginning of file |
SEEK_CUR |
Current position |
SEEK_END |
End of file |
ftell / rewind
long ftell(FILE *fp);
void rewind(FILE *fp);
ftell returns the current position (byte offset from the beginning of the file). rewind is equivalent to fseek(fp, 0, SEEK_SET).
Example
Using fseek and ftell to calculate file size:
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "rb");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
rewind(fp);
printf("File size: %ld bytes\n", size);
fclose(fp);
return 0;
}
Example
Random access to a specific record in a binary file:
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student students[3] = {
{1, "Alice", 92.5f},
{2, "Bob", 88.0f},
{3, "Carol", 95.5f}
};
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(students, sizeof(Student), 3, fp);
fclose(fp);
fp = fopen("students.dat", "rb");
if (fp == NULL) {
return 1;
}
int target = 2;
fseek(fp, (target - 1) * sizeof(Student), SEEK_SET);
Student s;
fread(&s, sizeof(Student), 1, fp);
fclose(fp);
printf("Record %d: ID=%d, Name=%s, Score=%.1f\n",
target, s.id, s.name, s.score);
return 0;
}
Record 2: ID=2, Name=Bob, Score=88.0
Other File Functions
feof
int feof(FILE *fp);
Checks whether the end of file has been reached. Returns non-zero at EOF. Note: feof only becomes true after a read operation has failed, not as a pre-check.
fflush
int fflush(FILE *fp);
Forces a flush of the buffer, writing buffered data to the file. Calling fflush on an input stream results in undefined behavior.
remove / rename
int remove(const char *filename);
int rename(const char *old, const char *new);
Delete and rename files. Return 0 on success.
General Pattern for File Operations
Most file operations follow this pattern:
- Open the file with
fopenand check the return value - Read or write data in a loop
- Close the file with
fclose
#include <stdio.h>
int main(void) {
FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
return 0;
}
fopen return value is an absolute rule! The file might not exist, you might lack permissions, or the disk could be full — using a null pointer without checking will crash immediately.
❓ FAQ
\n and \r\n during read/write, while binary mode performs no translation. On Linux, there is no difference between the two.📖 Summary
- Open files with
fopen, and always check if the return value isNULL - Use
fgets/fputs,fprintf/fscanffor text mode, andfread/fwritefor binary mode "w"mode truncates the file,"a"mode appends at the end — don't mix them upfseek/ftell/rewindcontrol the file position pointer for random access- Every
fopenmust have a correspondingfclose— make it a habit
📝 Exercises
- Write a program that counts the number of lines, words, and characters in a text file
- Implement a simple file copy program that reads a source file in binary mode and writes to a destination file
- Write a program that saves an array of structs to a binary file, then reads them back and prints them out



