File Handling
Everything a program stores in variables disappears when it ends. To keep data between runs, such as marks, settings or a day's sales, you save it in a file. C's file functions come from the same stdio.h header you already use for printf, and they work in a very similar way.
In this lesson you will open, write, read and close text files, handle errors properly, read comma-separated data, append to files, and store structures in binary files.
Opening and closing a file
fopen opens a file and returns a FILE *, a pointer to a structure that the library uses to track the open file. If the file cannot be opened, fopen returns a null pointer, so you must always check. When you are finished, fclose writes out any remaining data and releases the file.
The second argument of fopen is the mode:
| Mode | Meaning | If the file exists | If it does not exist |
|---|---|---|---|
"r" | Read | Reads from the start | Fails |
"w" | Write | Empties it first | Creates it |
"a" | Append | Adds to the end | Creates it |
"r+" | Read and write | Keeps the contents | Fails |
"w+" | Read and write | Empties it first | Creates it |
"wx" | Write, exclusive (C11) | Fails, protecting the file | Creates it |
Add b for binary files, as in "rb" or "wb". On Linux and macOS it makes no difference, but on Windows text mode converts line endings, so binary data must use b.
Writing a text file
fprintf works exactly like printf, but its first argument is the file to write to.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *names[] = {"Aarav", "Priya", "Kabir"};
int marks[] = {78, 91, 64};
FILE *fp = fopen("marks.txt", "w");
if (fp == nullptr) {
perror("marks.txt");
return EXIT_FAILURE;
}
for (int i = 0; i < 3; i++) {
fprintf(fp, "%s,%d\n", names[i], marks[i]);
}
fclose(fp);
printf("Saved 3 records to marks.txt\n");
return 0;
}Saved 3 records to marks.txt
The program prints only the confirmation; the data goes into the file. Open marks.txt in VS Code and you will see:
Aarav,78
Priya,91
Kabir,64The file is created in the program's working directory, the folder you ran it from, which is not always the folder holding the source code. fputs("text", fp) and fputc('x', fp) are also available for writing plain strings and single characters.
Reading a file line by line
fgets reads one line from a file into a buffer, just as it reads from the keyboard with stdin. It returns a null pointer at the end of the file, which makes a natural loop condition.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("marks.txt", "r");
if (fp == nullptr) {
perror("marks.txt");
return EXIT_FAILURE;
}
char line[100];
int number = 0;
while (fgets(line, sizeof line, fp) != nullptr) {
number++;
printf("%d: %s", number, line); // line already ends with '\n'
}
fclose(fp);
return 0;
}1: Aarav,78 2: Priya,91 3: Kabir,64
Reporting errors with perror
When fopen fails, the C library stores an error code in the variable errno. perror prints your message followed by a description of that error:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("attendance.txt", "r");
if (fp == nullptr) {
perror("Could not open attendance.txt");
return EXIT_FAILURE;
}
fclose(fp);
return 0;
}If the file does not exist, the output is:
Could not open attendance.txt: No such file or directory
Other common reasons are "Permission denied" and a wrong folder path. perror writes to stderr, the error stream, so the message still appears even when normal output is redirected to a file.
Reading structured data
Each line of marks.txt holds a name and a number separated by a comma, a simple CSV (comma-separated values) format. Read each line with fgets, then split it with sscanf:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("marks.txt", "r");
if (fp == nullptr) {
perror("marks.txt");
return EXIT_FAILURE;
}
char line[100];
char name[50];
int marks;
int count = 0, total = 0;
while (fgets(line, sizeof line, fp)) {
if (sscanf(line, "%49[^,],%d", name, &marks) == 2) {
printf("%-6s %3d\n", name, marks);
total += marks;
count++;
} else {
printf("Skipping bad line: %s", line);
}
}
fclose(fp);
if (count > 0) {
printf("Average of %d students: %.2f\n", count, (double) total / count);
}
return 0;
}Aarav 78 Priya 91 Kabir 64 Average of 3 students: 77.67
%49[^,] is a scanset: it reads up to 49 characters that are not a comma, which leaves room for the '\0' in the 50-character name. Checking that sscanf returned 2 means a damaged line is skipped instead of crashing the program or producing nonsense.
To add records without destroying the old ones, open the file in append mode: fopen("marks.txt", "a"). Every write then goes to the end of the file, which is how log files work.
Binary files: fwrite and fread
A text file stores numbers as characters. A binary file stores the raw bytes of your variables, which is compact and fast to load. fwrite(ptr, size, count, fp) writes count items of size bytes, and fread reads them back. Both return how many items they actually handled.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int roll;
int marks;
} Record;
int main(void)
{
Record out[] = {{101, 78}, {102, 91}, {103, 64}};
FILE *fp = fopen("records.bin", "wb");
if (fp == nullptr) {
perror("records.bin");
return EXIT_FAILURE;
}
size_t written = fwrite(out, sizeof out[0], 3, fp);
fclose(fp);
printf("Wrote %zu records\n", written);
Record in[3];
fp = fopen("records.bin", "rb");
if (fp == nullptr) {
perror("records.bin");
return EXIT_FAILURE;
}
size_t got = fread(in, sizeof in[0], 3, fp);
fseek(fp, 0, SEEK_END);
printf("File size: %ld bytes\n", ftell(fp));
fclose(fp);
for (size_t i = 0; i < got; i++) {
printf("Roll %d: %d\n", in[i].roll, in[i].marks);
}
return 0;
}Wrote 3 records File size: 24 bytes Roll 101: 78 Roll 102: 91 Roll 103: 64
Each Record is 8 bytes on common systems (two 4-byte int values), so three records take 24 bytes. fseek(fp, 0, SEEK_END) moves the file position to the end, and ftell reports that position, which is the file size. rewind(fp) moves back to the start. Binary files are not portable between systems with different type sizes or byte orders, and you must never write a structure that contains pointers, because the addresses mean nothing in the next run. For data you want to read or share, text formats are safer.
Common mistakes
- Not checking
fopen. Using a nullFILE *crashes the program. Check and callperror. - Opening with
"w"by accident. It empties an existing file immediately. Use"a"to add, or"wx"to refuse to overwrite. - Forgetting
fclose. Data may stay in memory buffers and never reach the disk, and open files are a limited resource. - Looping on
feof. Loop on the result offgets,fscanforfreadinstead. - Looking in the wrong folder. Relative file names are resolved from the working directory, not the source file's folder.
Key points
fopenreturns aFILE *or a null pointer; always check it andfclosethe file when done."r"reads,"w"creates or empties,"a"appends; addbfor binary data.- Write text with
fprintfand read it line by line withfgets, then parse withsscanf. perrorprints a clear reason, such as "No such file or directory", when an operation fails.fwriteandfreadmove raw bytes;fseek,ftellandrewindmove the file position.- Never loop on
feof; test the return value of the reading function.
Exercise
Create a text file expenses.csv (by hand or with a small program) containing the lines Rent,12000, Groceries,4350, Electricity,1150 and Internet,799. Write a program that reads it, prints each item with its amount aligned in columns, and prints the total. Report an error with perror if the file cannot be opened.
Show solution
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("expenses.csv", "r");
if (fp == nullptr) {
perror("expenses.csv");
return EXIT_FAILURE;
}
char line[100];
char item[40];
int amount;
int total = 0;
while (fgets(line, sizeof line, fp)) {
if (sscanf(line, "%39[^,],%d", item, &amount) == 2) {
printf("%-12s %6d\n", item, amount);
total += amount;
}
}
fclose(fp);
printf("%-12s %6d\n", "Total", total);
return 0;
}Rent 12000 Groceries 4350 Electricity 1150 Internet 799 Total 18299
The scanset %39[^,] reads the item name up to the comma, and the width stops it from overflowing item. Lines that do not match are ignored.
Quick Quiz
1. What does fopen("data.txt", "w") do if data.txt already contains text?
Mode "w" truncates an existing file to zero length; use "a" to append.
2. What does fgets return when it reaches the end of a file?
fgets returns a null pointer when no more characters can be read, which ends the loop.
3. Why is while (!feof(fp)) a bad reading loop?
The end-of-file flag is set by a failed read, so test the read function's result instead.
4. Which pair of functions reads and writes raw binary records?
fwrite and fread copy bytes directly between memory and the file.
Learn C with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
