78 Streams in reality — <stdio.h> ①
What to know first
Looking back
Chapter 9 said a stream is “a band whose other end the program does not know”, and that this is why the same program serves screen, file and other programs alike. Then when do the bytes a program wrote actually arrive at their destination?
A. Usually not at once. The standard library keeps a buffer per stream, gathers bytes there and sends them out in one go — because system calls are expensive (we meet this again in Part XIII). There are three ways of deciding when to empty it. Full buffering is when the buffer fills, line buffering when a newline is met, unbuffered is immediately. Standard output connected to a terminal is usually line-buffered, and when redirected to a file it turns into full buffering — meaning the moment at which the same program’s output appears changes with what it is connected to, and that is this chapter’s first trap.
The need for this chapter, and its context
<stdio.h> not only because it is the most used. What chapter 9 (the origin of streams) and chapter 23 (output) taught as an idea becomes an API here, and since those chapters sit very early, this is also the oldest debt. The longest-standing debts are paid first.By the end of this chapter
feof, the place introductory books get wrong over and over. The notion of a stream learned in chapter 9 becomes an API here.The questions this chapter answers
- What is done when the line length is unknown?
78.1 Open, write, close — failure can happen three times#
examples-en/stdio1/streams.c
#include <stdio.h>
#include <string.h>
/* Three realities of a stream — buffering, return values, knowing the end */
int main(void)
{
const char *path = "stream_demo.txt";
/* (1) writing: fopen returns null on failure */
FILE *f = fopen(path, "w");
if (!f) { perror("fopen"); return 1; }
/* fprintf can fail too — the return value is the number of characters printed */
int written = fprintf(f, "one\ntwo\nthree\n");
printf("fprintf wrote %d chars\n", written);
/* (2) closing can fail too: a failure while flushing the buffer surfaces here */
if (fclose(f) != 0) { perror("fclose"); return 1; }
/* (3) reading: a line at a time */
f = fopen(path, "r");
if (!f) { perror("fopen"); return 1; }
char line[64];
int n = 0;
while (fgets(line, sizeof line, f)) {
line[strcspn(line, "\n")] = '\0'; /* the idiom for stripping the newline */
printf("line %d: [%s]\n", ++n, line);
}
/* (4) why did it stop — end of file or an error? */
if (ferror(f)) printf("stopped by an error\n");
else if (feof(f)) printf("stopped at end of file\n");
/* (5) a line longer than the buffer is cut and arrives in pieces */
rewind(f);
char tiny[4];
printf("with a 4-byte buffer:\n");
for (int i = 0; i < 3 && fgets(tiny, sizeof tiny, f); i++)
printf(" chunk %d: [%s] (has newline: %s)\n",
i + 1, tiny, strchr(tiny, '\n') ? "yes" : "no");
fclose(f);
remove(path);
return 0;
}
Output
fprintf wrote 14 chars
line 1: [one]
line 2: [two]
line 3: [three]
stopped at end of file
with a 4-byte buffer:
chunk 1: [one] (has newline: no)
chunk 2: [
] (has newline: yes)
chunk 3: [two] (has newline: no)
It is worth noticing that the example checks for failure in three places.
① fopen — on failure it returns null. The reason is left in errno, and perror prints it as a sentence a human can read (chapter 90). The file may not exist, permission may be lacking, or too many files may be open.
② Writing — fprintf returns the number of characters printed and gives a negative value on failure. Code that checks it is rare, but if the disk fills or a pipe breaks it shows itself here.
③ fclose — here is the real trap. It is the place where what remained in the buffer is finally sent out, so it is common for a write failure to show itself for the first time on closing. A program that must not lose data therefore always checks fclose’s return value.
Counter-example. Ignoring the failure of closing
fprintf(f, "%s\n", important);
fclose(f); /* nobody asked whether it failed */
puts("saved"); /* it may in fact not have been saved */In buffered writing, the moment at which “it succeeded” may be said is after closing has succeeded. If it must truly be nailed to the disk, flush with fflush before closing and call the platform’s synchronisation call (fsync and the like) as well — that is what a database does.
78.2 How to know the end of a file — the misuse of feof#
The most widespread wrong answer is here.
examples-en/stdio1/feof_bad.c
#include <stdio.h>
/* Drive a loop with feof and the last item is processed once more —
the classic wrong answer of the C introductory books. */
int main(void)
{
const char *path = "feof_demo.txt";
FILE *f = fopen(path, "w");
if (!f) return 1;
fputs("10\n20\n30\n", f);
fclose(f);
printf("the wrong version — while (!feof(f)):\n");
f = fopen(path, "r");
if (!f) return 1;
while (!feof(f)) {
int v;
fscanf(f, "%d", &v); /* it goes round once more even after the last read fails */
printf(" read %d\n", v); /* 30 is printed twice */
}
fclose(f);
printf("the right version — driven by the read's return value:\n");
f = fopen(path, "r");
if (!f) return 1;
int v;
while (fscanf(f, "%d", &v) == 1)
printf(" read %d\n", v);
if (ferror(f)) printf(" (read error)\n");
fclose(f);
remove(path);
return 0;
}
Output
the wrong version — while (!feof(f)):
read 10
read 20
read 30
read 30
the right version — driven by the read's return value:
read 10
read 20
read 30
Why is while (!feof(f)) wrong? feof is not a prophet but a recorder — the mark saying “the end of the file was reached” is turned on only after a read has failed. So right after reading the last value it is still off, the loop turns once more, and the result of the failed read (= the previous value, unchanged) is used as it stands. That 30 was printed twice in the example is the evidence.
There is one rule. Control the loop by the reading function’s return value. feof and ferror are used after the loop ends, to tell “why did it end”.
| function | success | end or failure |
|---|---|---|
fgets | the buffer pointer | null — tell them apart with feof/ferror |
fscanf | the number of items filled | 0 (format mismatch) or EOF |
fgetc | the character read (an unsigned char as an int) | EOF |
fread | the number of elements read | fewer than requested means the end or an error |
Table 78.1 — Success and failure in <stdio.h> functions
A common misconception. “The result of fgetc may be put in a char”
fgetc returns an int, and that value is either a character in 0–255 or EOF (usually −1). The moment it goes into a char the two can no longer be told apart — on an implementation where char is signed, a 0xFF byte becomes −1 and is identical to EOF, and where it is unsigned, EOF becomes 255 and the loop never ends. So int c; while ((c = fgetc(f)) != EOF) is the canonical form. This one line is also the idiom most often miscopied in introductions to C.78.3 Lines longer than the buffer#
The last part of the first example (streams.c) shows such a line. If the buffer is too small fgets reads only that far and stops — it is not an error. So without checking whether a newline is in there, what you believed to be “one line” may in fact be the front piece of a line.
Read one\n with a 4-byte buffer and it comes split in two: one (no newline) and \n (a newline only). When code handling long lines in the field forgets this fact, one line is quietly processed as two records.
Q. What is done when the line length is unknown?
A. There are three roads. First, a big enough buffer plus a newline check — if there is no newline, read the rest away or treat it as an error. Second, reading while growing it yourself — gather one character at a time with fgetc and enlarge the buffer when needed (chapter 46′s dynamic allocation). Third, a function the platform gives — POSIX’s getline enlarges by itself, but it is not standard. To write with the standard alone the second is the right answer, and using a library so as not to write that code every time is Part XIII’s story.
78.4 Text mode and binary mode#
The b attached to fopen’s second argument selects binary mode. On the Unix family there is no difference, but on Windows there is — text mode turns \n into \r\n when writing and turns it back when reading. So opening a binary file in text mode quietly changes the bytes.
Platform note. Windows’ line-ending conversion
When handling binary data (images, compressed files, serialised structs) always open with "rb" or "wb". Open in text mode and a 0x0A byte grows into 0x0D 0x0A, and on reading it shrinks the other way — the file’s size and content differ. It is the place where the CR/LF story seen in chapter 8 is replayed in the file API.
Conversely, opening a text file in binary mode on Windows leaves a \r at the end of the line, so a line read with fgets ends with an invisible \r — the cause of a failing comparison is often here.
Counter-example. fflush(stdin)
scanf("%d", &n);
fflush(stdin); /* the intent is to empty the input buffer — it is outside the contract */fflush is a function for output streams. Using it on an input stream is behaviour the standard does not define (some implementations merely give it a meaning as an extension — see the box below), and it cannot be used in portable code. To throw away the remaining input you must read it away yourself.
int c;
while ((c = getchar()) != '\n' && c != EOF) { }★ But a question comes before “outside the contract”: throw away what? It is the question chapter 9 raised and chapter 26 examined. The program does not know what is on the other end of standard input. It may be a terminal with someone typing a line at a time, a redirection with a whole file attached, a pipe whose upstream is still running, or a network still delivering. So “discard now” comes to mean discard as much as happened to have arrived by that instant, and that amount is set by the other side’s pace rather than by the program’s logic. It is a place where the same program behaves differently on different days.
State the amount to discard in a form the program can decide — as the loop above does by naming “up to the newline” as its boundary. Which is why this book prefers the two-stage reading that never leaves anything to discard (chapter 26).
Platform note. fflush on an input stream — what POSIX and Microsoft define
Start with the clause of standard C. §7.23.5.2 defines fflush only for an output stream and for an update stream whose most recent operation was not input. The rest is undefined behaviour. Two families of implementations, however, have given that gap a meaning of their own. Looking at what they say makes the conclusion above firmer, not weaker.
POSIX (IEEE Std 1003.1-2024) defines fflush on a stream open for reading like this — if the stream refers to a file capable of seeking and is not already at its end, the offset of the underlying file is set to the stream’s position, and any characters pushed back with ungetc and not yet read are discarded. That is not “discard the input”. The bytes read ahead into the stdio buffer (the tub) are dropped, but because the offset goes back to the stream’s position, the next read reads them again from the same place. Nothing is lost. The point is to line up positions when the same file is shared with another process or another descriptor. And terminals and pipes cannot seek, so they are outside this definition. At the place fflush(stdin) aims at — a terminal with a person at it — POSIX promises nothing either.
Microsoft’s C runtime differs by version. Up to Visual Studio 2013 the runtime emptied the input buffer when fflush was given an input stream. From the new runtime of Visual Studio 2015 (the UCRT), the documentation says that for a stream opened in read mode “the call to fflush has no effect, and any buffer is retained”. That is why old code on the internet says “fflush(stdin) works”, and why the result differs on the same Windows depending on the compiler’s version.
Side by side:
| who defines it | what is defined | stdin on a terminal |
|---|---|---|
| standard C (§7.23.5.2) | nothing | undefined behaviour |
| POSIX | lines up the position, only on a file that can seek | not covered |
| Microsoft (to VS 2013) | empties the input buffer | discards what has arrived so far |
| Microsoft (from VS 2015) | no effect | nothing happens |
Table 78.2 — What fflush does to an input stream
POSIX gave a meaning only where there is a clear boundary. A file is all there already, and its position is a boundary. A terminal has no such boundary, so “discard what?” has no answer, and POSIX left it undefined. Only the old Microsoft runtime discarded “whatever had arrived so far” from a terminal, and that amount, as seen above, is set by the other side’s pace. If what a person typed really must be thrown away, a POSIX terminal has tcflush(fd, TCIFLUSH). But it too discards only what has arrived by that moment, and it empties the operating system’s terminal queue without touching characters already in stdio’s tub. In the end the portable road is the one above: read up to the boundary and throw it away.
78.5 File position and size#
fseek and ftell handle position, but with restrictions. On a text stream the value ftell returns is not guaranteed to be a byte offset, and fseek is safe only with that value or with the combination of SEEK_SET and 0. On a large file long may be too small, so the non-standard fseeko and ftello (POSIX) or a platform API become necessary.
The idiom “to learn a file’s size, go to the end and ftell” is safe only in binary mode, and even then it is meaningless if the file is changing.
Recap
<stdio.h> streams in summary.
| place | rule | if done wrong |
|---|---|---|
fopen | check for null | null dereference |
| writing | check the return value (optional), check fclose (compulsory) | quiet data loss |
| loop control | by the reading return value | misuse of feof — the last value duplicated |
fgetc | put it in an int | confusing EOF with 0xFF |
fgets | check for a newline | a long line processed in pieces |
| binary files | "rb"/"wb" | byte corruption on Windows |
| emptying input | read it away yourself | fflush(stdin) is outside the contract |
| position | ftell gives a byte offset only in binary mode | misunderstanding in text mode |
Table 78.3 — The rules for handling a stream
We have seen the skeleton of streams. The next chapter is the functions that actually read and write on top of it — and the story of a function deleted from the standard.