Basic Input and Output
Introduction
Almost every useful program talks to the outside world. In C, console I/O goes through the standard streams:
| Stream | Pointer | Typical use |
|---|---|---|
| Standard input | stdin |
Keyboard (or redirected file) |
| Standard output | stdout |
Normal program output |
| Standard error | stderr |
Diagnostics and errors |
The declarations live in <stdio.h>. This chapter covers formatted I/O (printf / scanf), character I/O, safer line input with fgets, buffer pitfalls, and two mini-projects: a menu-driven CLI and a simple invoice printer.
Compile examples with:
gcc -std=c17 -Wall -Wextra -Wpedantic -o prog prog.cThe <stdio.h> library
#include <stdio.h>You get:
- Formatted I/O:
printf,fprintf,sprintf,snprintf,scanf,fscanf,sscanf - Character I/O:
getchar,putchar,fgetc,fputc - Line I/O:
fgets,fputs - File operations (later module):
fopen,fclose, …
Output with printf
Prototype (conceptual)
int printf(const char *format, ...);Returns the number of characters written, or a negative value on error.
Minimal example
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}Format specifier table
| Specifier | Typical argument | Meaning |
|---|---|---|
%d / %i |
int |
Signed decimal |
%u |
unsigned int |
Unsigned decimal |
%ld / %lu |
long / unsigned long |
Long variants |
%lld / %llu |
long long / unsigned long long |
Long long variants |
%f |
double (for printf) |
Fixed-point floating |
%e / %E |
double |
Scientific notation |
%g / %G |
double |
Compact float format |
%c |
int (promoted char) |
Character |
%s |
char * |
String (must be null-terminated) |
%x / %X |
unsigned |
Hexadecimal |
%o |
unsigned |
Octal |
%p |
void * |
Pointer address |
%zu |
size_t |
Size type (C99+) |
%% |
— | Literal % |
Important: for printf, float is promoted to double, so %f is used for both. For scanf, %f is float * and %lf is double *.
Runnable demo — mixed types
#include <stdio.h>
int main(void) {
int age = 25;
unsigned int score = 9001u;
double height = 1.75;
char initial = 'J';
char name[] = "Jordan";
void *self = &age;
printf("Name: %s\n", name);
printf("Initial: %c\n", initial);
printf("Age: %d\n", age);
printf("Score: %u\n", score);
printf("Height: %.2f m\n", height);
printf("Hex age: 0x%x\n", (unsigned)age);
printf("Pointer: %p\n", self);
printf("Percent: 100%% done\n");
return 0;
}gcc -std=c17 -Wall -Wextra -Wpedantic -o print_types print_types.c
./print_typesWidth, precision, flags
#include <stdio.h>
int main(void) {
int n = 42;
double pi = 3.1415926535;
char word[] = "HelloWorld";
printf("|%10d|\n", n); /* right-align in width 10 */
printf("|%-10d|\n", n); /* left-align */
printf("|%010d|\n", n); /* zero-pad */
printf("|%.2f|\n", pi); /* 2 digits after decimal */
printf("|%8.2f|\n", pi); /* width 8, 2 decimals */
printf("|%.5s|\n", word); /* at most 5 chars from string */
return 0;
}| Pattern | Effect |
|---|---|
%10d |
Minimum width 10, right-aligned |
%-10d |
Left-aligned |
%010d |
Zero-filled to width 10 |
%.2f |
Two digits after decimal point |
%.5s |
Print at most 5 characters of a string |
%#x |
Hex with 0x prefix (alternate form) |
fprintf and streams
#include <stdio.h>
int main(void) {
printf("normal message\n");
fprintf(stdout, "also normal\n");
fprintf(stderr, "something went wrong\n");
return 1;
}Redirect separately:
./prog >out.txt 2>err.txtsnprintf — safe string formatting
Prefer snprintf over sprintf to avoid buffer overruns:
#include <stdio.h>
int main(void) {
char buf[32];
int age = 30;
int n = snprintf(buf, sizeof buf, "age=%d", age);
if (n < 0) {
fprintf(stderr, "encoding error\n");
return 1;
}
if ((size_t)n >= sizeof buf) {
fprintf(stderr, "output truncated\n");
}
printf("%s\n", buf);
return 0;
}Input with scanf
Prototype (conceptual)
int scanf(const char *format, ...);Returns the number of input items assigned, or EOF on input failure before any assignment.
Address operator
scanf needs pointers to the objects it will write:
int number;
scanf("%d", &number); /* correct */
/* scanf("%d", number); // wrong: passes value, not address */Arrays (and string buffers) already decay to pointers:
char name[50];
scanf("%49s", name); /* no & for the array */Specifier table for scanf (common cases)
| Specifier | Argument type | Notes |
|---|---|---|
%d |
int * |
Signed int |
%u |
unsigned int * |
Unsigned |
%ld |
long * |
Long |
%f |
float * |
Float |
%lf |
double * |
Double — easy to forget |
%c |
char * |
Does not skip whitespace unless you force it |
%s |
char * |
Stops at whitespace; always set a max width |
%x |
unsigned int * |
Hex input |
Full demo — reading several fields
#include <stdio.h>
int main(void) {
int age;
double height;
char initial;
char name[50];
printf("Enter age: ");
if (scanf("%d", &age) != 1) {
fprintf(stderr, "bad age\n");
return 1;
}
printf("Enter height (meters): ");
if (scanf("%lf", &height) != 1) {
fprintf(stderr, "bad height\n");
return 1;
}
printf("Enter middle initial: ");
if (scanf(" %c", &initial) != 1) { /* leading space skips whitespace */
fprintf(stderr, "bad initial\n");
return 1;
}
printf("Enter first name (no spaces): ");
if (scanf("%49s", name) != 1) {
fprintf(stderr, "bad name\n");
return 1;
}
printf("\n--- Profile ---\n");
printf("%s %c. age=%d height=%.2f\n", name, initial, age, height);
return 0;
}Buffer caveats with scanf
Leftover newlines
scanf("%d", &n) reads the number but leaves the terminating newline in the input buffer. A following %c can read that newline instead of the character you meant.
#include <stdio.h>
int main(void) {
int n;
char ch;
printf("Enter an integer: ");
if (scanf("%d", &n) != 1) {
return 1;
}
printf("Enter a character: ");
/* Without the leading space, ch often becomes '\n' */
if (scanf(" %c", &ch) != 1) {
return 1;
}
printf("n=%d ch='%c' (code %d)\n", n, ch, ch);
return 0;
}Rule of thumb: use a leading space in the format for %c when you want to skip leftover whitespace: scanf(" %c", &ch).
%s stops at whitespace
scanf("%s", buf) cannot read "Ada Lovelace" as one field—it stops at the space. Use fgets for full lines (below).
Always bound string width
char name[20];
scanf("%19s", name); /* leave room for '\0' */Unbounded %s is a classic buffer overflow.
Failed conversions leave bad data
If the user types abc for %d, scanf returns 0 and does not update the int reliably for your purposes—check the return value before using the variable.
Clearing the rest of a line
After a failed or partial read, discard through newline:
#include <stdio.h>
static void clear_line(void) {
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* discard */
}
}
int main(void) {
int n;
printf("Enter integer: ");
while (scanf("%d", &n) != 1) {
fprintf(stderr, "Not an integer. Try again: ");
clear_line();
}
clear_line(); /* eat trailing newline after a good read if needed later */
printf("Got %d\n", n);
return 0;
}Character I/O: getchar and putchar
int getchar(void); /* returns unsigned char cast to int, or EOF */
int putchar(int c);Echo a character
#include <stdio.h>
int main(void) {
int ch;
printf("Press a key then Enter: ");
ch = getchar();
if (ch == EOF) {
return 1;
}
printf("You typed: ");
putchar(ch);
putchar('\n');
return 0;
}Copy stdin to stdout until EOF
#include <stdio.h>
int main(void) {
int ch;
while ((ch = getchar()) != EOF) {
putchar(ch);
}
return 0;
}gcc -std=c17 -Wall -Wextra -Wpedantic -o catish catish.c
./catish < some_file.txt
# interactive: type text, end with Ctrl+D (Unix) or Ctrl+Z then Enter (Windows consoles)Count characters and lines
#include <stdio.h>
int main(void) {
int ch;
unsigned long chars = 0;
unsigned long lines = 0;
while ((ch = getchar()) != EOF) {
chars++;
if (ch == '\n') {
lines++;
}
}
printf("chars=%lu lines=%lu\n", chars, lines);
return 0;
}Use int for the variable that holds getchar() so EOF (−1) is distinguishable from a byte value 0xFF.
Reading lines safely with fgets
Prototype
char *fgets(char *s, int size, FILE *stream);Reads at most size - 1 characters into s, stops early on newline, always null-terminates on success. Returns s on success, NULL on EOF/error.
Basic pattern
#include <stdio.h>
#include <string.h>
int main(void) {
char line[128];
printf("Enter a line: ");
if (fgets(line, sizeof line, stdin) == NULL) {
fprintf(stderr, "no input\n");
return 1;
}
/* strip trailing newline if present */
line[strcspn(line, "\n")] = '\0';
printf("You said: \"%s\"\n", line);
return 0;
}gcc -std=c17 -Wall -Wextra -Wpedantic -o line_echo line_echo.c
./line_echoWhy prefer fgets for text lines?
| Approach | Risk |
|---|---|
gets |
Removed from C11 — never use; unbounded |
scanf("%s") |
Stops at space; easy to overflow without width |
scanf("%[^\\n]") |
Awkward; still easy to get wrong |
fgets |
Bounded; keeps spaces; predictable |
Mixing fgets and scanf
Leftover newlines still bite. Patterns that work:
Option A — prefer fgets for everything, then parse with sscanf:
#include <stdio.h>
int main(void) {
char line[64];
int age;
printf("Age: ");
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (sscanf(line, "%d", &age) != 1) {
fprintf(stderr, "invalid age\n");
return 1;
}
printf("age=%d\n", age);
return 0;
}Option B — after scanf, drain the line before the next fgets:
#include <stdio.h>
#include <string.h>
static void clear_line(void) {
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
}
int main(void) {
int id;
char name[64];
printf("id: ");
if (scanf("%d", &id) != 1) {
return 1;
}
clear_line();
printf("full name: ");
if (fgets(name, sizeof name, stdin) == NULL) {
return 1;
}
name[strcspn(name, "\n")] = '\0';
printf("id=%d name=\"%s\"\n", id, name);
return 0;
}Detecting truncated lines
If fgets fills the buffer without seeing \n, the line was longer than the buffer:
#include <stdio.h>
#include <string.h>
int main(void) {
char line[16];
printf("Short buffer demo (type a long line):\n");
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (strchr(line, '\n') == NULL) {
fprintf(stderr, "warning: line truncated\n");
/* optional: drain the rest of the line */
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
} else {
line[strcspn(line, "\n")] = '\0';
}
printf("stored: \"%s\"\n", line);
return 0;
}sscanf — parse from a string
#include <stdio.h>
int main(void) {
const char *input = "Jordan 25 1.75";
char name[32];
int age;
double height;
if (sscanf(input, "%31s %d %lf", name, &age, &height) != 3) {
fprintf(stderr, "parse failed\n");
return 1;
}
printf("%s is %d years old and %.2f m\n", name, age, height);
return 0;
}Useful when you already have a full line from fgets and want conversion with error checking.
Checking return values
printf
int n = printf("Hello\n");
if (n < 0) {
/* rare on console; more relevant with broken streams */
}scanf
int a, b;
int got = scanf("%d %d", &a, &b);
if (got != 2) {
fprintf(stderr, "expected two integers, got %d\n", got);
return 1;
}Treat return-value checks as part of correct I/O, not optional polish.
Mini-project 2 — Simple invoice printer
Reads a few line items and prints a formatted invoice on stdout.
#include <stdio.h>
#include <string.h>
#define MAX_ITEMS 5
#define NAME_LEN 48
int main(void) {
char customer[64];
char names[MAX_ITEMS][NAME_LEN];
int quantities[MAX_ITEMS];
double prices[MAX_ITEMS];
int count = 0;
double subtotal = 0.0;
const double tax_rate = 0.08;
char line[128];
printf("Customer name: ");
if (fgets(customer, sizeof customer, stdin) == NULL) {
return 1;
}
customer[strcspn(customer, "\n")] = '\0';
printf("How many line items (1-%d)? ", MAX_ITEMS);
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (sscanf(line, "%d", &count) != 1 || count < 1 || count > MAX_ITEMS) {
fprintf(stderr, "Invalid item count\n");
return 1;
}
for (int i = 0; i < count; i++) {
printf("\nItem %d name: ", i + 1);
if (fgets(names[i], NAME_LEN, stdin) == NULL) {
return 1;
}
names[i][strcspn(names[i], "\n")] = '\0';
printf("Quantity: ");
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (sscanf(line, "%d", &quantities[i]) != 1 || quantities[i] < 0) {
fprintf(stderr, "Invalid quantity\n");
return 1;
}
printf("Unit price: ");
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (sscanf(line, "%lf", &prices[i]) != 1 || prices[i] < 0.0) {
fprintf(stderr, "Invalid price\n");
return 1;
}
}
printf("\n========================================\n");
printf(" INVOICE\n");
printf("========================================\n");
printf("Customer: %s\n\n", customer);
printf("%-20s %5s %10s %10s\n", "Item", "Qty", "Unit", "Line");
printf("----------------------------------------\n");
for (int i = 0; i < count; i++) {
double line_total = quantities[i] * prices[i];
subtotal += line_total;
printf("%-20.20s %5d %10.2f %10.2f\n",
names[i], quantities[i], prices[i], line_total);
}
double tax = subtotal * tax_rate;
double total = subtotal + tax;
printf("----------------------------------------\n");
printf("%-28s %10.2f\n", "Subtotal", subtotal);
printf("%-28s %10.2f\n", "Tax (8%)", tax);
printf("%-28s %10.2f\n", "TOTAL", total);
printf("========================================\n");
return 0;
}gcc -std=c17 -Wall -Wextra -Wpedantic -o invoice invoice.c
./invoiceThis uses a C99-style for (int i = 0; ...) loop (fine under -std=c17). Arrays show up formally in a later module; here they are only storage for the demo.
Practical calculator (formatted I/O review)
#include <stdio.h>
int main(void) {
double num1, num2, result;
char op;
char line[128];
printf("Enter expression (e.g. 3.5 * 2): ");
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (sscanf(line, "%lf %c %lf", &num1, &op, &num2) != 3) {
fprintf(stderr, "Could not parse expression\n");
return 1;
}
switch (op) {
case '+': result = num1 + num2; break;
case '-': result = num1 - num2; break;
case '*': result = num1 * num2; break;
case '/':
if (num2 == 0.0) {
fprintf(stderr, "Division by zero\n");
return 1;
}
result = num1 / num2;
break;
default:
fprintf(stderr, "Invalid operator '%c'\n", op);
return 1;
}
printf("%.10g %c %.10g = %.10g\n", num1, op, num2, result);
return 0;
}Best practices (quick reference)
- Check return values of
scanf,fgets, and parsing helpers. - Bound every string read (
%49s,fgets(..., sizeof buf, ...)). - Prefer
fgets+sscanffor interactive line-oriented tools. - Never use
gets. - Send errors to
stderrwithfprintf(stderr, ...). - Mind whitespace when mixing
%dand%c, orscanfandfgets. - Use
snprintf, not unboundedsprintf, when building strings. - Compile with
-std=c17 -Wall -Wextra -Wpedantic.
Summary
printfformats output; width and precision control alignment and detail.scanfreads tokens; always pass addresses (except arrays), check the return count, and bound%s.- Character I/O with
getchar/putcharis simple and stream-friendly; store results inint. fgetsis the default safe way to read a line; strip\nwithstrcspnor equivalent.- Menu CLIs and small printers are good practice for real interactive programs.
Next module: data types and variables—so the values you print and scan have precise sizes, ranges, and conversion rules.
Exercises
Use -std=c17 -Wall -Wextra -Wpedantic for all of these.
- Specifier drill — Print the same integer as decimal, octal, and hex on one line.
- Column report — Print three rows of “name / quantity / price” using width specifiers so columns align.
- Scanf trap lab — Demonstrate the leftover-newline bug with
%dfollowed by%cwithout a leading space, then show the fixed version. - Line reverser (partial) — Read a line with
fgetsand print its length (excluding newline). Full reverse can wait until arrays/strings if needed; do it now if you already can. - Retry loop — Keep asking for an integer until
sscanfafterfgetssucceeds. - Menu extension — Add a fourth menu option that converts Celsius to Fahrenheit.
- Invoice tax flag — Modify the invoice program to ask whether tax applies (
y/n) before printing totals. - stderr routing — Write a program that prints
OKto stdout andBADto stderr; run with redirects and show both files. snprintfcard — Build a string"ID=###;NAME=..."into a 64-byte buffer and report truncation if it occurs.- Char histogram (lite) — Read characters until EOF and count how many were digits (
'0'–'9'). Print the count.