Input & Output

A program without I/O is invisible research. C's I/O is a thin wrapper over three standard streams (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:

SpecifierPrintsExample
%d / %isigned intprintf("%d", -7)
%uunsigned intprintf("%u", 7U)
%f / %gfloat/doubleprintf("%.2f", 3.14159) → 3.14
%c / %schar / C stringprintf("%s", "hi")
%x / %ohex / octalprintf("%x", 255) → ff
%ppointer addressprintf("%p", (void *)p)
%zusize_tprintf("%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.