Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    FILE *fopen(const char *path, const char *mode);
QuestionAnswer
assumespath and mode are valid strings; mode is one of the standard forms
returnsa FILE * you now own
fails byreturning NULL — convention A
does not promiseanything about why it failed — that is errno, Chapter 12.8

The mode is not a hint. Measured against an existing 9-byte file:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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
ModeReadsWritesIf the file existsIf it does not
"r"yesnoopened at the startfails
"w"noyestruncated to zerocreated
"a"noyesopened, writes go to the endcreated
"r+"yesyesopened at the start, not truncatedfails
"w+"yesyestruncated to zerocreated
"a+"yesyesreads anywhere, writes always at the endcreated

"w" truncates at open time, before you write anything. The measurement shows nine bytes becoming zero at the moment fopen returned. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

FunctionDoesFails by
fgets(buf, n, f)reads a lineNULL — end of file or error
fputs(s, f)writes a stringEOF
fprintf(f, fmt, ...)formatted writenegative
fscanf(f, fmt, ...)formatted reada count, or EOF
fgetc(f) / fputc(c, f)one characterEOF
fread / fwriteblocks of bytesa 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (fgets(line, sizeof line, f) == NULL) {
        if (feof(f))   { /* the file ended — normal */ }
        if (ferror(f)) { /* something went wrong — not normal */ }
    }

feof and ferror answer "which", never "whether". Test them after an operation has already reported failure. while (!feof(f)) as a loop condition is a classic error: feof only 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
20 lines written; bytes on disk so far: 0
fclose returned 0; bytes on disk now: 150

Zero. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after fputs, before any flush : file size = 0
after fflush                  : file size = 31
after a second fputs          : file size = 31
after fclose                  : file size = 45
A write passes through two stages. Your program calls fputs, fprintf or fwrite, which copies the bytes into the stream's buffer in memory; the call returns successfully at that point and nothing has reached the file. The buffer is emptied to the file only when it fills, or when fflush is called, or when fclose is called. Measured: twenty lines written left zero bytes on disk, and all 150 bytes appeared at fclose.Your programfputs · fprintf · fwriteThe stream's buffer20 lines written, 0 byteson diskThe fileon disk — all 150 bytesappeared at fclosealwaysfflush / fclose12
Figure 1 — where a write actually goes. The call you make copies bytes into a buffer in memory and returns successfully; at that moment the file is unchanged. The buffer reaches the file only when it fills, when you call fflush, or when you call fclose. The numbers in the captions are from the measurement above, and they are the reason the right-hand arrow is the important one: for a small file, every byte you wrote crosses it at the moment you close the stream, which makes the close the write rather than a tidy-up after it.

Buffering 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 leaknever closed — the file stays open, buffered data may be lost
freed twice — a double freeclosed twice — undefined, exactly as free(p); free(p);
used after freeingused after closing — undefined
two ownerstwo 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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

Azvya Education Pvt. Ltd.VLSI Mentor
copy a text file, checking every operation
#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

  1. 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.
  2. stdin, stdout and stderr are open before main; everything else you open and own.
  3. fopen returns NULL on failure and must always be checked.
  4. Measured: "w" and "w+" truncate the file at open time — 9 bytes became 0 before any write. "r" on a missing file returns NULL; "w" creates it.
  5. Pick the mode from your intent: "a" to add, "r+" to modify in place, "w" only to replace.
  6. feof and ferror answer "which", not "whether" — test after a failure. while (!feof(f)) runs one iteration too many.
  7. Measured: 20 lines written left 0 bytes on disk; all 150 appeared at fclose.
  8. So a successful write call is not a successful write — the data is in a buffer.
  9. fclose can fail and is the call that loses the file. Check it. The stream is invalid afterwards either way.
  10. fflush pushes data out early; it does not replace checking fclose.
  11. 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.
  12. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 after fclose
  • two modules both call fclose on 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/stderr come free.
  • Check every fopen — it fails by returning NULL.
  • Measured: "w" truncates at open time, 9 bytes to 0, before any write. Choose the mode from intent.
  • feof/ferror say 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. fclose can 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.