Standard I/O (<stdio.h>) is how a C program talks to a terminal, a
user, or another program connected via pipes - printf, scanf, and
everything built on stdin/stdout/stderr. It looks simple in a
classroom ("read a number, print a number"), but real-world code has to
handle input that's malformed, too long, the wrong type, empty, or
outright hostile - and most of the classic C security bugs you've heard
about (buffer overflows, format string exploits) trace directly back to
sloppy standard I/O usage. This guide is about doing it the way
production code actually does.
0. Why real-world I/O handling is needed, why industry treats it as a security boundary, and why your syllabus quietly skips most of it
Why it's needed at all
scanf("%d", &n) works perfectly... until someone types "banana"
instead of a number, or pastes in a 10,000-character string where you
expected a name, or pipes in a file with no trailing newline, or hits
Ctrl+D with nothing typed at all. The moment a program's input comes from
a real user (or a real file, or a real network connection) instead of a
tidy classroom test case, every assumption about "the input will look
like I expect" becomes a potential bug - or a security hole.
Why real projects treat this so seriously
- Untrusted input is the #1 source of real-world security bugs.
Buffer overflows from unchecked string reads, format string exploits
from passing user input directly as a
printfformat string, and integer overflows from unchecked numeric parsing are decades-old, extremely well-documented vulnerability classes - and every one of them starts with standard I/O code that trusted its input. gets()was so unsafe it was removed from the C language entirely in C11 - not deprecated, removed. No bound on how much it reads means any input longer than your buffer overwrites adjacent memory, potentially including the return address on the stack. This is the textbook example CS security courses use to teach buffer overflow exploitation, and it's why every real style guide bans it outright.- CLI tools have to work both interactively and in pipelines. A
command-line tool might get typed input from a human, or redirected
input from a file (
./prog < input.txt), or piped input from another program (cat data | ./prog) - real tools detect and handle all three correctly (section 14). - Malformed input must fail safely, not crash or corrupt memory. A production parser that receives garbage input should report an error and continue (or exit cleanly) - not silently misbehave, overflow a buffer, or leave the program in a corrupted state.
- Sensitive input (passwords, tokens) needs special handling - not echoed to the screen, not left sitting in a buffer longer than necessary (section 19).
Why your syllabus (probably) stopped early
- Assignments use trusted input by design - the grader types exactly
what's expected, so
scanf("%d", &n)"just works" in every test case a course actually runs, hiding the entire class of bugs this guide covers. - Teaching robust input handling requires teaching failure modes
first - you can't motivate
strtol's error-checking machinery without first showing what happens whenscanfsilently fails, which needs a lecture of its own that intro courses rarely have room for. - Security-focused I/O (format string exploits, buffer overflow
mechanics) is usually deferred to a dedicated security or systems
course, if your program offers one at all - general "intro to C"
courses treat it as out of scope, even though it's directly caused by
the exact
printf/scanffunctions being taught in that same course. - Terminal-specific behavior (raw input mode, detecting pipes vs. interactive use, disabling echo for passwords) is OS-specific (POSIX), not part of standard C - a portable "intro to C" course has a real reason to leave it out, even though virtually every real CLI tool needs it.
This is maybe the single most consequential gap between "passes a classroom C assignment" and "ships C code that survives contact with real, uncooperative input" - and it's almost entirely learnable from one guide, which is exactly what this one is trying to be.
1. The printf/scanf family - a quick refresher
Both functions are built around format specifiers that describe the type of each value. Getting the specifier wrong for the actual argument type is undefined behavior - the compiler often warns, but doesn't always catch it.
#include <stdio.h>
int main(void) {
int age = 25;
float gpa = 3.8f;
char grade = 'A';
char name[] = "Alice";
printf("%s is %d years old, GPA %.1f, grade %c\n", name, age, gpa, grade);
int readAge;
printf("Enter age: ");
if (scanf("%d", &readAge) == 1) { // ALWAYS check scanf's return value
printf("You entered: %d\n", readAge);
} else {
printf("That wasn't a valid number.\n");
}
return 0;
}
scanf returns the number of items it successfully matched and
assigned - not 0 for "worked," and not automatically 1 just because
you called it. Ignoring that return value is where most scanf-based
bugs start.
2. Why raw scanf("%s", ...) is dangerous
%s with scanf (and fscanf) reads until whitespace with no bound
on length - just like gets(), it will happily write past the end of
your buffer if the input is longer than you allocated for.
#include <stdio.h>
int main(void) {
char name[8];
// DANGEROUS: if the user types more than 7 characters, this
// overflows the buffer -- scanf has no idea how big 'name' is.
printf("Enter name: ");
scanf("%s", name); // no length limit specified!
printf("Hello, %s\n", name);
return 0;
}
The fix is a width specifier, which caps how many characters %s
will write:
#include <stdio.h>
int main(void) {
char name[8];
printf("Enter name: ");
scanf("%7s", name); // reads at most 7 chars + the null terminator = fits in 8
printf("Hello, %s\n", name);
return 0;
}
Even with a width limit, scanf for strings is still fragile in other
ways (section 5) - for real-world input, fgets (section 4) is generally
preferred over scanf entirely.
3. Why gets() must never be used
gets() reads a line into a buffer with no length parameter at all -
there is no way to tell it how big your buffer is, so there is no way for
it to avoid overflowing it. This is exactly the vulnerability class
behind the 1988 Morris Worm, one of the first major internet security
incidents, and why gets() was removed from the C standard entirely in
C11 (not deprecated - deleted).
#include <stdio.h>
int main(void) {
char buffer[10];
// NEVER DO THIS. gets() cannot know buffer is only 10 bytes.
// Typing more than 9 characters overflows into adjacent memory --
// potentially corrupting other variables, or the return address
// on the stack, making this a classic exploitable buffer overflow.
// gets(buffer); // <-- won't even compile on a C11+ standard-conformant compiler
// ALWAYS use fgets instead -- it takes an explicit size limit.
fgets(buffer, sizeof(buffer), stdin);
printf("%s", buffer);
return 0;
}
If you ever see gets() in a codebase (or in a textbook example), treat
it as a bug to fix, not a pattern to follow - fgets is the direct,
safe replacement in every situation gets() was used for.
4. The real-world pattern: fgets + parsing, instead of raw scanf
Production code overwhelmingly prefers reading a whole line with fgets
(which has a hard size limit) and then parsing that line, rather than
using scanf directly on stdin. This separates "safely getting bytes
off the input stream" from "interpreting what those bytes mean," which
makes error handling much cleaner.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char line[64];
printf("Enter your age: ");
if (fgets(line, sizeof(line), stdin) == NULL) {
fprintf(stderr, "No input received.\n");
return 1;
}
// fgets keeps the trailing newline -- strip it before parsing
line[strcspn(line, "\n")] = '\0';
char *endPtr;
long age = strtol(line, &endPtr, 10);
if (endPtr == line || *endPtr != '\0') {
fprintf(stderr, "'%s' is not a valid number.\n", line);
return 1;
}
printf("Age: %ld\n", age);
return 0;
}
This pattern - fgets for safe, bounded input, then strtol/strtod/
sscanf for parsing with explicit error checking - is close to universal
in real-world C command-line tools.
5. The classic scanf leftover-newline trap
scanf("%d", ...) consumes the digits but leaves the trailing \n sitting
in the input buffer. The next read (especially a %c or a subsequent
fgets) then immediately consumes that leftover newline instead of the
input you meant it to read - a famously confusing bug for anyone learning
C.
#include <stdio.h>
int main(void) {
int age;
char name[32];
printf("Enter age: ");
scanf("%d", &age); // reads "25", leaves "\n" sitting in the buffer
printf("Enter name: ");
fgets(name, sizeof(name), stdin); // WRONG RESULT: immediately reads the
// leftover "\n" from the previous
// line, giving an empty name
printf("Age: %d, Name: %s\n", age, name);
return 0;
}
Fix: explicitly consume the rest of the line after a scanf call, before
switching to fgets:
#include <stdio.h>
int main(void) {
int age;
char name[32];
printf("Enter age: ");
scanf("%d", &age);
int c;
while ((c = getchar()) != '\n' && c != EOF) {
// discard leftover characters, including the newline, up to end of line
}
printf("Enter name: ");
fgets(name, sizeof(name), stdin); // now reads the actual next line correctly
printf("Age: %d, Name: %s", age, name);
return 0;
}
This is exactly why many real-world programs avoid mixing scanf and
fgets altogether, and just use fgets + parsing everywhere
(section 4).
6. Validating and sanitizing numeric input with strtol/strtod
atoi/atof have no error reporting at all - they silently return
0 for both "0" and "garbage", making it impossible to tell valid
input from invalid input. strtol/strtod solve this with an "end
pointer" that tells you exactly how much of the string was actually
consumed as a number.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
int parseInt(const char *str, long *outValue) {
char *endPtr;
errno = 0; // strtol doesn't reset errno itself -- you must do it first
long value = strtol(str, &endPtr, 10);
if (endPtr == str) {
return 0; // no digits were consumed at all -- not a number
}
if (*endPtr != '\0') {
return 0; // trailing garbage after the number, e.g. "42abc"
}
if (errno == ERANGE || value > INT_MAX || value < INT_MIN) {
return 0; // out of range, e.g. overflowed a long, or too big for int
}
*outValue = value;
return 1; // success
}
int main(void) {
long result;
printf("%d\n", parseInt("42", &result)); printf("%ld\n", result); // 1, 42
printf("%d\n", parseInt("banana", &result)); // 0 -- rejected
printf("%d\n", parseInt("42abc", &result)); // 0 -- rejected (trailing junk)
printf("%d\n", parseInt("99999999999999", &result)); // 0 -- rejected (out of range)
return 0;
}
This is the real-world standard for "safely turn user text into a
number" - atoi should be treated as unsuitable for anything where the
input isn't already guaranteed valid.
7. Validating and sanitizing string input
Beyond just bounding the length (section 2), real-world input often needs content validation - rejecting or stripping characters that don't belong, especially before that input is used somewhere sensitive (a file path, a shell command, a database query, HTML output).
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// Returns 1 if every character is alphanumeric or underscore -- e.g. for
// validating something like a username before using it in a file path.
int isValidUsername(const char *str) {
if (str[0] == '\0') return 0; // reject empty input
for (size_t i = 0; str[i] != '\0'; i++) {
if (!isalnum((unsigned char)str[i]) && str[i] != '_') {
return 0;
}
}
return 1;
}
int main(void) {
printf("%d\n", isValidUsername("alice_92")); // 1 -- valid
printf("%d\n", isValidUsername("../etc/passwd")); // 0 -- rejected: contains '/' and '.'
printf("%d\n", isValidUsername("")); // 0 -- rejected: empty
return 0;
}
The general principle real-world code follows is allow-listing (explicitly permitting only known-safe characters) rather than block-listing (trying to enumerate every dangerous character) - it's far easier to accidentally miss a dangerous character than to miss a safe one, so allow-listing is the safer default.
8. Safe formatted output: snprintf instead of sprintf
sprintf writes to a buffer with no length limit, exactly like the
gets() problem but on the output side - if the formatted result is
longer than your buffer, it overflows. snprintf takes an explicit size
limit and truncates safely instead.
#include <stdio.h>
int main(void) {
char buffer[16];
// DANGEROUS: if the formatted string exceeds 16 bytes, this
// overflows the buffer just like an unchecked strcpy would.
// sprintf(buffer, "Hello, %s! You are visitor #%d", "Alice", 123456789);
// SAFE: snprintf never writes more than 'sizeof(buffer)' bytes,
// including the null terminator -- excess output is simply truncated.
int written = snprintf(buffer, sizeof(buffer), "Hello, %s! You are visitor #%d",
"Alice", 123456789);
printf("buffer: %s\n", buffer);
printf("would have needed %d bytes (excluding null terminator)\n", written);
return 0;
}
snprintf's return value is the number of bytes that would have been
written with an unlimited-size buffer - useful for detecting truncation
(written >= sizeof(buffer) means the output didn't fully fit).
9. Format string vulnerabilities - never pass user input as the format string
printf(userInput) (rather than printf("%s", userInput)) is a serious,
well-known vulnerability class: if the attacker's input contains format
specifiers like %x or %n, printf will try to read (or, with %n,
write to) arguments that were never actually passed - leaking stack
memory, crashing the program, or in the worst cases enabling arbitrary
memory writes.
#include <stdio.h>
int main(void) {
char userInput[64];
fgets(userInput, sizeof(userInput), stdin);
// DANGEROUS: if userInput contains format specifiers like "%x %x %x %n",
// printf will try to read/write nonexistent arguments -- this is a real,
// historically-exploited vulnerability class (a "format string bug").
// printf(userInput);
// SAFE: user input is always passed as a VALUE to a format specifier,
// never as the format string itself.
printf("%s", userInput);
return 0;
}
The fix is simple and absolute: user-controlled data is never the
format string - it's always an argument to %s (or another appropriate
specifier) in a format string you wrote. This one rule eliminates the
entire vulnerability class.
10. Integer overflow when parsing numeric input
Beyond just rejecting non-numeric input, real code has to guard against
technically-valid numbers that are simply too large for the target type -
strtol reports this via ERANGE, but only if you actually check for it
(as shown fully in section 6). Here's the failure mode in isolation:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
int main(void) {
const char *hugeNumber = "999999999999999999999";
errno = 0;
long value = strtol(hugeNumber, NULL, 10);
if (errno == ERANGE) {
printf("Input was out of range for a long -- rejecting.\n");
} else {
printf("Parsed value: %ld\n", value);
}
// If you need it to additionally fit in a smaller type (like int),
// check against that type's own limits too:
if (value > INT_MAX || value < INT_MIN) {
printf("Value doesn't fit in an int either.\n");
}
return 0;
}
Without this check, a program that assumes "if it parsed, it's a reasonable number" can end up with silently wrapped-around or clamped values feeding into array indices, memory allocation sizes, or loop bounds - a well-known source of real security bugs.
11. Handling EOF and empty input gracefully
Input can simply stop - the user hits Ctrl+D (Unix) / Ctrl+Z (Windows), a piped file runs out, or a redirect points at an empty file. Real code checks for this explicitly instead of assuming input always arrives.
#include <stdio.h>
#include <string.h>
int main(void) {
char line[64];
printf("Enter a command (Ctrl+D to quit): ");
while (fgets(line, sizeof(line), stdin) != NULL) {
line[strcspn(line, "\n")] = '\0';
if (strlen(line) == 0) {
printf("(empty input, try again)\n");
} else {
printf("You entered: %s\n", line);
}
printf("Enter a command (Ctrl+D to quit): ");
}
// fgets returned NULL -- either EOF or a read error
if (feof(stdin)) {
printf("\nEOF reached, exiting cleanly.\n");
} else if (ferror(stdin)) {
fprintf(stderr, "\nAn input error occurred.\n");
}
return 0;
}
A program that instead assumes fgets/scanf always succeeds - and
doesn't check the return value - will typically end up in an infinite
loop or working with stale/garbage data once input actually runs out.
12. Trimming and normalizing input
Real user input almost always needs cleanup before it's usable - trailing newlines (already shown), leading/trailing whitespace, and sometimes case normalization for case-insensitive comparisons.
#include <stdio.h>
#include <string.h>
#include <ctype.h>
// Trims leading and trailing whitespace IN PLACE.
void trim(char *str) {
// Trim trailing whitespace first
size_t len = strlen(str);
while (len > 0 && isspace((unsigned char)str[len - 1])) {
str[--len] = '\0';
}
// Trim leading whitespace by shifting the remaining content left
size_t start = 0;
while (str[start] != '\0' && isspace((unsigned char)str[start])) {
start++;
}
if (start > 0) {
memmove(str, str + start, len - start + 1); // +1 to include the null terminator
}
}
void toLowercase(char *str) {
for (size_t i = 0; str[i] != '\0'; i++) {
str[i] = (char)tolower((unsigned char)str[i]);
}
}
int main(void) {
char input[] = " Hello, World! \n";
trim(input);
printf("trimmed: '%s'\n", input);
toLowercase(input);
printf("lowercased: '%s'\n", input);
return 0;
}
Normalizing input like this before comparing it (e.g. against a list of valid commands) avoids a huge class of "works when I type it, fails when a real user types it slightly differently" bugs.
13. Parsing structured input robustly
Real command-line tools often need to parse a line into multiple fields - robust code checks that every expected field was actually present, rather than assuming a fixed input shape.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void) {
char line[128];
printf("Enter: name age city (e.g. 'Alice 30 Boston')\n> ");
if (fgets(line, sizeof(line), stdin) == NULL) {
return 1;
}
char name[32], city[32];
int age;
// sscanf's return value tells you exactly how many fields matched --
// treat anything less than expected as a parsing failure.
int matched = sscanf(line, "%31s %d %31s", name, &age, city);
if (matched != 3) {
fprintf(stderr, "Expected 3 fields, got %d -- invalid input.\n", matched);
return 1;
}
printf("Name: %s, Age: %d, City: %s\n", name, age, city);
return 0;
}
Note the width limits (%31s) even inside sscanf - the same overflow
risk from section 2 applies here too, since the source buffer's content
is attacker/user-controlled either way.
14. Detecting interactive vs. piped/redirected input
Real CLI tools often behave differently depending on whether stdin is
an interactive terminal or a pipe/file - e.g. showing a prompt only when
a human is actually typing. POSIX's isatty checks this.
#include <stdio.h>
#include <unistd.h> // POSIX -- not part of standard C, but nearly universal on Unix-like systems
int main(void) {
if (isatty(fileno(stdin))) {
printf("Running interactively -- I'll show prompts.\n");
printf("Enter something: ");
} else {
// stdin is a pipe or redirected file -- a prompt here would just
// clutter piped output with no one there to read it
fprintf(stderr, "Reading from a pipe/file, no interactive prompt.\n");
}
char line[64];
if (fgets(line, sizeof(line), stdin) != NULL) {
printf("Got: %s", line);
}
return 0;
}
This is why well-behaved CLI tools don't print prompts (or print them to
stderr rather than stdout) when their input/output is redirected -
./tool < input.txt > output.txt shouldn't have prompt text polluting
output.txt.
15. Buffering behavior differs between terminals and pipes
stdout is typically line-buffered when connected to an interactive
terminal (flushed automatically at each newline) but fully buffered
(flushed only when the buffer fills or the program exits) when redirected
to a file or a pipe - a frequent source of "my output isn't showing up
during a long-running redirected process" confusion.
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("Starting a long process...\n");
// When run as `./program`, this line appears immediately (line-buffered
// terminal). When run as `./program > log.txt`, it may not actually
// hit the file until much later, or until the program exits, because
// stdout is now fully buffered instead.
for (int i = 0; i < 3; i++) {
printf("Step %d\n", i);
fflush(stdout); // force it out immediately regardless of buffering mode --
// important for progress output during redirected/piped runs
sleep(1);
}
return 0;
}
Any program whose output is meant to be watched live (progress bars,
streaming logs) while also supporting redirection needs to fflush
explicitly rather than relying on the default buffering behavior.
16. Prompting correctly: flush before you read
If you printf a prompt and then immediately call a blocking read (like
scanf or fgets), the prompt might not actually be visible yet if
stdout's buffer hasn't been flushed - an explicit fflush(stdout)
guarantees the user sees the prompt before the program waits for their
input.
#include <stdio.h>
int main(void) {
char name[32];
printf("Enter your name: ");
fflush(stdout); // guarantees the prompt is visible before we block waiting for input
fgets(name, sizeof(name), stdin);
printf("Hello, %s", name);
return 0;
}
In practice this often "just works" without the explicit fflush because
terminals are commonly line-buffered - but as shown in section 15, that
guarantee disappears the moment output is redirected, so real-world code
includes the fflush rather than relying on the terminal's default
behavior.
17. Sensitive input: disabling terminal echo for passwords
Reading a password with plain fgets leaves it visible on screen as it's
typed. POSIX's termios API lets you temporarily disable terminal echo,
the standard technique behind every command-line password prompt.
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
void readPassword(char *buffer, size_t size) {
struct termios oldSettings, newSettings;
tcgetattr(STDIN_FILENO, &oldSettings); // save the current terminal settings
newSettings = oldSettings;
newSettings.c_lflag &= ~ECHO; // turn off the ECHO flag specifically
tcsetattr(STDIN_FILENO, TCSANOW, &newSettings);
printf("Password: ");
fflush(stdout);
fgets(buffer, size, stdin);
buffer[strcspn(buffer, "\n")] = '\0';
tcsetattr(STDIN_FILENO, TCSANOW, &oldSettings); // ALWAYS restore, even the echo setting
printf("\n");
}
int main(void) {
char password[64];
readPassword(password, sizeof(password));
printf("Password length: %zu\n", strlen(password));
// In real code: never print or log the password itself.
return 0;
}
This is POSIX-specific (Windows uses a different API, _getch from
<conio.h>, for similar functionality) - cross-platform tools typically
wrap this behind their own portable function.
18. Locale-aware input/output
setlocale affects how certain standard I/O functions interpret and
format things like decimal points, digit grouping, and date formats -
relevant for any real-world program that might run on a machine
configured for a different region/language than the one it was developed
in.
#include <stdio.h>
#include <locale.h>
int main(void) {
printf("Default locale: %.2f\n", 1234.5); // e.g. 1234.50
setlocale(LC_NUMERIC, ""); // adopt the user's OS-configured locale settings
// In some European locales, this would print using a comma as the
// decimal separator instead of a period, if the underlying printf
// implementation honors LC_NUMERIC (behavior is implementation-defined
// for some format specifiers).
printf("After setlocale: %.2f\n", 1234.5);
return 0;
}
Most command-line tools intended for a single, known audience skip this entirely; it matters most for software distributed internationally.
19. Separating interactive output from machine-readable output
A well-behaved CLI tool keeps human-facing messages (progress, prompts,
warnings) separate from its actual data output - typically by sending
diagnostics to stderr and only the real output to stdout - so that
piping the tool's output into another program doesn't also pipe in
unrelated chatter.
#include <stdio.h>
int main(void) {
fprintf(stderr, "Processing input...\n"); // diagnostic -- goes to stderr
int result = 42;
printf("%d\n", result); // the actual DATA -- goes to stdout, and only this
fprintf(stderr, "Done.\n"); // diagnostic -- goes to stderr
return 0;
}
This is why ./tool | grep something works cleanly for real-world
Unix tools - grep only ever sees the data that went to stdout, none
of the progress/status noise that went to stderr.
20. Common pitfalls checklist
Using
gets()- removed from the C standard in C11 specifically because it cannot avoid buffer overflows; usefgetsalways (section 3).Using
scanf("%s", ...)without a width specifier - unbounded, the same overflow risk asgets()(section 2).Not checking
scanf's/fgets's return value - silently proceeding with unassigned or partially-read variables is a common source of bugs that only show up with unexpected input (sections 1, 11).Mixing
scanf("%d", ...)andfgetswithout consuming the leftover newline - the classic "why did my next input get skipped" bug (section 5).Using
atoi/atoffor input that needs validation - they give no way to distinguish"0"from"garbage"; usestrtol/strtodwith proper end-pointer anderrnochecks instead (section 6).Passing user input directly as a
printfformat string - a real, historically-exploited vulnerability class; user data is always an argument, never the format string itself (section 9).Using
sprintfinstead ofsnprintf- unbounded output, same overflow class asgets()/unboundedscanf("%s")(section 8).Trusting that a parsed number fits your target type - always check
ERANGEand the target type's own limits (INT_MAX/INT_MIN, etc.) before using a parsed value (section 10).Printing interactive prompts to
stdoutin a tool meant to support piping - pollutes piped/redirected output; usestderrfor anything that isn't the program's actual data output (section 19).
Quick reference
| Concern | Unsafe / naive approach | Safer real-world approach | Section |
|---|---|---|---|
| Reading a line | gets(buf) | fgets(buf, sizeof(buf), stdin) | 3 |
| Reading a word/string | scanf("%s", buf) | scanf("%Ns", buf) or (better) fgets + parse | 2, 4 |
| Parsing a number | atoi(str) | strtol(str, &endPtr, 10) with error checks | 6 |
| Formatting into a buffer | sprintf(buf, ...) | snprintf(buf, sizeof(buf), ...) | 8 |
| Printing user-controlled text | printf(userInput) | printf("%s", userInput) | 9 |
Leftover newline after scanf | (ignored) | discard chars up to '\n'/EOF with getchar() | 5 |
| Detecting no more input | assuming input always exists | check fgets/scanf return value, feof/ferror | 11 |
| Trimming input | (skipped) | strip whitespace / trailing newline before use | 12 |
| Structured parsing | assuming fixed input shape | check sscanf's matched-field count | 13 |
| Prompt visibility | relying on default buffering | fflush(stdout) before blocking reads | 16 |
| Interactive vs. piped behavior | one-size-fits-all output | isatty(fileno(stdin)) (POSIX) | 14 |
| Password input | plain fgets (echoes to screen) | disable terminal echo via termios (POSIX) | 17 |
| Diagnostics vs. data | everything to stdout | data to stdout, diagnostics to stderr | 19 |
Coverage note
This guide focuses specifically on the gap between "standard I/O that
works in a classroom" and "standard I/O that survives real, untrusted,
unpredictable input" - buffer safety, format string safety, numeric
validation, EOF/empty-input handling, buffering behavior across
terminals and pipes, and the POSIX-level techniques (isatty, disabling
echo) that real CLI tools depend on. Combined with the file handling
guide (which covers FILE *-based file I/O rather than
stdin/stdout), these two guides cover the complete practical space of
I/O in real-world C programs.
Functions in C
Fundamentals of C