Why this matters
Redirection lets a program read one input and write one output, but real programs juggle files: a configuration file, an input data set, an output report, a log. They also need to cope when a file is missing. This lesson covers the standard C way to work with files by name, the stdio functions you will use constantly, and the one loop pattern that reads a file correctly.
Opening a file: fopen
FILE *fopen(const char *path, const char *mode);
fopen asks the operating system to open the file at path and, if that works, allocates a FILE object (with its buffer) and returns a pointer to it. If it fails, it returns NULL. You must check, every time: a missing file is not exotic, it is the most common error a program meets.
FILE *in = fopen("poem.txt", "r");
in ---> +--------------------------+
| FILE |
| fd: 3 (kernel handle) |
| buffer: [ 8192 bytes ] |
| position, error flag, |
| end-of-file flag ... |
+--------------------------+
The FILE lives on the heap inside the C library. You never look inside it; you pass the pointer to stdio functions. Descriptors 0, 1 and 2 are taken by the standard streams, so the first file a program opens usually gets descriptor 3.
The mode string says what you intend:
| mode | read | write | if file exists | if missing |
|---|---|---|---|---|
"r" | yes | no | read from the start | fail (NULL) |
"w" | no | yes | truncate to empty | create |
"a" | no | yes | write at the end | create |
"r+" | yes | yes | start at the beginning, keep contents | fail |
"w+" | yes | yes | truncate | create |
"a+" | yes | yes | reads anywhere, writes at the end | create |
Adding b ("rb", "wb") means binary. On Linux it makes no difference; on Windows text mode translates line endings, so binary files must use b. The next lessons cover binary files.
Relative paths like "poem.txt" are relative to the process's current working directory, the directory you ran the program from, not the directory where the executable lives.
Closing: fclose
int fclose(FILE *stream);
fclose flushes any buffered output, releases the descriptor and frees the FILE. Forgetting it has real consequences: buffered output that was never flushed may be lost if the program ends abnormally, and a process can only have a limited number of files open at once (often 1024). After fclose, the pointer is dangling; do not use it again.
Reading lines: fgets
char *fgets(char *buf, int size, FILE *stream);
fgets reads characters into buf until it has stored a newline, or reached end of file, or filled size - 1 characters, whichever comes first. It always adds the terminating '\0'. It returns buf on success and NULL when nothing could be read (end of file or an error).
The newline is kept in the buffer. This program numbers the lines of a file:
#include <stdio.h>
int main(void)
{
FILE *in = fopen("poem.txt", "r");
if (in == NULL) {
fprintf(stderr, "cannot open poem.txt\n");
return 1;
}
char line[256];
int n = 0;
while (fgets(line, sizeof line, in) != NULL) {
n++;
printf("%3d %s", n, line);
}
fclose(in);
return 0;
}
$ ./numberlines
1 Roses are red,
2 violets are blue,
3 C has no strings,
4 only char arrays too.
The format is "%3d %s" with no \n because line already ends with one. sizeof line is 256, the size of the array, which keeps fgets inside it. If a line were longer than 255 characters, fgets would return the first 255, and the rest on the next call; the lesson on robust parsing shows how to detect that.
The other readers: fgetc and fscanf
fgetc(stream) reads one byte and returns it as an int, or EOF. It is getchar for any stream, and the loop is identical:
int c;
while ((c = fgetc(in)) != EOF) {
/* use c */
}
fscanf(stream, format, ...) is scanf for any stream. It returns the number of items it successfully converted, which you compare against the number you asked for:
int a, b;
long total = 0;
while (fscanf(in, "%d %d", &a, &b) == 2)
total += b;
This stops at end of file (fscanf returns EOF) and also at the first malformed record (it returns 0 or 1), which is usually what you want. The format %s reads one whitespace-separated word; always give it a width one less than the buffer size, as in %31s for char name[32], or a long word will overflow the array.
Writing: fprintf, fputs, fputc
These are the stream versions of printf, puts and putchar:
#include <stdio.h>
int main(void)
{
FILE *out = fopen("squares.txt", "w");
if (out == NULL) {
fprintf(stderr, "cannot create squares.txt\n");
return 1;
}
for (int i = 1; i <= 5; i++)
fprintf(out, "%d %d\n", i, i * i);
fclose(out);
FILE *log = fopen("run.log", "a");
if (log == NULL)
return 1;
fputs("wrote squares.txt\n", log);
fclose(log);
return 0;
}
Run it twice:
$ ./writelog
$ ./writelog
$ cat squares.txt
1 1
2 4
3 9
4 16
5 25
$ cat run.log
wrote squares.txt
wrote squares.txt
squares.txt has five lines, not ten: mode "w" emptied it on the second run. run.log grew, because "a" appends. Note that fputs, unlike puts, does not add a newline.
The correct reading loop, and the wrong one
Every stdio read function tells you whether it worked through its return value. The pattern is always "try to read; if it worked, use the data; otherwise stop":
while (fgets(line, sizeof line, in) != NULL) /* good */
while ((c = fgetc(in)) != EOF) /* good */
while (fscanf(in, "%d", &x) == 1) /* good */
There is also a function feof(stream), and beginners are drawn to writing:
while (!feof(in)) { /* WRONG */
fgets(line, sizeof line, in);
printf("> %s", line);
}
$ ./feofbug
> Roses are red,
> violets are blue,
> C has no strings,
> only char arrays too.
> only char arrays too.
The last line is printed twice. feof only reports that a previous read already hit the end; it cannot know that the next one will. After the fourth line, no read has failed yet, so the loop runs a fifth time, fgets fails and leaves line untouched, and the stale contents are printed. feof and its partner ferror are for asking why a loop stopped, after it stopped:
if (ferror(in))
fprintf(stderr, "read error\n");
Common mistakes
- Not checking
fopen. Using aNULLFILE *crashes: The crash happens infgetsorfprintf, not where the real mistake was.Segmentation fault (core dumped) - Opening with
"w"when you meant"r"or"a". The file is emptied before you write a single byte. There is no undo. while (!feof(f)), as shown above.- Forgetting that
fgetskeeps the newline, then comparinglinewith"yes"and wondering why it never matches (it contains"yes\n"). %swithout a width infscanf: a long word overruns the buffer.- Relative paths resolved from the wrong place. Running
./build/progfrom the project root makes"data.txt"mean./data.txt, not./build/data.txt.
Recap
fopen(path, mode)returns aFILE *orNULL; always check.fcloseflushes and releases.- Modes:
"r"read,"w"create or truncate,"a"append,+adds the other direction,bfor binary. fgetsreads a line (keeping the newline, bounded by the size you give),fgetca byte,fscanfformatted values;fprintf,fputsandfputcwrite.- Loop on the read function's return value.
feofandferrorexplain why a loop ended; they do not control it.
Going further: man 3 fopen lists every mode. Try opening the same file twice, once for reading and once for appending, and see what the reader sees after a write that has not been flushed yet.