How Do I Read and Write Files in C?
fopen, fclose, fprintf, fscanf — getting data in and out of files
After this lesson, you will be able to:
- Open and close files using
fopen/fclosewith correct mode andNULLcheck - Write to files with
fprintfand read withfscanf, checking return values - Read lines safely with
fgetsand strip the trailing newline - Explain why
while (!feof(fp))is a bug
Beyond the Keyboard
Every program so far has read from the keyboard (stdin) and written to the screen (stdout). Real programs read configuration files, process data files, and write reports. C’s file I/O functions mirror printf/scanf — once you know those, files are straightforward.
File Operations
Opening and Closing Files
FILE *fp = fopen("students.txt", "r"); // Open for reading
if (fp == NULL)
{
fprintf(stderr, "Error: cannot open students.txt\n");
return 1;
}
// ... read from fp ...
fclose(fp); // Always close when done
File modes:
| Mode | Meaning |
|---|---|
"r" |
Read (file must exist) |
"w" |
Write (creates or truncates) |
"a" |
Append (creates or adds to end) |
Common Pitfall: Always check if
fopenreturns NULL. The file might not exist, you might not have permission, or the path might be wrong. Skipping this check leads to crashes when you try to read from a NULL pointer.
fopen("data.txt", "w") do if data.txt already exists and contains 100 lines of data?"w" mode truncates existing files — all content is destroyed before you write anything. This is one of the most common data-loss bugs. If you want to add to an existing file, use "a" (append). Option A describes append mode, not write mode.
Writing to Files
fprintf works exactly like printf, but writes to a file:
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) { /* handle error */ }
fprintf(fp, "Name: %s\n", name);
fprintf(fp, "GPA: %.2f\n", gpa);
fclose(fp);
Reading from Files
fscanf — formatted reading (like scanf):
FILE *fp = fopen("data.txt", "r");
int id;
double gpa;
while (fscanf(fp, "%d %lf", &id, &gpa) == 2)
{
printf("ID: %d, GPA: %.2f\n", id, gpa);
}
fclose(fp);
fgets — read a line (safer):
char line[256];
while (fgets(line, sizeof(line), fp) != NULL)
{
line[strcspn(line, "\n")] = '\0'; // Strip newline
printf("Line: %s\n", line);
}
The Trick:
fgetsis generally safer thanfscanffor reading text. It prevents buffer overflows and handles lines with spaces. Read the whole line withfgets, then parse it withsscanforstrtok.
Why does this matter?
Almost every lab from here on reads data from files. The fgets + sscanf pattern is your workhorse — it reads a line safely, then parses it. Getting comfortable with this pattern now saves you debugging time on every future assignment.
fscanf returns the number of items successfully read — when it returns something other than 1, you're done. Option A is the classic feof trap: feof only returns true after a read has already failed, so the last iteration processes garbage. Option C has the fgets arguments in the wrong order. Option D checks if the file pointer is NULL, which it isn't once opened — this loops forever.
The feof Trap
Common Pitfall: Never use
while (!feof(fp))as your loop condition.feofreturns true only after a read has already failed — so the last iteration processes garbage data. Instead, check the return value of your read function:
// WRONG:
while (!feof(fp))
{
fscanf(fp, "%d", &value); // Processes garbage on last iteration
}
// RIGHT:
while (fscanf(fp, "%d", &value) == 1)
{
// Process value
}
Deep dive: File I/O connects to shell redirection
Remember from Lesson 1.5: printf writes to stdout and shell redirection (>) sends stdout to a file. File I/O functions are the C-level version of the same mechanism:
| Shell concept | C equivalent | Effect |
|---|---|---|
printf(...) |
fprintf(stdout, ...) |
Write to screen |
./prog > file.txt |
fprintf(fp, ...) |
Write to file |
scanf(...) |
fscanf(stdin, ...) |
Read from keyboard |
./prog < input.txt |
fscanf(fp, ...) |
Read from file |
fprintf(stderr, ...) |
./prog 2> err.txt |
Error output |
In fact, printf("Hello") is literally defined as fprintf(stdout, "Hello") and scanf("%d", &x) is fscanf(stdin, "%d", &x). The stdin, stdout, and stderr streams from Lesson 1.5 are just FILE * pointers that the C runtime opens for you automatically.
This also explains why perror() writes to stderr — error messages should go to a separate stream so they aren’t captured when you redirect stdout to a file:
FILE *fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("data.txt"); // Writes to stderr, not stdout
return 1;
}
When you run ./program > output.txt, the perror message still appears on screen (stderr) while normal output goes to the file (stdout).
Reading Structs from Files
typedef struct
{
char name[50];
int id;
double gpa;
} Student;
int read_students(const char *filename, Student roster[], int max)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL) return -1;
int count = 0;
char line[256];
while (count < max && fgets(line, sizeof(line), fp) != NULL)
{
line[strcspn(line, "\n")] = '\0';
sscanf(line, "%49s %d %lf",
roster[count].name,
&roster[count].id,
&roster[count].gpa);
count++;
}
fclose(fp);
return count;
}
From Java: In Java, you’d use
Scanner scanner = new Scanner(new File("data.txt"))withscanner.nextLine()andscanner.nextInt(). C’sfopen/fgets/fscanfserve the same purpose — but you manage the file handle yourself (no automatic closing, no try-with-resources). Alwaysfclosewhen done.
read_students function above, what does line[strcspn(line, "\n")] = '\0'; do?fgets includes the newline character in the buffer. strcspn(line, "\n") returns the index of the first newline (or the string length if none). Setting that position to '\0' effectively chops off the newline. This is a standard idiom you'll use every time you read lines with fgets.
Quick Check: Why should you always check fopen's return value?
fopen returns NULL if it can’t open the file (doesn’t exist, no permission, bad path). Passing NULL to fprintf or fscanf causes a crash. Checking lets you print a meaningful error message instead.
Quick Check: What's wrong with while (!feof(fp))?
feof returns true only after a read has already failed, not before. The last iteration of the loop reads garbage because the previous read hit EOF but feof hasn’t been checked yet. Check the read function’s return value instead.
Quick Check: What's the difference between "w" and "a" modes?
"w" truncates the file (erases existing content) and writes from the beginning. "a" appends to the end without erasing. Both create the file if it doesn’t exist.
Big Picture: File I/O transforms your programs from interactive toys into tools that process real data. Every database, compiler, and web server reads and writes files using the same primitives you just learned. Combined with structs, you can build programs that load, process, and save structured records — which is what most real software does.
Files Connect Programs to Data
File I/O is how your programs interact with the outside world — reading data sets, writing reports, processing logs. Combined with structs, you can read structured records from files, manipulate them in memory, and write results back.
Next: sorting structs. You’ll sort your struct arrays by different fields — name, ID, GPA — using the selection sort you learned in Lesson 2.14, adapted for structured data.