C / Basic: Read and Construct Programs / Lesson 3
Input, Output, and Validation
Treat external text as untrusted data, and separate reading a bounded record from interpreting its meaning.
On this page
Lesson goals
- Read text with an explicit capacity.
- Validate conversion end position and range.
- Match formatted output arguments to their declared types.
A stream is not a validated value
The standard streams stdin, stdout, and stderr are library interfaces for input, ordinary output, and diagnostics. A successful read supplies characters, not a trustworthy number. Define a record policy first: whether blank lines are allowed, how long a line may be, and whether trailing text is an error. That policy determines what the parser must check.
fgets reads at most capacity minus one characters and adds a null terminator on success. It can preserve a newline, and a missing newline can mean either end of file or a partial long line. A production line reader must distinguish these cases and discard or assemble the remainder deliberately.
Demonstrate a bounded read
This complete example uses a temporary standard-C stream containing a known short record. That makes the input reproducible while exercising the same fgets contract used for stdin. Every operation that can fail is checked; no external file is overwritten. tmpfile itself may be unavailable at runtime, in which case the program reports failure through its exit status.
The buffer has room for seven characters plus its terminator. The input red plus newline fits, and fputs reproduces the retained newline without adding another. For interactive use, substitute stdin as the stream but retain the capacity and record-completeness policy. Never replace this with gets, which was removed from C11.
#include <stdio.h>
int main(void)
{
char line[8];
FILE *input = tmpfile();
if (input == NULL) return 1;
if (fputs("red\n", input) == EOF) { fclose(input); return 1; }
rewind(input);
if (fgets(line, sizeof line, input) == NULL) { fclose(input); return 1; }
if (fputs(line, stdout) == EOF) { fclose(input); return 1; }
return fclose(input) == 0 ? 0 : 1;
}
red
On the successful I/O path, fgets keeps the newline and fputs writes it exactly once.
Parse a number with evidence
strtol separates conversion from input acquisition. Set errno to zero, request base ten, and inspect both the end pointer and ERANGE. If the end pointer equals the start, no digits were converted. If characters remain, the record may contain unwanted suffixes; accepting surrounding whitespace is a separate policy that needs an explicit check.
After obtaining a long, verify the application range before narrowing to int. errno alone cannot distinguish valid zero from a failed conversion, because zero can be a successful result. The example accepts the entire fixed string and then enforces the small domain zero through one hundred. It rejects 42kg rather than silently interpreting it as 42.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *text = "42";
char *end;
errno = 0;
long value = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE || value < 0 || value > 100) {
puts("invalid");
return 1;
}
printf("accepted=%ld\n", value);
return 0;
}
accepted=42
All characters are consumed, no range error occurs, and %ld matches the long result.
Formatting is a typed interface
printf uses the format string to interpret variadic arguments. %d expects int, %ld expects long, %zu expects size_t, and %f expects double after argument promotions. A mismatched specifier is not a harmless display preference; it violates the library contract and can produce undefined behavior. A compiler can often diagnose literal-format mistakes when warnings are enabled.
Never let external text become the format string: printf("%s", text) treats percent characters as data. Check output errors when successful delivery matters. fprintf(stderr, ...) is useful for diagnostics, but merely sending a message to stderr does not make an operation recoverable or change its return status.
Build a validation pipeline
A reliable pipeline has distinct stages: acquire a complete bounded record, recognize syntax, check representation range, enforce domain rules, and only then update application state. Keep the original text available for a useful diagnostic. Mixing these stages tends to leave partial state changes when the last check fails.
scanf can be used carefully, but requires checking the conversion count, controlling widths for string conversions, and handling leftover input. Its convenience does not remove range or record-boundary concerns. For beginner-facing interfaces, bounded line acquisition followed by explicit parsing makes the contract easier to inspect and test.
Common errors and fixes
Input 12x is accepted as twelve.
- Why it happens
- Only the converted prefix was inspected.
- How to fix it
- Require an allowed suffix and check the end pointer.
- Prevent it next time
- Write tests for valid prefixes followed by garbage.
A long line becomes two application records.
- Why it happens
- The reader mistook a full buffer for a complete line.
- How to fix it
- Detect missing terminators and handle the remaining characters under one record policy.
- Prevent it next time
- Test inputs at capacity minus one, capacity, and beyond capacity.
Output behaves unpredictably with a long argument.
- Why it happens
- The format requests int while the value is long.
- How to fix it
- Use %ld and enable format diagnostics.
- Prevent it next time
- Treat the format and arguments as one reviewed interface.
Check your understanding
1. Classify the records "", "42", "42x", and "101" under the example parser. Explain which check rejects each invalid record.
Show worked solution
The empty string fails end == text because it has no converted digits. "42" consumes the full record and lies in range, so it is accepted. "42x" leaves x at the end pointer and fails the suffix check. "101" has valid decimal syntax but violates the application maximum. Syntax and domain failures are different even when both are reported as invalid.
2. Design tests for a line buffer of eight characters without using unbounded input. Include an input failure case.
Show worked solution
Use a temporary input stream and test an empty stream, "a\n", six letters plus newline, seven letters plus newline, and eight letters plus newline. The last two require inspecting whether newline was captured and handling the remainder consistently. Check the read result before inspecting the array. For an empty stream, report end of input rather than parsing old buffer contents.
Key takeaways
- Acquisition, syntax, range, and domain checks are separate responsibilities.
- Bounded input still needs a policy for truncated records.
- A format string is a contract; external text belongs in a data argument.