C Part I · Module 12 · Standard Library, Files and Program Interfaces
Reading Input Safely
A program reading from the outside world knows nothing in advance: not how much is coming, not what shape it has, not whether it is well-formed at all. Each of those unknowns is somewhere for things to go wrong, and C's reading functions handle them with strikingly uneven success. The obvious functions handle them badly in two specific ways that this chapter measures. One family has no way to limit how much it writes, which is why a function was removed from the language over it. The other reports a malformed input and the end of all input with different values that programs routinely conflate — and on a malformed input it leaves the offending characters exactly where they were, so the naive retry loop runs forever without making progress.
Intermediate20 min readCPart IFundamentalsStandard Library
Part I · Module 12 · Chapter 12.3 · Reading Input Safely
1. Why Input Is Harder Than Output
When you call printf, you know the values, their types and how many there are. When you read, you know none of that. Four things can go wrong and all four are normal:
- there is more input than your buffer holds,
- there is less input than you expected,
- the input is malformed for the type you wanted,
- the input has ended.
A reading function must report all four, and the ones C gives you report them with varying degrees of clarity — in one case with the same value for two of them.
Everything below is read with Chapter 12.1's four questions, and two of the three conventions from Figure 1 there turn up immediately.
2. gets — Removed From the Language
char buf[8];
gets(buf); /* never do this */gets reads until a newline with no way to say how big the buffer is. Its contract therefore has a precondition no caller can guarantee: that the input is shorter than the buffer. If it is not, gets writes past the end, which is Chapter 6.4's out-of-bounds write and undefined behaviour.
This is not a style objection. gets was removed from the C standard in C11 — not deprecated, removed — which is a thing the committee has done to almost nothing else.
Your compiler may still provide it for compatibility with old code, and will tell you so:
warning: 'gets' is deprecated: This function is provided for compatibility
reasons only. Due to security concerns inherent in the design of gets(3), it
is highly recommended that you use fgets(3) instead.
[-Wdeprecated-declarations]
getsis gone from the language and present in some libraries as a shim. If you see it in code, the code predates C11 or was never updated. Replace it withfgets.
3. scanf and the Overflow Trap
char buf[10];
scanf("%s", buf); /* unbounded — same problem as gets */A bare %s has exactly gets's defect: nothing tells scanf how large buf is, so a long input writes past the end.
The fix is a field width, and it works:
char buf[10];
int r = scanf("%9s", buf); /* at most 9 chars plus the terminator */input "short" -> scanf %9s returned 1, got "short" (len 5)
input "averyverylongword" -> scanf %9s returned 1, got "averyvery" (len 9)The buffer is safe. Note what else happened, because it is the chapter's second theme arriving early: the long input was truncated, scanf returned 1 — success — and nothing in the return value says the input was cut. The remaining characters are still in the stream, waiting to be read as if they were the next word.
Two rules follow, and the second is the one people forget.
Always give %s a width, and make it one less than the buffer — %9s for char buf[10], because the terminator needs the last byte. The number must be written into the format string, which means it cannot be sizeof buf - 1 without constructing the format at run time, which Chapter 12.2 §4 just told you not to do.
A width prevents the overflow and not the truncation. You are safe and you may still have half a word.
4. scanf and the Silent-Failure Trap
This is the more interesting failure, and it needs measuring rather than describing.
int v = -999;
int r = scanf("%d", &v);scanf returns the number of conversions that succeeded, which for one conversion means 1, 0 or EOF. Those three are not three degrees of the same thing:
input "abc":
first scanf returned 0, v=-999
second scanf returned 0, w=-999
feof=0 ferror=0
input "42" then end of input:
first scanf returned 1, v=42
second scanf returned -1, w=-999
feof=1 ferror=0| Return | Means | The stream |
|---|---|---|
| 1 | the conversion succeeded | advanced past what it read |
| 0 | a conversion failed — the input did not match | not advanced at all |
EOF (-1) | input ended before any conversion | at end of file |
Two consequences, and both are measured above.
The variable is untouched on failure. v was still -999 after a return of 0. A program that ignores the return value and uses the variable reads whatever was there before — and if it was never initialised, that is Chapter 9.12's indeterminate value.
The offending input is not consumed. This is the part that surprises people. After a failed %d, the characters that failed to convert are still at the front of the stream, so the obvious retry loop never advances:
while (scanf("%d", &v) != 1) {
printf("please enter a number\n"); /* nothing was removed */
}Bounded at five attempts so it terminates, on input abc:
attempt 1: scanf returned 0
attempt 2: scanf returned 0
attempt 3: scanf returned 0
attempt 4: scanf returned 0
attempt 5: scanf returned 0
stopped after 5 attempts, last return 0Five calls, five failures, no progress. Unbounded, that loop prints forever.
A
scanfloop that does not distinguish 0 fromEOF, and does not consume the bad input itself, is an infinite loop waiting for malformed input. Recovering requires reading and discarding characters until the next newline — which is to say, doing by hand the thingfgetsdoes for you.
5. fgets — Read a Line, Then Parse It
char *fgets(char *buf, int n, FILE *stream);| Question | Answer |
|---|---|
| assumes | buf has at least n writable bytes; n > 0 |
| returns | buf on success |
| fails by | returning NULL — for end of file or an error, so feof/ferror decides which |
| does not promise | that a whole line was read |
It reads at most n - 1 characters, stops after a newline, and always terminates the result. Crucially, it keeps the newline — which looks like an annoyance and is the only way to detect truncation:
char buf[8];
fgets(buf, sizeof buf, stdin);input "ab\nthis-line-is-too-long\n"
line1: got 3 chars, newline present ? 1 -> "ab."
line2: got 7 chars, newline present ? 0 -> "this-li"
line3: got 7 chars, newline present ? 0 -> "ne-is-t"The first line fitted and ends with a newline. The long line did not, so there is no newline in the result — and the rest of it came back on the next call.
6. Parsing a Line You Already Have
Once the line is in a buffer, converting it is a separate problem with a separate function. strtol is the one to use, and Chapter 12.1 §3 established why it needs care: it uses convention C, so the failure value looks legitimate.
errno = 0;
char *end;
long v = strtol(line, &end, 10);It reports three different things in three different places:
| Situation | How you detect it |
|---|---|
| nothing was converted | end == line — no digits found |
| trailing characters remain | *end is not the end of the string |
the value is out of long's range | errno == ERANGE, and the return is LONG_MAX/LONG_MIN |
And a fourth that strtol cannot report at all: the value fits a long and not an int. strtol returns long, so ERANGE is about long's range. On a target where long is eight bytes and int is four — which is this one — a value of three billion converts successfully and then truncates when you store it. That check is yours.
atoi is the function to avoid: it has no way to report any of the four, returning 0 for both the string "0" and the string "banana".
7. A Routine That Handles All of It
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* returns 1 and sets *out on success; 0 on a bad line; -1 at end of input */
static int read_int(FILE *in, int *out)
{
char line[64];
if (fgets(line, sizeof line, in) == NULL) { return -1; }
size_t len = strlen(line);
if (len > 0 && line[len - 1] != '\n' && !feof(in)) {
return 0; /* the line was truncated */
}
errno = 0;
char *end;
long v = strtol(line, &end, 10);
if (end == line) { return 0; } /* no digits at all */
while (*end == ' ' || *end == '\n' || *end == '\t') { end++; }
if (*end != '\0') { return 0; } /* trailing junk */
if (errno == ERANGE) { return 0; } /* outside long's range */
if (v < -2147483647L || v > 2147483647L) { return 0; } /* outside int */
*out = (int)v;
return 1;
}
int main(void)
{
int v, r;
while ((r = read_int(stdin, &v)) != -1) {
if (r == 1) { printf(" ok %d\n", v); }
else { printf(" rejected\n"); }
}
printf("end of input\n");
return 0;
}Given seven lines covering every case:
input: 42 / abc / -7 / 99999999999999999999 / " 13 " / 5x / (blank)
ok 42
rejected
ok -7
rejected
ok 13
rejected
rejected
end of inputSix checks, each traceable to a measurement in this chapter:
| Check | Catches | From |
|---|---|---|
fgets returned NULL | end of input | §5 |
| no trailing newline | a truncated line | §5 |
end == line | no digits (abc, a blank line) | §6 |
*end after skipping space | trailing junk (5x) | §6 |
errno == ERANGE | outside long | §6 |
| explicit range test | outside int | §6 |
And three properties of the interface are worth copying.
Three distinct return values, so the caller can tell a bad line from the end of input — the thing scanf conflates.
*out is written only on success, so a caller who checks the return can never read a value that was not set. This is Chapter 9.8 §8's "commit only on success", applied to a parse.
The buffer size is sizeof line, not a repeated constant — Chapter 6.1's idiom, valid here because line is an array in this scope (Chapter 10.6 §5).
8. Choosing a Reading Function
| You want | Use | Watch for |
|---|---|---|
| a line of text | fgets | keeps the newline; detect truncation |
| a number | fgets then strtol | all four strtol cases, including the int range |
| one character | getchar | returns int, not char, so EOF is distinguishable |
| a fixed-size record | fread | returns a count that may be short — 12.1 §3 |
| anything at all | not gets | removed from C11 |
| a word | scanf("%9s", ...) | bounded, and still truncates silently |
The getchar row matters more than it looks. It returns int because EOF is a negative value that must be distinguishable from every valid character — so char c = getchar(); is a bug, and comparing the result against EOF after narrowing it loses the distinction. Chapter 2.6's point about char's signedness is the reason.
The general shape for text input: read a whole line, then parse the line. It separates "did I get the input" from "is the input valid", which are different questions with different answers, and it is the only arrangement in which a malformed input cannot stall the loop.
9. What You Should Be Able to Say Now
- Input has four failure modes — too much, too little, malformed, ended — and a reading function must report all four.
getswas removed from C11. Measured: this platform still provides it and warns "'gets' is deprecated ... security concerns inherent in the design".- A bare
scanf("%s", buf)is unbounded and hasgets's defect;%9sfor a 10-byte buffer bounds it. - Measured: a bounded
%9struncated and still returned 1 — success. A width stops the overflow, not the truncation. scanfreturns the number of successful conversions: 1, 0 (malformed), orEOF(ended) — three different things.- Measured: on failure the variable is untouched and the input is not consumed.
- Measured: five consecutive
scanf("%d")calls onabcall returned 0 with no progress — the naive retry loop never terminates. fgetskeeps the newline, which is how truncation is detected; it returnsNULLfor end of file or error, sofeof/ferrordecides.- Measured: a line longer than the buffer became two reads — a bigger buffer moves the threshold, it does not remove it.
strtolreports three things in three places —end == line,*end,errno == ERANGE— and cannot report anintoverflow at all.atoican report nothing;getcharreturnsintso thatEOFis distinguishable.- Read a whole line, then parse the line, and write
*outonly on success.
10. Exercises
Exercise 1 — name the failure. For each, say what goes wrong and with what input.
char buf[16]; gets(buf);
char buf[16]; scanf("%s", buf);
char buf[16]; scanf("%16s", buf);
int v; scanf("%d", &v); use(v);
char c = getchar(); if (c == EOF) { }Exercise 2 — three return values. Write the scanf loop that correctly distinguishes a successful read, a malformed input and the end of input, and that makes progress on malformed input. Say which part is the one most programs omit.
Exercise 3 — detect the truncation. Write the two lines that decide whether an fgets result is a complete line, and say what a missing newline means when feof is true.
Exercise 4 — predict the reads. With char buf[6] and input "hello world\n", give every fgets result in order, with the exact contents and whether a newline is present.
Exercise 5 — complete the parse. This accepts things it should not. Add the missing checks and say what each one catches.
char *end;
long v = strtol(line, &end, 10);
*out = (int)v;
return 1;Exercise 6 — the int range. Explain why errno == ERANGE is not enough when the destination is an int, using the sizes on a target where long is 8 bytes. Give the check, and say what happens without it for the input 3000000000.
Exercise 7 — choose the function. For each, pick from §8 and justify.
- reading a configuration file of
key=valuelines - reading one keypress to answer a yes/no prompt
- reading a 32-byte fixed record from a file
- reading an arbitrarily long line of user input
Exercise 8 — design the interface. Write the signature and contract — in Chapter 12.1's four-question form — for a function that reads one line and reports success, a bad line, a line too long, and end of input as four distinguishable outcomes.
11. Summary
- Input can be too much, too little, malformed, or ended — all four are normal.
getswas removed from C11; measured, it survives here as a warning-generating shim.- Bound every
%swith a width — and measured, a bounded%sstill truncates and still returns success. scanfreturns 1, 0 orEOF— success, malformed, ended. On 0 the variable is untouched and the input is not consumed.- Measured: five retries, five failures, no progress. The naive loop is infinite.
fgetskeeps the newline — the only truncation signal — and returnsNULLfor end-of-file or error.- Measured: one long line became two reads. A bigger buffer only moves the threshold.
strtolreports three ways and misses theintrange entirely.atoireports nothing.getcharreturnsintsoEOFstays distinguishable.- Read a whole line, then parse it, and write the output only on success.
Every read in this chapter came from stdin, which was simply there — open before main started and closed after it ended, requiring nothing of you. That is the exception rather than the rule. 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 or both, because the answer changes what the call does to a file that already exists; and it has to give the stream back when it is finished, with all the obligations Chapter 9.8 described for any resource that has an owner. The next chapter is the full life of a stream — every call that can fail, what each one promises, and the one that programs most often leave out and that can lose data written minutes earlier.
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.
