Module 13 · Files
Files: Making Data Outlive the Program
In this lesson
- Explain why a file outlives the program, open one with
fopenand a mode, and close it withfclose. - Check what
fopenreturns forNULL, and print the reason withperror. - Use
stdin,stdoutandstderras three files already open, and say what the Playground allows with files.
Maria's till program adds up the day's sales in a variable. At closing time she stops the program. The next morning the total starts again at 0, and yesterday's sales are gone.
Nothing crashed. The program did exactly what it was told, and nobody told it to remember.
This lesson gives Maria's sales a place that outlives the program: a file. It also shows that your programs have used files all along.
A variable lives only while the program runs
A variable is a box in memory (Module 2). A program gets its memory when it starts and gives it all back when it ends, every box with it.
A file is a named sequence of bytes that the operating system keeps on a disk. The operating system is the software that runs the computer and its disks, such as Linux, Windows or macOS. A file stays after the program that wrote it has ended.
| Where the day's sales live | When the program ends | The next morning |
|---|---|---|
A variable, int total | Gone with the program's memory | Starts again at 0 |
A file, sales.txt | Stays on the disk | The program reads it back |
So a variable lasts for one run of the program, and a file lasts until someone deletes it.
Opening a file: fopen, a mode and a FILE *
C reads and writes a file through a stream: a flow of characters taken or added in order. fopen from <stdio.h> connects a stream to a file and gives you a handle for it.
The handle's type is FILE *. A FILE is the library's record of one open stream: where it has got to, and what it still holds. fopen sets it up and hands you its address, a pointer (Module 11).
Opening a file
FILE *f = fopen("sales.txt", "w");
FILE *fis the handle. Every later call on this file takesf."sales.txt"is the file's name. With no folder in it, the file lives in the folder the program runs in."w"is the mode, what you will do with the file. It is a string, so it takes double quotes.- If the file cannot be opened,
fopenreturnsNULL.
Three modes cover this module's text files. A text file holds characters in lines, the kind any text editor opens.
| Mode | Opens the file for | If the file exists | If it does not |
|---|---|---|---|
"r" | reading | reads from the start | fopen returns NULL |
"w" | writing | empties it first | creates it |
"a" | appending, which means adding at the end | keeps it and writes after its end | creates it |
A b added to a mode, as in "rb" or "wb", opens a binary file, its bytes stored exactly as they are. The b matters on Windows, where text mode writes each \n as two characters. On Linux, where the Playground runs, it changes nothing. Lesson 4 writes binary files.
Maria writes two sales into sales.txt, closes it, opens it again for reading, and prints what is there. fprintf is printf with a stream in front.
#include <stdio.h>
#define LINE_LEN 256
int main(void)
{
FILE *f = fopen("sales.txt", "w");
char line[LINE_LEN] = "";
if (f == NULL) {
perror("sales.txt");
return 1;
}
fprintf(f, "rice 40\n");
fprintf(f, "oil 12\n");
fclose(f);
f = fopen("sales.txt", "r");
if (f == NULL) {
perror("sales.txt");
return 1;
}
while (fgets(line, sizeof line, f) != NULL) {
printf("from the file: %s", line);
}
fclose(f);
return 0;
}
from the file: rice 40
from the file: oil 12
The reading loop is Module 10's: fgets reads one line, newline and all, and returns NULL when the lines run out. Here it reads from f, not the keyboard. Lesson 2 takes the loop apart.
This program runs on the Playground as it is. A run there may create, write, close and read back a file, and the Playground section below lists the rest.
So fopen turns a name and a mode into a FILE *, and fprintf and fgets take that handle.
Mode w empties the file first
The next morning, Maria's program opens sales.txt with "w" again and writes the first sale of the day. She expects three lines. This program does both days in one run.
#include <stdio.h>
#define LINE_LEN 256
void print_lines(FILE *in);
int main(void)
{
FILE *f = fopen("sales.txt", "w");
if (f == NULL) {
perror("sales.txt");
return 1;
}
fprintf(f, "rice 40\n");
fprintf(f, "oil 12\n");
fclose(f);
f = fopen("sales.txt", "w");
if (f == NULL) {
perror("sales.txt");
return 1;
}
fprintf(f, "salt 30\n");
fclose(f);
f = fopen("sales.txt", "r");
if (f == NULL) {
perror("sales.txt");
return 1;
}
printf("sales.txt now holds:\n");
print_lines(f);
fclose(f);
return 0;
}
void print_lines(FILE *in)
{
char line[LINE_LEN] = "";
while (fgets(line, sizeof line, in) != NULL) {
printf(" %s", line);
}
}
sales.txt now holds:
salt 30
One line. "w" empties a file that exists before a single byte is written, so yesterday's lines were gone the moment fopen returned. No message at any command line: the Playground, -Wall and -Wall -Wextra all stay silent.
To keep the old lines and add after them, open with "a"; lesson 2 does that for Maria's daily log. The printing sits in print_lines, which takes any open stream as a FILE * parameter.
So "w" starts every file empty, and "a" adds to what is there.
When fopen fails: NULL and perror
fopen fails when the file does not exist, or when the program may not write where you asked. Then it returns NULL, the pointer to nothing (Module 11). Reading or writing through NULL is undefined behaviour, so this track checks every fopen at once.
Zara tests the missing file first. Her program opens data.txt for reading, and no run has written it.
#include <stdio.h>
int main(void)
{
FILE *in = fopen("data.txt", "r");
if (in == NULL) {
perror("data.txt");
return 1;
}
printf("data.txt is still here\n");
fclose(in);
return 0;
}
perror prints your string, a colon, a space, and why the last library call failed. The library keeps that reason as a number in errno, from <errno.h>. Pass the file's name, so the message says which file.
Measured on the live Playground, this run shows the badge Runtime error, the line data.txt: No such file or directory, and Exit: 1. Nothing crashed: return 1; reports the failure on purpose.
The number main returns is the program's exit status: 0 means success, anything else means failure. The Playground shows every exit status other than 0 as Runtime error, with the number after Exit:.
So fopen returns NULL when it fails, and perror says why. It writes on a second output stream, stderr, which the last section explains.
fclose gives the file back
fclose(f) closes the stream. First it writes out whatever the library still holds for the file. Then it hands the file back to the operating system.
fprintf does not go to the disk for every line. It collects characters in a buffer, a block of memory, and writes the block at once. One big write costs far less than many small ones. Writing a buffer out is called flushing, and fclose flushes.
The operating system also lets a program hold only so many open files, and each fopen without its fclose keeps one. Lessons 5 and 6 measure what a crash does to a buffer that was never flushed.
So close every file once you are done with it, and always before reading back what you wrote.
Files on the Playground: each run starts in an empty folder
The Playground runs your program on Linux, in a folder of its own. Measured on the live Playground, signed out:
- A run may create, write, close, reopen and read files. A program that writes
rice 40intodata.txtand reads it back shows Success andread back: rice 40. - A file lives until the run ends, and every run starts in an empty folder. Zara's program ran right after that one and still got
No such file or directory. - A folder the program may not write gives
/probe.txt: Permission deniedforfopen("/probe.txt", "w"). - A run may write at most 1,048,576 bytes to a file, which is 1 MiB. One byte more ends it with Output limit exceeded (1 MB).
So a program in this module that reads a file writes it first, in the same run.
On your own machine the file stays. Save a program as sales.c, then compile and run it in a terminal:
gcc -std=c17 -Wall -Wextra sales.c -o sales
./sales
A second run finds the file the first one left. Example 2 below prints less on its second run for that reason.
So the Playground allows files, but every run starts with none: write before you read, in one run, under 1 MiB.
stdin, stdout and stderr are files already open
Every C program starts with three streams already open, before main runs. These standard streams are three FILE * values named in <stdio.h>.
stdin, standard input: the keyboard, or a judge's test input.stdout, standard output: the results.printfwrites here.stderr, standard error: a second output for warnings and errors.perrorwrites here.
So printf is fprintf(stdout, ...) with a shorter name, and scanf reads stdin. fgets(line, sizeof line, stdin) is Module 10's line reader: the same fgets, handed a different stream.
#include <stdio.h>
#define LINE_LEN 256
int main(void)
{
char line[LINE_LEN] = "";
if (fgets(line, sizeof line, stdin) == NULL) {
fprintf(stderr, "warning: no input\n");
return 1;
}
printf("printf: %s", line);
fprintf(stderr, "fprintf to stderr: %s", line);
fprintf(stdout, "fprintf to stdout: %s", line);
return 0;
}
printf: the shop opens at 9
fprintf to stdout: the shop opens at 9
That output is for the input the shop opens at 9. The middle line went to stderr, so the block above leaves it out. The Playground shows it below both stdout lines: the page prints all of stdout first, then all of stderr.
A judge reads only stdout and ignores stderr. A warning on stderr never fails a test, but one extra line on stdout does. So this module's graded problems read stdin and write stdout, through the same functions a file uses.
In a terminal, the shell (the program that reads your commands) can redirect a stream to a file. ./sales < in.txt feeds in.txt to stdin, and ./sales > out.txt sends stdout into out.txt. The Playground's runner does the same, sending stdout and stderr into two files.
So the standard streams are ordinary streams. They just open before main, and close when the program ends.
Alice prints the shop's hours from one function that takes the stream as a parameter. stdout and stderr are FILE * values, so either can be passed in.
#include <stdio.h>
void print_hours(FILE *out);
int main(void)
{
print_hours(stdout);
print_hours(stderr);
return 0;
}
void print_hours(FILE *out)
{
fprintf(out, "Maria's shop: open from 9 to 21\n");
}
Maria's shop: open from 9 to 21
The same line also went to stderr, so the Playground shows it twice. Hand print_hours a stream from fopen and it writes the file instead, without knowing the difference.
David's program reads its settings from config.txt. On the first run there is no such file, and that should not stop it. So it says why, writes default settings, and opens those.
#include <stdio.h>
#define LINE_LEN 256
int write_defaults(void);
void print_settings(FILE *in);
int main(void)
{
FILE *f = fopen("config.txt", "r");
if (f == NULL) {
perror("config.txt");
printf("no config.txt yet: writing the defaults\n");
if (!write_defaults()) {
return 1;
}
f = fopen("config.txt", "r");
if (f == NULL) {
perror("config.txt");
return 1;
}
}
print_settings(f);
fclose(f);
return 0;
}
int write_defaults(void)
{
FILE *out = fopen("config.txt", "w");
if (out == NULL) {
perror("config.txt");
return 0;
}
fprintf(out, "page_size 10\n");
fprintf(out, "currency taka\n");
fclose(out);
return 1;
}
void print_settings(FILE *in)
{
char line[LINE_LEN] = "";
while (fgets(line, sizeof line, in) != NULL) {
printf("setting: %s", line);
}
}
no config.txt yet: writing the defaults
setting: page_size 10
setting: currency taka
stderr got config.txt: No such file or directory, yet the program returns 0: here a missing file means a first day, not a failure. On the Playground every run prints all three lines. On your own machine the second run finds config.txt and prints only the settings.
Amara types each student's name and mark. A mark outside 0 to 100 is a typing slip. Good marks go into marks.txt, each slip gets a warning on stderr, and the file is printed back at the end.
#include <stdio.h>
#define NAME_LEN 20
#define LINE_LEN 256
void print_lines(FILE *in);
int main(void)
{
FILE *f = fopen("marks.txt", "w");
char name[NAME_LEN] = "";
int mark = 0;
int saved = 0;
if (f == NULL) {
perror("marks.txt");
return 1;
}
while (scanf("%19s %d", name, &mark) == 2) {
if (mark < 0 || mark > 100) {
fprintf(stderr, "warning: %s %d is not a mark, skipped\n", name, mark);
} else {
fprintf(f, "%s %d\n", name, mark);
saved = saved + 1;
}
}
fclose(f);
f = fopen("marks.txt", "r");
if (f == NULL) {
perror("marks.txt");
return 1;
}
printf("marks.txt holds %d marks:\n", saved);
print_lines(f);
fclose(f);
return 0;
}
void print_lines(FILE *in)
{
char line[LINE_LEN] = "";
while (fgets(line, sizeof line, in) != NULL) {
printf("%s", line);
}
}
marks.txt holds 3 marks:
Amara 91
Zara 78
Kenji 85
That output is for the input Amara 91, Bob 140, Zara 78 and Kenji 85, one per line. stderr says warning: Bob 140 is not a mark, skipped, so marks.txt holds only marks Amara can trust.
Where this is used
- Logs. The nginx web server adds one line to its access log for every request it answers. The log is a text file, so it survives a restart.
- Settings. git keeps a repository's settings in
.git/config, a plain text file.git config user.name Mariawrites a line there for later git commands to read. - Whole databases. SQLite keeps an entire database, every table in it, in one ordinary file. Android and iOS both include SQLite for their apps' data.
- Command-line tools. grep prints the lines it finds on stdout, and complaints such as
grep: missing.txt: No such file or directoryon stderr.
Common mistakes
1. The star left out of FILE *.
FILE f = fopen("sales.txt", "w");
fprintf(f, "rice 40\n");
fclose(f);
An error on every command line, the Playground included: error: invalid initializer. Each later use of f adds another, such as error: incompatible type for argument 1 of 'fprintf' with note: expected 'FILE * restrict' but argument is of type 'FILE'. fopen returns a pointer, which cannot start a whole FILE. Write FILE *f. You will drop the star because int never needed one.
2. No check for NULL.
FILE *f = fopen("data.txt", "r");
char line[LINE_LEN] = "";
while (fgets(line, sizeof line, f) != NULL) {
printf("%s", line);
}
No message at any command line: GCC cannot know whether data.txt will exist. On a fresh run it does not, so f is NULL, and fgets on NULL is undefined behaviour. One run on Compiler Explorer's GCC 12 died with Program terminated with signal SIGSEGV (11). Check for NULL and call perror. You will skip it because the file was there when you tested.
3. Bob reads the file back before closing it.
fprintf(out, "rice 40\n");
in = fopen("sales.txt", "r");
if (in == NULL) {
perror("sales.txt");
return 1;
}
if (fgets(line, sizeof line, in) == NULL) {
printf("sales.txt looks empty\n");
}
No message at any command line. Bob's full program checks both fopen calls, and one run on Compiler Explorer's GCC 12 prints sales.txt looks empty. The line was still in out's buffer. The Playground agrees: a second stream saw the file empty until the first was flushed. Call fclose(out); before reopening. You will forget because fprintf returned without complaint.
4. The mode in single quotes.
FILE *f = fopen("sales.txt", 'w');
A warning on every command line, the Playground included, because it is on by default: warning: passing argument 2 of 'fopen' makes pointer from integer without a cast [-Wint-conversion], with note: expected 'const char * restrict' but argument is of type 'int'. GCC 14 makes it an error. 'w' is one character, a small integer, and the mode must be a string. One run on Compiler Explorer's GCC 12 died with SIGSEGV. Write "w". You will slip because a one-letter mode looks like a char.
Maria's till prints each sale for the receipt, and a summary for her own log. Write void print_sale(FILE *out, const char *item, int price), which prints one sale on the stream it is given; main passes stdout. The judge sees stdout only.
Input. A line with n, then n lines item price.
Output. On stdout, one line per sale, <item> <price>, in the order read. On stderr, not judged: sales: <n>, total: <sum>.
Constraints. 0 <= n <= 1000; an item is 1 to 19 lower-case letters; 1 <= price <= 100000.
Sample. Input 3, rice 65, oil 180, salt 40, one per line, gives rice 65, oil 180 and salt 40 on three lines of stdout. stderr shows sales: 3, total: 285.
#include <stdio.h>
#define NAME_LEN 20
void print_sale(FILE *out, const char *item, int price);
int main(void)
{
int n = 0;
char item[NAME_LEN] = "";
int price = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%19s %d", item, &price);
print_sale(stdout, item, price);
}
/* The report goes to stderr, where the judge never looks:
sales: the count, total: the sum. Keep a running total in the loop. */
return 0;
}
void print_sale(FILE *out, const char *item, int price)
{
/* One line on the stream it is given: the item, a space, the price. */
}
Graded as till-streams. The hidden tests include n = 0, where stdout stays empty, and a summary printed to stdout fails every one of them.
Zara tests the missing file first. The program writes notes.txt, then reads a file name. Write int can_open(const char *name). It returns 1 if the file opens for reading, and closes it again. Otherwise perror prints why on stderr, and it returns 0.
Input. One file name.
Output. opened <name> if the file opened, otherwise cannot open <name>. The reason goes to stderr.
Constraints. The name is one word of 1 to 29 characters, with no spaces.
Sample. Input notes.txt gives opened notes.txt. Input sales.txt gives cannot open sales.txt, and stderr shows sales.txt: No such file or directory.
#include <stdio.h>
#define FILE_NAME_LEN 30
int can_open(const char *name);
int main(void)
{
FILE *f = fopen("notes.txt", "w");
char name[FILE_NAME_LEN] = "";
if (f == NULL) {
perror("notes.txt");
return 1;
}
fprintf(f, "rice 40\n");
fclose(f);
scanf("%29s", name);
if (can_open(name)) {
printf("opened %s\n", name);
} else {
printf("cannot open %s\n", name);
}
return 0;
}
int can_open(const char *name)
{
/* Try to open name for reading. When fopen gives NULL, who says why?
Anything you opened, close before you return. */
return 0;
}
Not graded on its own. A judge's run starts in an empty folder, so every name but the one the program wrote gives the same answer.
Run in CompilerCommon doubts
Where does sales.txt end up on my own machine?
In the program's working folder, the folder you ran it from. Run
./salesin your project folder and the file appears besidesales.c.Why is it FILE * and not just FILE?
fopensets up the library's record and hands you its address; the record itself stays with the library. Mistake 1 shows GCC's answer to a missing star.Is perror just printf with my own message?
No.
perrorwrites to stderr, away from your results, and adds the reason fromerrno, whichprintfcannot know.strerror(errno), from<string.h>, gives the same words as a string.Do I have to close stdin, stdout and stderr?
No. A normal end of the program flushes and closes every stream, those three included. Close the files you opened yourself, as soon as you are done with them.
Key takeaways
- A variable lives only while the program runs; a file on the disk outlives it.
fopen(name, mode)returns aFILE *:"r"reads a file that must exist,"w"creates or empties one,"a"adds at the end.- Check every
fopenforNULL.perror(name)prints why on stderr, andreturn 1;shows Runtime error withExit: 1on the Playground. fcloseflushes the buffer and gives the file back: close a file before you read back what you wrote.stdin,stdoutandstderrareFILE *values already open, and a judge reads only stdout.- Every Playground run starts in an empty folder: write a file before reading it, and keep it to 1 MiB.
Next, Maria keeps a daily log that grows by a line every run. Lesson 2 appends with "a", reads a file line by line to its end, and reads it twice with rewind.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Reading and Writing Text Files