C Part I · Module 12 · Standard Library, Files and Program Interfaces
File Streams: Opening, Reading, Closing
Every read in the previous chapter came from stdin, which was simply there — open before main started, closed after it ended, requiring nothing of you. A program that works with files has to ask for each one, and asking can fail. It has to say whether it intends to read or write, because the answer changes what the call does to a file that already exists, in one case destroying it before you have written anything. And it has to give the stream back when finished, with every obligation Chapter 9.8 described for a resource that has an owner. The last of those is the one programs most often treat as a formality, and this chapter measures why that is a mistake: a file whose every byte was still sitting in memory at the moment it was closed.
Intermediate19 min readCPart IFundamentalsStandard Library
Part I · Module 12 · Chapter 12.4 · File Streams: Opening, Reading, Closing
1. What a Stream Is
#include <stdio.h>
FILE *f = fopen("data.txt", "r");A stream is the library's view of a source or destination of bytes, and a FILE * is a handle to one. Three are open before main begins: stdin, stdout and stderr. Everything else you ask for.
Three facts about FILE set the shape of the whole chapter.
It is an opaque type. You hold a FILE * and never look inside. This is exactly Chapter 10.9 §5's handle pattern — the standard library's own example of it, and the reason that pattern was worth learning.
It owns resources. An operating-system file, a buffer, a position. Chapter 9.8's vocabulary applies without modification: fopen transfers ownership to you, and fclose is the matching release.
It is buffered, which is §4's subject and the thing that makes closing more than bookkeeping.
2. Opening, and What the Mode Does
FILE *fopen(const char *path, const char *mode);| Question | Answer |
|---|---|
| assumes | path and mode are valid strings; mode is one of the standard forms |
| returns | a FILE * you now own |
| fails by | returning NULL — convention A |
| does not promise | anything about why it failed — that is errno, Chapter 12.8 |
The mode is not a hint. Measured against an existing 9-byte file:
r existing file: 9 bytes before, 9 after opening
w existing file: 9 bytes before, 0 after opening
a existing file: 9 bytes before, 9 after opening
r+ existing file: 9 bytes before, 9 after opening
w+ existing file: 9 bytes before, 0 after opening
a+ existing file: 9 bytes before, 9 after opening
r on a missing file -> NULL
w on a missing file -> created| Mode | Reads | Writes | If the file exists | If it does not |
|---|---|---|---|---|
"r" | yes | no | opened at the start | fails |
"w" | no | yes | truncated to zero | created |
"a" | no | yes | opened, writes go to the end | created |
"r+" | yes | yes | opened at the start, not truncated | fails |
"w+" | yes | yes | truncated to zero | created |
"a+" | yes | yes | reads anywhere, writes always at the end | created |
"w"truncates at open time, before you write anything. The measurement shows nine bytes becoming zero at the momentfopenreturned. A program that opens a file for writing and then decides it has nothing to write has already destroyed it.
That is the single most expensive mistake in this table, and the defence is to pick the mode from what you intend: "a" to add, "r+" to modify in place, "w" only when replacing the contents is the point.
Every fopen must be checked. It is Chapter 9.6 §3's rule with a different function:
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "cannot open %s\n", path);
return -1;
}3. Reading and Writing
The text functions, each read with Chapter 12.1's conventions:
| Function | Does | Fails by |
|---|---|---|
fgets(buf, n, f) | reads a line | NULL — end of file or error |
fputs(s, f) | writes a string | EOF |
fprintf(f, fmt, ...) | formatted write | negative |
fscanf(f, fmt, ...) | formatted read | a count, or EOF |
fgetc(f) / fputc(c, f) | one character | EOF |
fread / fwrite | blocks of bytes | a short count — 12.5 |
Everything Chapter 12.3 established about stdin applies here with the stream named: fgets keeps the newline, truncation is detected by its absence, and fscanf has the same three-valued return and the same unconsumed-input behaviour.
And the question NULL from fgets leaves open:
if (fgets(line, sizeof line, f) == NULL) {
if (feof(f)) { /* the file ended — normal */ }
if (ferror(f)) { /* something went wrong — not normal */ }
}
feofandferroranswer "which", never "whether". Test them after an operation has already reported failure.while (!feof(f))as a loop condition is a classic error:feofonly becomes true after a read has already failed, so the loop body runs one extra time on data that was never read.
The correct shape puts the read in the condition:
while (fgets(line, sizeof line, f) != NULL) {
/* line is valid here */
}
if (ferror(f)) { /* the loop ended because of an error, not EOF */ }4. The Buffer, and Why Closing Is the Write
FILE *f = fopen("f_flight.txt", "w");
for (int i = 0; i < 20; i++) { fprintf(f, "line %d\n", i); }
/* how much is on disk right now? */
int rc = fclose(f);20 lines written; bytes on disk so far: 0
fclose returned 0; bytes on disk now: 150Zero. Twenty successful fprintf calls and not one byte had reached the file. All 150 appeared when the stream was closed.
The finer-grained version shows the mechanism:
after fputs, before any flush : file size = 0
after fflush : file size = 31
after a second fputs : file size = 31
after fclose : file size = 45Buffering exists because writing to a file is expensive and writing one byte at a time would be ruinous. The cost is that a successful write call is not a successful write.
5. The Stream Is an Owned Resource
Every failure mode from Chapter 9.8 has a stream-shaped twin, and they are the same bugs:
| Memory (Module 9) | Streams |
|---|---|
| never freed — a leak | never closed — the file stays open, buffered data may be lost |
| freed twice — a double free | closed twice — undefined, exactly as free(p); free(p); |
| used after freeing | used after closing — undefined |
| two owners | two parts of the program each think they must close it |
So the rules transfer unchanged. One owner per stream. One fclose. Close on every path, including the error paths.
FILE *f = fopen(path, "r");
if (f == NULL) { return -1; }
if (read_header(f) != 0) {
fclose(f); /* the error path closes too */
return -1;
}
if (read_body(f) != 0) {
fclose(f);
return -1;
}
if (fclose(f) != 0) { return -1; }
return 0;With more than two error paths, this is exactly Chapter 9.8 §8's situation and the same answer applies — a single cleanup label, reached by goto, so there is one fclose instead of four:
int rc = -1;
FILE *f = fopen(path, "r");
if (f == NULL) { return -1; }
if (read_header(f) != 0) { goto done; }
if (read_body(f) != 0) { goto done; }
rc = 0;
done:
if (fclose(f) != 0) { rc = -1; }
return rc;One fclose, checked, on every path.
Open resources are limited. A program that leaks streams eventually fails to open another one — and unlike a memory leak, which Chapter 9.9 measured as growing quietly, this one produces a sudden fopen returning NULL for a file that is obviously there.
6. A Complete Program
#include <stdio.h>
static int copy_text(const char *from, const char *to)
{
FILE *in = fopen(from, "r");
if (in == NULL) {
fprintf(stderr, "cannot open %s for reading\n", from);
return -1;
}
FILE *out = fopen(to, "w");
if (out == NULL) {
fprintf(stderr, "cannot open %s for writing\n", to);
fclose(in); /* the first stream is already ours */
return -1;
}
int rc = 0;
char line[512];
while (fgets(line, sizeof line, in) != NULL) {
if (fputs(line, out) == EOF) {
fprintf(stderr, "write failed\n");
rc = -1;
break;
}
}
if (rc == 0 && ferror(in)) { /* the loop may have ended on error */
fprintf(stderr, "read failed\n");
rc = -1;
}
fclose(in); /* a read stream: nothing to flush */
if (fclose(out) != 0) { /* a write stream: this IS the write */
fprintf(stderr, "failed to finish writing %s\n", to);
rc = -1;
}
return rc;
}Six checks, each one a failure this chapter or Chapter 12.3 measured:
Both fopens, because either can fail and the second failure must release the first stream — Chapter 9.8 §8's reverse-order cleanup.
fputs's return, because a write can fail while the buffer is being emptied.
ferror(in) after the loop, because fgets returning NULL means end-of-file or error and the loop cannot tell which.
fclose(out)'s return, for §4's reason.
And one thing deliberately not checked: fclose(in). A read stream has nothing buffered to lose, so its close cannot fail in a way that costs data. Checking it is harmless; not checking it is defensible, and knowing which is which is the point of reading contracts.
Note also that line is 512 bytes and the loop copies whatever it gets. Chapter 12.3 §5's truncation problem does not apply here, because a split line is written out in two pieces and the result is byte-identical — one of the few situations where truncation is genuinely harmless, and it is worth noticing why rather than assuming it in general.
7. What You Should Be Able to Say Now
- A stream is the library's view of a byte source or destination; a
FILE *is an opaque handle — the standard library's own instance of Chapter 10.9's handle pattern. stdin,stdoutandstderrare open beforemain; everything else you open and own.fopenreturnsNULLon failure and must always be checked.- Measured:
"w"and"w+"truncate the file at open time — 9 bytes became 0 before any write."r"on a missing file returnsNULL;"w"creates it. - Pick the mode from your intent:
"a"to add,"r+"to modify in place,"w"only to replace. feofandferroranswer "which", not "whether" — test after a failure.while (!feof(f))runs one iteration too many.- Measured: 20 lines written left 0 bytes on disk; all 150 appeared at
fclose. - So a successful write call is not a successful write — the data is in a buffer.
fclosecan fail and is the call that loses the file. Check it. The stream is invalid afterwards either way.fflushpushes data out early; it does not replace checkingfclose.- A stream is an owned resource — Chapter 9.8's leak, double-free and use-after-free all have stream twins. One owner, one
fclose, on every path. - A read stream's close has nothing to lose; a write stream's close is the write.
8. Exercises
Exercise 1 — choose the mode. For each, give the fopen mode and say what happens to an existing file.
- append a line to a log
- rewrite a configuration file completely
- read a data file, failing if it is absent
- update a record in place without disturbing the rest
- create a file only if it does not already exist
The last one cannot be done with fopen alone. Say why, using the table in §2.
Exercise 2 — the destroyed file. A program does FILE *f = fopen(path, "w");, then validates its input, and returns early if the input is bad — without writing. Describe the state of path afterwards and give two different fixes.
Exercise 3 — fix the loop. This reads one line too many. Explain why, and rewrite it.
while (!feof(f)) {
fgets(line, sizeof line, f);
process(line);
}Exercise 4 — which failure. fgets returned NULL. Write the code that distinguishes the two causes and say what your program should do differently in each case.
Exercise 5 — count the closes. This function has four exit paths. Say how many fclose calls it needs as written, rewrite it with a single cleanup label, and name the chapter that established the pattern.
FILE *f = fopen(p, "r");
if (!f) return -1;
if (step1(f)) return -2;
if (step2(f)) return -3;
fclose(f);
return 0;Exercise 6 — explain the loss. A program writes a 2 KB report, calls fclose without checking, and exits with status 0. The disk was full. Describe exactly what the user sees, using §4's measurement, and say which single line would have reported it.
Exercise 7 — classify the twins. For each stream mistake, name the Module 9 bug it corresponds to and the chapter that measured it.
- the stream is never closed
- the stream is closed in two different functions
- a
FILE *is used afterfclose - two modules both call
fcloseon a handle one of them passed to the other
Exercise 8 — read the contract. Using Chapter 12.1's four questions, write the contract for fflush, including what fflush(NULL) does and what it does not guarantee about data reaching physical storage.
9. Summary
- A
FILE *is an opaque, owned handle;stdin/stdout/stderrcome free. - Check every
fopen— it fails by returningNULL. - Measured:
"w"truncates at open time, 9 bytes to 0, before any write. Choose the mode from intent. feof/ferrorsay which, not whether.while (!feof(f))is one iteration too many.- Measured: 20 lines written, 0 bytes on disk; 150 bytes at
fclose. - A successful write call is not a successful write.
fclosecan fail and is where the file is lost — check it. - The handle is invalid after
fclose, success or not. - One owner, one
fclose, on every path — Chapter 9.8, unchanged. - A read close has nothing to lose; a write close is the write.
Everything in this chapter treated a file as lines of text, and fgets and fputs were enough because a line is a self-describing unit — it ends where the newline is. A great many files are not like that. An image, a recording, a saved structure, a packet capture: these are bytes whose meaning comes from a layout agreed in advance, with no delimiter to find and nothing in the file that says where one field stops. Reading them needs different functions, a mode flag whose effect is invisible on one platform and not on another, and a set of assumptions about layout that Chapter 10.5's padding measurements should already be making you uneasy about. The next chapter is about moving raw bytes, and about which of the assumptions people routinely make when doing so are actually safe.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
