C · Advanced
File Handling in C
In short: C reads and writes files through a FILE pointer: fopen opens a path in a mode such as "r", "w" or "a", fgets and fscanf read from it, fprintf and fputs write to it, and fclose flushes and releases it. Every fopen must be checked for NULL, and stdin, stdout and stderr are FILE pointers you can pass to the same functions.
Streams and the FILE pointer
A program that only reads the keyboard and prints to the screen forgets everything when it exits. Files are how a program keeps data between runs and exchanges it with other programs. C's standard library treats a file as a stream of bytes reached through a FILE *, an opaque handle that fopen returns and that every read and write function takes as an argument.
fopen(path, mode) opens the file and returns NULL if it cannot: the file does not exist, the directory is not writable, the path is wrong. That check is not optional, because passing NULL to fgets is undefined behaviour. The mode string decides what you may do. "r" reads an existing file, "w" creates the file or empties it if it exists, and "a" appends to the end, creating the file if needed. Adding + allows both reading and writing, and adding b opens in binary mode, which on Linux changes nothing and on Windows disables newline translation.
The reading and writing functions are the ones you already use for the console, with a FILE * in front. fprintf(f, ...) is printf to a file, fputs(s, f) writes a string, fgets(buf, size, f) reads a line, fscanf(f, ...) parses formatted fields, and fgetc and fputc move one character. stdin, stdout and stderr are FILE * values that are already open, so a function written to take a FILE * works on the console and on files alike, and printf(...) is exactly fprintf(stdout, ...).
The reliable way to read a whole file is a loop that tests the reading function's result: while (fgets(line, sizeof line, f) != NULL). fgets returns NULL at end of file or on error, so the loop ends when the data does. Do not write while (!feof(f)): feof only becomes true after a read has already failed, so that loop runs one extra time with stale data. Use feof and ferror after the loop, to tell a normal end from an error.
fclose(f) writes out any buffered data and releases the handle. Output is buffered, so until fclose or fflush runs, what you wrote may still be in memory, and a program that crashes before closing can lose the end of its file. Operating systems also limit how many files a process may hold open, so close every file you open. remove(path) deletes a file and rename(old, new) renames one; for positioning within a file, fseek, ftell and rewind move and report the current offset.
Syntax
#include <stdio.h>
FILE *f = fopen("data.txt", "r"); /* "r" read, "w" write (truncate), "a" append */
if (f == NULL) {
perror("data.txt"); /* prints the reason to stderr */
return 1;
}
char line[128];
while (fgets(line, sizeof line, f) != NULL) { /* one line per iteration, newline kept */
/* use line */
}
int qty; double price;
while (fscanf(f, "%d %lf", &qty, &price) == 2) { /* two fields per record */ }
fprintf(f, "%s,%d\n", name, qty); /* formatted write */
fputs("plain text\n", f);
fclose(f); /* flush and release */
remove("data.txt"); /* delete */The same functions work on stdin, stdout and stderr, which are FILE * values that are open when main starts. perror prints your label, a colon and the operating system's description of the last error.
fopen modes
| Mode | Opens for | If the file exists | If it does not exist |
|---|---|---|---|
| "r" | reading | reads from the start | fopen returns NULL |
| "w" | writing | contents are discarded | it is created |
| "a" | appending | writes go after the existing end | it is created |
| "r+" | reading and writing | kept, position at the start | fopen returns NULL |
| "w+" | reading and writing | contents are discarded | it is created |
| "a+" | reading and appending | kept, writes go at the end | it is created |
Writing a file and reading it back line by line
Three lines of visit counts are written, then read back and numbered.
#include <stdio.h>
int main(void) {
FILE *out = fopen("visits.txt", "w");
if (out == NULL) {
printf("could not create visits.txt\n");
return 1;
}
fprintf(out, "%s %d\n", "monday", 42);
fprintf(out, "%s %d\n", "tuesday", 37);
fputs("wednesday 51\n", out);
fclose(out);
FILE *in = fopen("visits.txt", "r");
if (in == NULL) {
printf("could not open visits.txt\n");
return 1;
}
char line[64];
int number = 0;
while (fgets(line, sizeof line, in) != NULL) {
number++;
printf("%d: %s", number, line);
}
fclose(in);
printf("%d lines\n", number);
remove("visits.txt");
return 0;
}Output
1: monday 42 2: tuesday 37 3: wednesday 51 3 lines
The first fopen uses "w", which creates visits.txt in the current directory or empties it if it exists. fprintf and fputs write to it exactly as they would to the screen, and fclose makes sure the data is flushed before the file is reopened for reading. The read loop is the standard shape: fgets returns NULL when the lines run out, so the body runs three times. The line printed with %s already ends in the newline that fgets kept, so the format string has none. remove deletes the file at the end so the program leaves nothing behind.
Appending records and reading them with fscanf
A helper opens the file in append mode for each rainfall reading; a second loop totals them.
#include <stdio.h>
#include <string.h>
int append_reading(const char *path, const char *site, double mm) {
FILE *f = fopen(path, "a");
if (f == NULL) {
return 0;
}
fprintf(f, "%s %.1f\n", site, mm);
fclose(f);
return 1;
}
int main(void) {
const char *path = "rain.txt";
append_reading(path, "north", 4.5);
append_reading(path, "south", 0.0);
append_reading(path, "north", 2.5);
append_reading(path, "west", 7.0);
FILE *f = fopen(path, "r");
if (f == NULL) {
printf("cannot open %s\n", path);
return 1;
}
char site[16];
double mm;
double total = 0.0;
int records = 0, from_north = 0;
while (fscanf(f, "%15s %lf", site, &mm) == 2) {
records++;
total += mm;
if (strcmp(site, "north") == 0) {
from_north++;
}
}
if (ferror(f)) {
printf("read error\n");
}
fclose(f);
printf("%d records, %d from north, %.1f mm in total\n", records, from_north, total);
remove(path);
return 0;
}Output
4 records, 2 from north, 14.0 mm in total
Each call to append_reading opens the file with "a", so the four lines accumulate instead of replacing each other, and each call closes the file again so the data is flushed. The reading loop asks fscanf for two fields at a time, a word of at most 15 characters and a double, and continues while it gets both; the width in %15s protects the 16-byte array. After the loop, ferror distinguishes a read error from an ordinary end of file. Records with a fixed number of fields per line suit fscanf well; free-form text is better read with fgets and then parsed.
stdin and stdout are files too
One function copies lines from any input stream to any output stream; it is used on stdin, on a file and on stdout.
#include <stdio.h>
int copy_lines(FILE *in, FILE *out) {
char line[128];
int n = 0;
while (fgets(line, sizeof line, in) != NULL) {
fputs(line, out);
n++;
}
return n;
}
int main(void) {
FILE *f = fopen("capture.txt", "w");
if (f == NULL) {
return 1;
}
int saved = copy_lines(stdin, f);
fclose(f);
printf("saved %d lines\n", saved);
f = fopen("capture.txt", "r");
if (f == NULL) {
return 1;
}
int shown = copy_lines(f, stdout);
fclose(f);
printf("shown %d lines\n", shown);
FILE *missing = fopen("no-such-file.txt", "r");
if (missing == NULL) {
printf("no-such-file.txt could not be opened\n");
} else {
fclose(missing);
}
remove("capture.txt");
return 0;
}Input given to the program: first shift ↵ second shift
Output
saved 2 lines first shift second shift shown 2 lines no-such-file.txt could not be opened
copy_lines knows nothing about where its streams come from. First it copies the two lines of standard input into capture.txt; then, with the file reopened for reading, it copies them to stdout, which is why they appear in the output. The count returned each time confirms that both directions saw two lines. The final fopen shows the failure case: a file that does not exist, opened with "r", yields NULL, and the program reports it instead of crashing. Calling perror there would add the operating system's reason to the message on stderr.
Common mistakes
Not checking whether fopen succeeded
Why it goes wrong: A wrong path, a missing file or a permission problem makes fopen return NULL, and the next fgets or fprintf on it crashes the program.
Fix: Test the result immediately, report the problem, and stop or skip the file.
C · fixFILE *f = fopen(path, "r"); if (f == NULL) { perror(path); return 1; }Looping on feof
Why it goes wrong:
while (!feof(f)) { fgets(line, sizeof line, f); process(line); }processes the last line twice, because feof becomes true only after fgets has already failed at the end of the file.Fix: Put the read in the loop condition:
while (fgets(line, sizeof line, f) != NULL). Check feof or ferror after the loop if you need to know why it stopped.Opening with "w" when you meant to keep the contents
Why it goes wrong: "w" truncates the file to zero length the moment fopen returns. Existing data is gone before the first write.
Fix: Use "a" to add to the end, or "r+" to update in place.
Forgetting to close the file
Why it goes wrong: Written data sits in a buffer until fclose or fflush; if the program ends abnormally, the tail of the file is lost. Each open file also uses a handle from a limited pool.
Fix: Close each file as soon as you are done with it, on every path out of the function.
Where you use this
A small monitoring program appends one line per measurement to a log file with "a", closing the file after each write so that nothing is lost if the machine is switched off; a reporting program opens the same log with "r", reads it line by line and totals the values. Configuration files follow the same pattern: read each line with fgets, cut the newline with strcspn, skip blank lines and comments, and split the rest at =. Because the reader takes a FILE *, the same code can be tested by feeding it stdin.
char line[128];
while (fgets(line, sizeof line, cfg) != NULL) {
line[strcspn(line, "\n")] = '\0';
if (line[0] == '\0' || line[0] == '#') {
continue; /* blank line or comment */
}
char *eq = strchr(line, '=');
if (eq == NULL) {
continue;
}
*eq = '\0';
apply_setting(line, eq + 1); /* key, value */
}Key points
fopenreturns aFILE *or NULL; check it every time.- Modes: "r" reads, "w" truncates and writes, "a" appends; add
+for both directions. - Read with
fgetsorfscanfin the loop condition; never loop onfeof. - Write with
fprintfandfputs; the data may stay buffered untilfclose. stdin,stdoutandstderrare ordinaryFILE *streams.removedeletes,renamerenames, andfseek/ftellmove within a file.
Try it yourself
Read n from standard input, write the lines 1 1, 2 4, up to n n*n into squares.txt, close it, reopen it for reading, add up the second number of every line with fscanf, print sum of squares: <total> and remove the file. For the input 5 the total is 55.
#include <stdio.h>
int main(void) {
int n;
if (scanf("%d", &n) != 1) {
return 1;
}
/* 1. open squares.txt for writing and write n lines: "1 1", "2 4", "3 9", ...
2. close it, reopen it for reading
3. read every line with fscanf and add up the squares
4. print "sum of squares: <total>" and remove the file */
return 0;
}5sum of squares: 55#include <stdio.h>
int main(void) {
int n;
if (scanf("%d", &n) != 1) {
return 1;
}
FILE *f = fopen("squares.txt", "w");
if (f == NULL) {
return 1;
}
for (int i = 1; i <= n; i++) {
fprintf(f, "%d %d\n", i, i * i);
}
fclose(f);
f = fopen("squares.txt", "r");
if (f == NULL) {
return 1;
}
int number, square, total = 0;
while (fscanf(f, "%d %d", &number, &square) == 2) {
total += square;
}
fclose(f);
printf("sum of squares: %d\n", total);
remove("squares.txt");
return 0;
}Practise this
Exercises for this lesson are in the C practice set.
Related lessons
- Strings in CHow C stores text as null-terminated char arrays, reads a line safely with fgets, and copies, joins, compares and searches text with the string.h functions.9 min
- Dynamic Memory in CRequesting memory at run time in C with malloc, calloc and realloc, checking for NULL, sizing allocations correctly, and freeing every block exactly once.10 min
- Structures in CHow a C struct bundles related values into one record, how to initialise, copy and pass structs to functions, and when to use -> instead of the dot.9 min
- Loops in C: for, while and do-whileThe three C loops and when each fits: how a for loop's three parts run, reading input until it ends with while and scanf, and using break and continue safely.9 min
Frequently asked questions
How do I check whether a file exists in C?
Standard C has no dedicated function; the usual approach is to try to open it for reading and treat a NULL result as missing or unreadable, closing the file again if it opened. On POSIX systems access(path, F_OK) from <unistd.h> answers the question without opening. Either way, prefer to open the file for the operation you actually want and handle failure there, because a file can appear or disappear between the check and the use.
What is the difference between text mode and binary mode?
In text mode the C library may translate line endings between the system's convention and \n; on Linux and macOS that is a no-op, on Windows \n becomes \r\n on output. Binary mode, requested by adding b to the mode string, writes and reads bytes exactly as given, which is what images, compressed data and anything else that is not lines of text need, usually through fread and fwrite.
Why is my output file empty until the program ends?
Because output is buffered: fprintf places bytes in a memory buffer that reaches the operating system only when it fills, when you call fflush(f) or fclose(f), or when the program exits normally. If you need the data on disk at a specific moment, call fflush, and always close files before the program ends.
How this page was checked. Every program on it was run with GCC 13 (C11) at build time by the publishing checks, and the output shown is what it printed. Running C inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C11) locally.