Input & Output
stdin, stdout, stderr) and a FILE abstraction for disks. Format specifiers drive every conversion — and the single most dangerous function in the language, scanf, sits in this corner. Learn the safe idioms first.
printf — Formatted Output
printf writes its format string to stdout, replacing each %spec with a converted argument. The format must match the argument's type exactly — a mismatch is undefined behavior and the usual source of "it prints garbage". The essential specifiers:
| Specifier | Prints | Example |
|---|---|---|
%d / %i | signed int | printf("%d", -7) |
%u | unsigned int | printf("%u", 7U) |
%f / %g | float/double | printf("%.2f", 3.14159) → 3.14 |
%c / %s | char / C string | printf("%s", "hi") |
%x / %o | hex / octal | printf("%x", 255) → ff |
%p | pointer address | printf("%p", (void *)p) |
%zu | size_t | printf("%zu", n) |
#include <stdio.h>
int main(void) {
char name[] = "Ada";
int level = 3;
double score = 92.34567;
printf("agent %s reached level %d with score %.1f\n",
name, level, score); // %.1f keeps one decimal
printf("|%10s| %-10d| hex %x\n", name, level, level); // widths
return 0;
}
Widths and precision (%10s, %.2f) align columns; %-10d left-justifies. For awkward types such as uint32_t use the PRI macros from <inttypes.h> (see the types page).
scanf — Know Its Limits
scanf reads and converts formatted input — and it is the first function beginners reach for. It has two structural flaws: it writes past your buffer when a string is longer than declared (the classic overflow attack surface), and on malformed input it silently leaves data behind. Fine for toy programs with trusted input; never for production.
#include <stdio.h>
int main(void) {
int age;
printf("age? ");
if (scanf("%d", &age) != 1) { // check the return: one conversion?
printf("not a number\n");
return 1; // malformed input — bail out cleanly
}
printf("age is %d\n", age);
return 0;
}
Notice the return-value check: scanf returns how many conversions succeeded. Ignore it and your program will happily process half-read input.
The Safe Input Idiom: fgets + Parse
Production C reads a line with fgets (which is bounds-aware — it never overwrites your buffer) and then parses the string with sscanf or strtol. This separates "reading bytes" from "interpreting them", so bad input is just a string you can inspect, not a memory corruption.
#include <stdio.h>
#include <stdlib.h> // strtol
#include <errno.h> // errno
int main(void) {
char line[128];
printf("enter a number: ");
if (fgets(line, sizeof(line), stdin) == NULL) {
return 1; // EOF or read error
}
char *end = NULL;
errno = 0;
long value = strtol(line, &end, 10); // parse base-10, end points past digits
if (errno != 0 || end == line) { // overflow OR nothing parsed
printf("not a valid number\n");
return 1;
}
printf("you entered %ld\n", value);
return 0;
}
strtol is the gold standard: it reports overflow through errno, tells you exactly where parsing stopped, and never writes out of bounds. If you remember one input pattern, remember this one.
File I/O
Files are streams too: open with fopen, operate with the same family of read/write functions, and close with fclose. The mode string decides the contract: "r" read, "w" write (truncates!), "a" append, "rb"/"wb" for binary. Always check the FILE * returned — NULL means the open failed.
#include <stdio.h>
int main(void) {
FILE *out = fopen("notes.txt", "w"); // create/truncate for writing
if (out == NULL) { // open failed: disk full, no perms
perror("notes.txt"); // print the system reason
return 1;
}
fprintf(out, "line one\nline two\n"); // formatted file output
fclose(out); // flush buffers + release handle
FILE *in = fopen("notes.txt", "r");
if (in == NULL) return 1;
char buf[256];
while (fgets(buf, sizeof(buf), in) != NULL) { // read line by line
printf("read: %s", buf);
}
fclose(in);
return 0;
}
Binary data uses fread/fwrite with explicit sizes: fwrite(&record, sizeof(record), 1, fp). Struct padding (see composite types) means such files are only portable within one compiler/architecture — a recurring C reality.
stdout vs stderr
Diagnostics belong on stderr, results on stdout. The shell can then redirect them separately (prog 1>out.txt 2>errors.log), and a crash mid-run still leaves your partial results in the output file.
fprintf(stdout, "the result: %d\n", 42); // data the user wants
fprintf(stderr, "warning: input truncated\n"); // operator diagnostics
Next: the preprocessor — the text phase of compilation that runs before the language itself.