Input/Output
Standard streams, formatted input and output, character I/O, line input, string parsing, and numeric conversion.
Input and output are boundaries between a program and the outside world. Boundaries deserve suspicion: input can be missing, malformed, too long, or outside an acceptable range, while output can fail or go to the wrong destination. Good I/O code makes those cases part of the design.
Standard Streams
C models input and output as streams: ordered sequences of characters or bytes. A stream hides many device details, so the same formatted-output function can write to a terminal, file, or redirected pipeline.
Three text streams are predefined for a hosted program:
| Stream | Purpose | Typical destination |
|---|---|---|
stdin | ordinary input | keyboard or redirected file |
stdout | ordinary results | terminal or redirected file |
stderr | diagnostics | terminal |
Their types are FILE *, declared by <stdio.h>. Redirection illustrates why their roles matter:
./report < readings.txt > summary.txt 2> errors.txt
Normal results go to summary.txt; diagnostics remain separate in errors.txt.
Buffering
Libraries often buffer stream operations to reduce expensive device access. stdout connected to a terminal is commonly line-buffered; redirected output may be fully buffered. stderr is not fully buffered by default.
When a prompt has no newline, flush it before waiting for input:
fputs("Temperature: ", stdout);
fflush(stdout);
Do not use fflush(stdin). The C standard defines fflush for output streams and update streams in an output state, not as a portable way to discard console input.
Formatted Output
printf interprets a format string and subsequent arguments:
#include <stdio.h>
int main(void) {
const char *name = "Mina";
int modules = 5;
double average = 86.375;
printf("%-10s modules=%2d average=%6.2f%%\n",
name, modules, average);
return 0;
}
Output:
Mina modules= 5 average= 86.38%
Each conversion specification begins with % and describes exactly one argument. %% prints a literal percent sign and consumes no argument.
Output Conversions
Common conversions include:
| Conversion | Required argument after promotions | Meaning |
|---|---|---|
%d, %i | int | signed decimal integer |
%u | unsigned int | unsigned decimal integer |
%x, %X | unsigned int | hexadecimal integer |
%c | int representing a character | one character |
%s | pointer to a null-terminated character sequence | string |
%f | double | fixed-point floating output |
%e | double | scientific notation |
%g | double | compact floating format |
%p | void * | implementation-defined pointer representation |
%zu | size_t | size value |
Length modifiers change the required type: %ld is for long, %lld for long long, and %Lf for long double. Because variadic functions cannot infer the argument types, a mismatch causes undefined behaviour.
long population = 9000000L;
printf("%ld\n", population); /* correct */
Width
A minimum field width aligns columns:
printf("|%8d|\n", 42); /* right-aligned */
printf("|%-8d|\n", 42); /* left-aligned */
Width does not truncate output. If a representation needs more characters, the field expands.
An asterisk reads width from an int argument:
printf("%*d\n", column_width, value);
Precision
Precision has type-dependent meaning:
printf("%.3f\n", 2.71828); /* three digits after decimal */
printf("%.5s\n", "compiler"); /* at most five characters */
printf("%.6d\n", 42); /* at least six digits */
Use sufficient precision when values must round-trip through text. For exploratory double output, %.17g commonly exposes the represented value, though <float.h> macros provide the portable precision properties.
Return Value
printf returns the number of characters written, or a negative value when an output error occurs. Interactive exercises often omit the check, but durable programs must detect important output failure, especially when writing files.
Formatted Input
scanf reads from stdin according to a format. Unlike printf, most conversions require pointers so the function can store results:
int age = 0;
double height = 0.0;
int converted = scanf("%d %lf", &age, &height);
if (converted != 2) {
fputs("expected an integer and a decimal number\n", stderr);
}
For scanf, %d expects int *, %f expects float *, and %lf expects double *. The l distinction differs from printf, where a float argument is promoted to double.
Return Values
The return value is essential:
- a positive number counts successful assignments;
0means the next input did not match a requested conversion;EOFmeans input ended or an input failure occurred before any assignment.
An assignment count reports matching and storage, not general numeric range validation. For a numeric scanf conversion, the converted result must be representable in the destination object’s type; otherwise the behavior is undefined. Use direct formatted numeric input only when the input contract guarantees representability. For untrusted numeric text, read a bounded line with fgets, then use a strto* function such as strtol or strtod to check syntax and range before narrowing.
Never assume success:
int quantity;
if (scanf("%d", &quantity) != 1) {
fputs("quantity must be an integer\n", stderr);
return 1;
}
Whitespace Rules
Most numeric conversions skip leading whitespace. %c does not. Therefore:
scanf(" %c", &choice);
contains a leading space in the format to consume any amount of whitespace before the character. A trailing whitespace character in a format can make an interactive program appear to wait indefinitely for a following non-whitespace character.
Bounded Strings
Plain %s has no knowledge of the destination capacity and can overflow an array. Supply a maximum field width one less than the buffer capacity:
char word[20];
if (scanf("%19s", word) == 1) {
printf("word: %s\n", word);
}
%s still stops at whitespace, so it cannot read a full name containing spaces. For general interactive input, a bounded line read followed by parsing is more controllable.
Stream Recovery
If %d fails because the next character is x, that character remains unread. Repeating the same scanf("%d", ...) can fail forever. Either consume and discard the rest of the line, or preferably read a complete line with fgets and parse the in-memory text.
Mixing token-oriented scanf with line-oriented fgets also causes surprises: the newline after a scanned number remains for the next line read. Choose one input strategy per interaction when possible.
Character I/O
getchar reads one character from stdin; putchar writes one character to stdout. Although they handle characters, getchar returns int, not char, so it can represent every unsigned char value plus the distinct marker EOF.
#include <stdio.h>
int main(void) {
int ch;
size_t line_count = 0;
while ((ch = getchar()) != EOF) {
if (ch == '\n') {
line_count++;
}
}
printf("lines: %zu\n", line_count);
return 0;
}
Storing getchar() directly in char can make EOF indistinguishable from a valid byte on some implementations.
Check output when failure matters:
if (putchar(ch) == EOF) {
fputs("output failed\n", stderr);
return 1;
}
Line Input
fgets reads at most one less than the supplied capacity, stores a null terminator, and retains a newline if one was read:
#include <stdio.h>
#include <string.h>
enum { LINE_CAPACITY = 128 };
int main(void) {
char line[LINE_CAPACITY];
fputs("Name: ", stdout);
fflush(stdout);
if (fgets(line, sizeof line, stdin) == NULL) {
fputs("no input available\n", stderr);
return 1;
}
line[strcspn(line, "\n")] = '\0';
printf("Hello, %s!\n", line);
return 0;
}
strcspn(line, "\n") returns the first newline position or the terminating-null position if no newline occurs. Replacing that position with \0 removes the newline safely.
Truncated Lines
No newline in the buffer can mean either:
- the final input line ended at end-of-file without a newline, or
- the input exceeded the buffer and unread characters remain.
For interactive input, detect whether the array filled and decide whether to reject the long line, dynamically accumulate it, or discard the remainder deliberately. Silent truncation can transform one invalid input into several misleading inputs.
String Parsing
Separating acquisition from interpretation produces a useful pipeline:
external bytes → bounded line → lexical conversion → range validation → domain validation
Parsing with sscanf
sscanf applies formatted scanning to an existing string:
#include <stdio.h>
int main(void) {
char line[128];
int hours;
int minutes;
char extra;
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
int fields = sscanf(line, "%d:%d %c", &hours, &minutes, &extra);
if (fields != 2 || hours < 0 || hours > 23 ||
minutes < 0 || minutes > 59) {
fputs("expected HH:MM in 24-hour time\n", stderr);
return 1;
}
printf("accepted %02d:%02d\n", hours, minutes);
return 0;
}
The extra %c detects non-whitespace junk after the two expected fields. Conversion success and domain validity are separate questions.
This example also assumes that each decimal field is representable as int. %d cannot turn an out-of-range integer into a recoverable sscanf failure. When field magnitudes are not already constrained by the input contract, split the line into fields and parse each numeric field with an appropriate strto* function before converting to its destination type.
Numeric Conversion
sscanf is convenient for fixed multi-field formats. The strtol family provides finer control for a single integer because it reports where conversion ended and can signal range failure.
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char line[128];
char *end;
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
errno = 0;
long value = strtol(line, &end, 10);
if (end == line) {
fputs("no integer was found\n", stderr);
return 1;
}
if (errno == ERANGE || value < INT_MIN || value > INT_MAX) {
fputs("integer is out of range\n", stderr);
return 1;
}
while (isspace((unsigned char)*end)) {
end++;
}
if (*end != '\0') {
fputs("unexpected characters after the integer\n", stderr);
return 1;
}
int accepted = (int)value;
printf("accepted: %d\n", accepted);
return 0;
}
The conversion to int occurs only after range validation. Chapter 8 expands numeric string conversion and whitespace handling.
Input State Trace
Suppose the program expects an age from 0 through 130 and receives twenty:
| Stage | State |
|---|---|
| prompt | written to stdout, then flushed |
| line read | buffer contains "twenty\n" |
| conversion | strtol consumes no digits, so end == line |
| validation | lexical failure reported to stderr |
| program state | age remains unmodified |
Receiving 200 passes lexical conversion but fails domain validation. These failures deserve different messages.
Line Transaction
A robust input operation can be treated as a transaction:
acquire bytes -> confirm complete record -> parse -> validate -> commit
The destination program state changes only at the final step. This complete program reads one Celsius temperature, rejects an overlong line, distinguishes lexical and range errors, and assigns the accepted value only after every check succeeds.
#include <ctype.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char line[96];
fputs("Celsius temperature: ", stdout);
fflush(stdout);
if (fgets(line, sizeof line, stdin) == NULL) {
if (ferror(stdin)) {
perror("reading temperature");
} else {
fputs("no temperature supplied\n", stderr);
}
return EXIT_FAILURE;
}
if (strchr(line, '\n') == NULL) {
int ch = getchar();
if (ch == EOF && ferror(stdin)) {
perror("finishing temperature line");
return EXIT_FAILURE;
}
if (ch != '\n' && ch != EOF) {
while ((ch = getchar()) != '\n' && ch != EOF) {
}
fputs("temperature line is too long\n", stderr);
return EXIT_FAILURE;
}
}
errno = 0;
char *end;
double candidate = strtod(line, &end);
if (end == line) {
fputs("expected a number\n", stderr);
return EXIT_FAILURE;
}
if (errno == ERANGE || !isfinite(candidate)) {
fputs("temperature is outside the numeric range\n", stderr);
return EXIT_FAILURE;
}
while (isspace((unsigned char)*end)) {
end++;
}
if (*end != '\0') {
fputs("unexpected text after temperature\n", stderr);
return EXIT_FAILURE;
}
if (candidate < -273.15) {
fputs("temperature is below absolute zero\n", stderr);
return EXIT_FAILURE;
}
double celsius = candidate;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
printf("%.2f C = %.2f F\n", celsius, fahrenheit);
return EXIT_SUCCESS;
}
When the buffer contains no newline, the program reads one more character to distinguish three boundary cases. A newline means the record had exactly the maximum storable number of data bytes, so consuming that delimiter completes it. Ordinary end-of-file means the final record ended without a newline. Any other byte proves that the record exceeded capacity, so the discard loop restores the next record boundary before reporting failure. An error while probing remains distinct from ordinary end-of-file.
candidate is temporary. No application variable named celsius exists until lexical, numeric, trailing-text, and physical-domain checks all succeed. That structure prevents half-updated state and scales naturally to records containing several fields: parse all fields into candidates, validate relationships among them, then commit the record as one unit.
Four Input Outcomes
| Input bytes | Conversion state | Domain state | Result |
|---|---|---|---|
25.5\n | full finite number | valid | commit 25.5 |
cold\n | no characters consumed | not checked | lexical error |
25C\n | numeric prefix then C | not checked | trailing-text error |
-300\n | full finite number | below absolute zero | domain error |
Reporting these as one generic “bad input” message discards useful evidence. A user can correct a unit suffix differently from an impossible physical value, and a test suite can verify the exact failed layer.
Stream State Machine
A stream remembers state between calls. It has a current position, buffering state, and end/error indicators. One input call does not operate on an isolated string unless the program first creates that isolation with a line buffer.
For token-oriented scanf, matching failure may leave the offending byte unread:
unread stream: x 4 2 \n
scanf("%d", &value)
result: 0 assignments
unread stream: x 4 2 \n (unchanged)
Repeating the same call repeats the same failure. Recovery must consume or replace the invalid record. Reading one entire line with fgets guarantees that the next iteration can advance, provided overlong lines are also drained deliberately.
End-of-file is likewise state, not a byte stored in a file. An input function attempts a read and then reports that no more bytes were available. That is why while (!feof(stream)) performs one iteration too many: the indicator becomes true only after a read attempt reaches the end. Put the read operation in the loop condition and process only when it succeeds.
Output as Data
Formatting choices form part of a program’s external contract. Human-facing alignment, machine-readable delimiters, precision, locale assumptions, and diagnostic destinations all affect consumers.
int written = printf("%.2f\n", measurement);
if (written < 0) {
/* output failed */
}
A successful printf returns the number of characters produced; a negative result reports failure. Output errors are easy to overlook because buffered output may fail only when the buffer is flushed or the stream is closed. A reporting tool should check the operations whose failure could lose data.
Avoid mixing decoration with data intended for a pipeline. A command whose standard output is a numeric result should send prompts and progress messages elsewhere or offer a quiet/non-interactive mode. Separating results from diagnostics lets another program consume the data without scraping prose.
Conversion Anatomy
A formatted conversion combines flags, width, precision, length modifier, and conversion character. Not every component applies to every conversion.
% - 10 . 3 f
| | | | |
| | | | floating conversion
| | | precision
| | minimum field width
| left alignment flag
conversion begins
For output, width is a minimum, not a truncation limit. A larger value expands the field. Precision means different things by conversion: digits after the decimal point for %f, significant digits for %g, maximum bytes from a string for %s, and minimum digits for integer conversions.
For input, a width limits how many input characters a conversion consumes. With %s, reserve one additional destination element for the null terminator:
char word[12];
if (scanf("%11s", word) == 1) {
/* at most 11 bytes plus '\0' */
}
This reads one whitespace-delimited token, not a whole name containing spaces, and it may leave the remainder of an overlong token unread. Bounds safety and record semantics are separate concerns.
Length modifiers are part of the type contract. %ld expects long for output; %zu expects size_t; %lf in scanf expects double *. A mismatch in a variadic call is not repaired automatically because the function has no ordinary parameter type for each later argument from which to infer the intended object.
Blank, Invalid, and End
These input events must remain distinct:
blank line -> a record containing only a line ending
invalid line -> bytes exist but violate the grammar
end-of-file -> no next record exists
read error -> acquisition failed for another reason
An interactive prompt may retry after blank or invalid input but exit cleanly on EOF because the user signalled no more data. A batch processor may treat a blank line as a meaningful empty field or a malformed record. The stream function reports bytes and status; the application defines policy.
For fgets, an empty line normally produces a successful buffer containing "\n". End-of-file before any byte produces NULL. A final nonempty line without newline succeeds once, then a later read returns NULL with EOF set.
Truncation Recovery
Suppose char line[8] reads the record abcdefghij\n:
first fgets: "abcdefg\0" no newline captured
unread: "hij\n"
If the program immediately calls fgets again as though it were a new record, "hij\n" becomes a misleading second line. A rejecting reader should drain through newline or EOF before returning an “overlong record” status.
If truncation is an accepted policy, the function should still report that it occurred. Silent truncation can cause two distinct identifiers or paths to become equal. The caller may choose to display shortened text while refusing to use it as an identity.
A reusable line reader therefore benefits from a status with at least:
complete line
complete final line without newline
overlong line discarded
end-of-file before data
stream error
Collapsing these states into true/false forces callers to guess which recovery is safe.
Buffering Timeline
stdout may be line-buffered when attached to an interactive terminal and fully buffered when redirected. A prompt without newline can remain invisible while the program waits for input:
fputs("choice: ", stdout);
if (fflush(stdout) == EOF) {
/* output failure */
}
stderr is intended for diagnostics but its exact buffering still should not be used as a synchronization mechanism. Explicitly flush when a protocol requires output to be visible before waiting.
At normal program termination, open output streams are flushed, but errors discovered during that flush cannot affect a return status that ignored them. Important writers should check fflush/fclose at the ownership boundary. Calling abort or suffering undefined behaviour may bypass orderly flushing, which is another reason debug logs sometimes end before the actual fault.
Parser Separation
Keep acquisition and interpretation independently testable:
stream -> bounded line reader -> complete byte record
record -> parser -> candidate values or lexical status
candidates -> validator -> domain values or domain status
domain values -> computation
Tests for parsing can pass ordinary string literals without simulating a terminal. Tests for the line reader can focus on newline, EOF, error, and capacity. This separation also lets file, network, and interactive front ends reuse the same grammar when their record acquisition differs.
Partial Assignments
A multi-field scanf or sscanf can assign a prefix before a later conversion fails:
int day = 0;
int month = 0;
int year = 0;
int fields = sscanf("12/x/2028", "%d/%d/%d",
&day, &month, &year);
fields is 1; day becomes 12, while later outputs retain their previous values. If these variables are already part of application state, the failed parse leaves a partially updated record.
Parse into temporary candidates:
int candidate_day;
int candidate_month;
int candidate_year;
int fields = sscanf(line, "%d/%d/%d",
&candidate_day,
&candidate_month,
&candidate_year);
if (fields == 3 && date_is_valid(candidate_day,
candidate_month,
candidate_year)) {
day = candidate_day;
month = candidate_month;
year = candidate_year;
}
This still needs a trailing-text check if the grammar requires the whole line. The important pattern is that library assignment count describes acquisition progress, while the program controls when candidates become committed state.
Prompt Loop Contract
An interactive retry loop must eventually make progress:
print and flush prompt
attempt to acquire one complete line
EOF -> stop according to policy
error -> report and stop or recover explicitly
invalid record -> report, already consumed, retry
valid record -> commit and leave loop
If matching failure leaves invalid bytes unread, retrying the same token conversion cannot progress. If an overlong line is rejected without draining its suffix, retries process fragments. The line-transaction design makes each invalid attempt consume exactly one record.
Limit retries only when the domain calls for it; an ordinary terminal form may allow correction until EOF. A password or remote service may need attempt limits for security and resource reasons. Input mechanics and policy remain separate layers.
Input Failures
- Ignoring an input function’s return value: destination objects may remain unchanged or indeterminate.
- Using unbounded
%s: a long token can overwrite adjacent storage. - Storing
getcharinchar:EOFcan be lost. - Using
fflush(stdin): it is not a portable input-discard operation. - Mixing
scanfandfgetswithout a plan: leftover delimiters become apparent empty lines. - Assuming
fgetsalways reads a complete line: inspect whether a newline was captured. - Accepting a numeric prefix: reject unwanted trailing characters after conversion.
- Printing diagnostics on
stdout: pipelines cannot separate data from failures.
Stream Reasoning
stdin,stdout, andstderrseparate ordinary input, ordinary results, and diagnostics.- Formatted I/O conversions are type contracts; a mismatch can cause undefined behaviour.
- Input return values report assignments, matching failure, and end-of-file.
getcharmust returnintso all byte values remain distinct fromEOF.fgetsbounds storage but requires deliberate newline and truncation handling.- Read-then-parse keeps stream state manageable and makes lexical, range, and domain validation explicit.
- Successful conversion does not prove that a value is acceptable to the application.
Input Practice
Read the Contract
- Why should ordinary results and diagnostics use different streams?
- What are the three broad meanings of a
scanfreturn value? - Why does
getcharreturnint? - Explain why
%lfdiffers betweenprintfandscanf.
Follow the Stream
- Trace
scanf("%d", &n)when the unread input begins withabcand the call is attempted twice. - For a 10-byte array, determine the stored bytes after
fgetsreadshello\nand after it reads the first part ofabcdefghijkl.
Repair the Parser
- Repair
char name[8]; scanf("%s", name);without claiming that a width-limited token can read spaces. - Correct a program that stores
getchar()incharand loops untilEOF. - Diagnose why
fgetsappears to return an empty line immediately after a successful numericscanf.
Process a Line
- Read a complete name and print it in a 30-character left-aligned field.
- Read a line containing exactly two
doublevalues. Reject missing values, extra non-whitespace text, and a zero second value before division. - Build a menu prompt that repeatedly reads a line until it contains exactly one choice from
A,B, orQ.
Define the Dialogue
- Write down an input policy for overlong lines: maximum length, error message, and how remaining characters are handled. Implement it.
- Compare direct
scanfinput withfgetsplus parsing for a small questionnaire. List the failure cases each design makes easy or difficult to recover from.