Working with files
Everything a program keeps in variables disappears when it ends. To keep data (scores, settings, a log, a saved game) a program writes it to a file, and reads it back next time. C's file functions are in <stdio.h>, the same header as printf, and they work much like printf and scanf, but with a file at the other end.
Opening a file: FILE and fopen
FILE *f = fopen("scores.txt", "w");fopen opens a file and returns a FILE *, a pointer to the information C keeps about the open file (where it is, how far you've read, its buffer). Every other file function takes that pointer. The second argument is the mode:
| Mode | Opens for | If the file exists | If it doesn't |
|---|---|---|---|
"r" | reading | reads from the start | fopen fails, returns NULL |
"w" | writing | empties it first | creates it |
"a" | appending | writes at the end | creates it |
"r+" | reading and writing | keeps it | fails |
"w+" | reading and writing | empties it | creates it |
"a+" | reading and appending | keeps it | creates it |
Add b (as in "rb" or "wb") for binary files. On Linux and macOS it changes nothing; on Windows, text mode translates line endings, so binary data needs b.
Writing a file
#include <stdio.h>
int main() {
FILE *f = fopen("scores.txt", "w");
if (f == NULL) {
printf("Couldn't open scores.txt for writing\n");
return 1;
}
fprintf(f, "Asha %d\n", 92); // like printf, into the file
fprintf(f, "Ravi %d\n", 78);
fputs("Meera 85\n", f); // a plain string, no formatting
fclose(f); // finish writing and release the file
printf("Saved 3 scores\n");
return 0;
}Saved 3 scoresfprintf(f, ...)isprintfwith a file as its first argument.fputs(text, f)writes a string as it is. Unlikeputs, it adds no new line.fputc(ch, f)writes one character.fclose(f)finishes the job. Writes go through a buffer in memory, andfclosemakes sure everything reaches the disk. Forgetting it can lose the end of your data, and every open file uses up one of a limited number of slots.
Reading line by line: fgets
fgets(line, size, f) reads one line (up to size - 1 characters, including the new line) and returns NULL at the end of the file, which makes the standard reading loop:
#include <stdio.h>
int main() {
FILE *out = fopen("notes.txt", "w");
if (out == NULL) return 1;
fputs("Buy milk\nCall Ravi\nFinish the C lesson\n", out);
fclose(out);
FILE *f = fopen("notes.txt", "r");
if (f == NULL) {
printf("Couldn't open notes.txt\n");
return 1;
}
char line[100];
int n = 0;
while (fgets(line, sizeof line, f) != NULL) {
n++;
printf("%d: %s", n, line); // line already ends with \n
}
fclose(f);
return 0;
}1: Buy milk
2: Call Ravi
3: Finish the C lessonReading numbers: fscanf
fscanf is scanf reading from a file, and it returns how many values it read, so it can drive a loop too:
#include <stdio.h>
int main() {
FILE *out = fopen("marks.txt", "w");
if (out == NULL) return 1;
fprintf(out, "Asha 92\nRavi 78\nMeera 85\nKabir 64\n");
fclose(out);
FILE *f = fopen("marks.txt", "r");
if (f == NULL) return 1;
char name[30];
int mark, count = 0, total = 0, best = -1;
char top[30] = "";
while (fscanf(f, "%29s %d", name, &mark) == 2) { // stop when a line doesn't give 2 values
count++;
total += mark;
if (mark > best) {
best = mark;
snprintf(top, sizeof top, "%s", name);
}
}
fclose(f);
printf("%d students, average %.1f, top: %s with %d\n", count, (double) total / count, top, best);
return 0;
}4 students, average 79.8, top: Asha with 92Looping while (fscanf(...) == 2) stops cleanly at the end of the file, and also at a badly formatted line, instead of looping forever on input it can't read.
Character by character: fgetc and EOF
fgetc(f) returns the next character, or the special value EOF (end of file) when there are none left. It returns an int, not a char, precisely so that EOF (usually -1) can't be confused with a real character:
#include <stdio.h>
int main() {
FILE *out = fopen("poem.txt", "w");
if (out == NULL) return 1;
fputs("Twinkle twinkle little star\nHow I wonder what you are\n", out);
fclose(out);
FILE *f = fopen("poem.txt", "r");
if (f == NULL) return 1;
int ch; // int, so it can hold EOF
int chars = 0, words = 0, lines = 0, inWord = 0;
while ((ch = fgetc(f)) != EOF) {
chars++;
if (ch == '\n') lines++;
if (ch == ' ' || ch == '\n' || ch == '\t') {
inWord = 0;
} else if (!inWord) {
inWord = 1; // the first letter of a new word
words++;
}
}
fclose(f);
printf("%d lines, %d words, %d characters\n", lines, words, chars);
return 0;
}2 lines, 10 words, 54 charactersThat's the core of the Unix wc (word count) program. Note the pattern (ch = fgetc(f)) != EOF: read, store and test in one condition.
Appending
Mode "a" adds to the end and never erases what's there, which is what a log needs:
#include <stdio.h>
void logLine(const char *message) {
FILE *f = fopen("app.log", "a");
if (f == NULL) return;
fprintf(f, "%s\n", message);
fclose(f);
}
int main() {
remove("app.log"); // start this demo with no log
logLine("started");
logLine("user signed in");
logLine("lesson 3 completed");
FILE *f = fopen("app.log", "r");
if (f == NULL) return 1;
char line[100];
while (fgets(line, sizeof line, f) != NULL) printf("log: %s", line);
fclose(f);
return 0;
}log: started
log: user signed in
log: lesson 3 completedremove(name) deletes a file, and rename(old, new) renames one; both return 0 when they succeed.
Copying a file
Read from one, write to the other, until the end:
#include <stdio.h>
int main() {
FILE *out = fopen("original.txt", "w");
if (out == NULL) return 1;
fputs("Line one\nLine two\n", out);
fclose(out);
FILE *src = fopen("original.txt", "r");
FILE *dst = fopen("copy.txt", "w");
if (src == NULL || dst == NULL) {
printf("Couldn't open the files\n");
return 1;
}
int ch, copied = 0;
while ((ch = fgetc(src)) != EOF) {
fputc(ch, dst);
copied++;
}
fclose(src);
fclose(dst);
printf("Copied %d characters\n", copied);
return 0;
}Copied 18 charactersBinary files: fwrite and fread
Text files store numbers as digits you can read. Binary files store the bytes exactly as they are in memory: smaller, faster, and no conversion, but not readable in a text editor. fwrite and fread move whole blocks at once:
#include <stdio.h>
int main() {
int scores[5] = {92, 78, 85, 64, 99};
FILE *f = fopen("scores.bin", "wb");
if (f == NULL) return 1;
size_t written = fwrite(scores, sizeof scores[0], 5, f); // 5 ints from the array
fclose(f);
int back[5] = {0};
f = fopen("scores.bin", "rb");
if (f == NULL) return 1;
size_t got = fread(back, sizeof back[0], 5, f);
fclose(f);
printf("wrote %d, read %d:", (int) written, (int) got);
for (int i = 0; i < 5; i++) printf(" %d", back[i]);
printf("\n");
return 0;
}wrote 5, read 5: 92 78 85 64 99Both take the address of the data, the size of one item, how many items, and the file, and return how many items they actually moved, which is how you detect a short or damaged file. Structures can be saved the same way, which is how simple programs save a whole record in one call.
Reporting errors: perror
When fopen fails, the C library records why in errno (from <errno.h>). strerror(errno) turns that into words, and perror prints your message followed by them:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main() {
FILE *f = fopen("no-such-file.txt", "r");
if (f == NULL) {
printf("Couldn't open no-such-file.txt: %s\n", strerror(errno));
perror("no-such-file.txt"); // the same reason, on the error channel
return 1;
}
fclose(f);
return 0;
}Couldn't open no-such-file.txt: No such file or directoryperror writes to standard error (stderr), the error channel, which stays separate from normal output (so its line, no-such-file.txt: No such file or directory, isn't in the output above); fprintf(stderr, ...) writes there too. That's why programs can send results to a file while errors still reach the screen.
The file functions at a glance
| Task | Function |
|---|---|
| Open / close | fopen(name, mode), fclose(f) |
| Write formatted / a string / a character | fprintf, fputs, fputc |
| Read formatted / a line / a character | fscanf, fgets, fgetc |
| Binary blocks | fwrite, fread |
| End of file | the reading function's result: NULL, EOF, or fewer items than asked |
| Delete / rename | remove, rename |
| Why it failed | perror, errno |