---
title: 'Input and Output'
source: 'https://academia.sh/en/courses/c-fundamentals/input-and-output'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures which part of a format string is decided at compile time and which at runtime'
    - "Reads a write function's return value and notices a truncated write"
    - 'Distinguishes a failed read by counting how many fields a text read assigned'
---

# Input and Output

A format string's text is known at compile time, but what it means is only known at runtime: of nine observations, one is decided at the preprocessor, two at the compiler, one at the linker, five at runtime. The bounding measurement shows that the language itself does not guarantee the match between a format string and its arguments.

The question asked throughout the course was always the same: at which stage is this
decision made. Input and output fits the course's closing lesson well, because it can
spread that question across all four stages within a single line. In a write call, a
macro inside the format string is substituted at the **preprocessor**, the string's text
is fixed at the **compiler**, the called function's definition is found at **linking**,
and what the string means is only known at **runtime**.

This lesson measures that distribution. The measurement continues from where the core's
value computed from data line left off: there, a sum was computed at runtime; here, the
question is how that sum is **written**.

## Nine Observations

```c
// inputoutput.c — at which stage a format string is decided
#include <stdio.h>
#include <string.h>

#define LABEL "measurement"

static void observe(const char *name, const char *stage, const char *evidence) {
    printf("%-34s%-14s%s\n", name, stage, evidence);
}

int main(void) {
    char buffer[64];
    char small[8];

    printf("%-34s%-14s%s\n", "observation", "stage", "evidence");

    /* preprocessor */
    snprintf(buffer, sizeof buffer, "the %s label was placed into the text", LABEL);
    observe("macro in the format string", "preprocessor", buffer);

    /* compiler */
    snprintf(buffer, sizeof buffer, "element count %d", (int)(sizeof buffer / sizeof buffer[0]));
    observe("buffer length", "compiler", buffer);

    const char *constant = "value: %d";
    snprintf(buffer, sizeof buffer, "text is %d characters", (int)strlen(constant));
    observe("text of the format string", "compiler", buffer);

    /* linker */
    observe("formatting function definition", "linker", "no definition in the source, the call was still resolved");

    /* runtime */
    int value = 42;
    const char *format = (value < 100) ? "%d" : "[%d]";
    snprintf(buffer, sizeof buffer, format, value);
    observe("format string selection", "runtime", buffer);

    int width = (int)strlen("four") + 1;
    snprintf(buffer, sizeof buffer, "%*d", width, value);
    observe("field width argument", "runtime", buffer);

    int wanted = snprintf(small, sizeof small, "%s", "hello, world!");
    snprintf(buffer, sizeof buffer, "wanted %d written %d", wanted, (int)strlen(small));
    observe("truncated write", "runtime", buffer);

    int assigned = 0, number = 0;
    assigned = sscanf("temperature 21 degrees", "temperature %d", &number);
    snprintf(buffer, sizeof buffer, "assigned fields %d value %d", assigned, number);
    observe("reading from text", "runtime", buffer);

    int unmatched = sscanf("temperature twentyone", "temperature %d", &number);
    snprintf(buffer, sizeof buffer, "assigned fields %d", unmatched);
    observe("non-matching read", "runtime", buffer);

    printf("\n%-14s%d\n", "preprocessor", 1);
    printf("%-14s%d\n", "compiler", 2);
    printf("%-14s%d\n", "linker", 1);
    printf("%-14s%d\n", "runtime", 5);
    printf("total observations: %d\n", 9);
    return 0;
}
```

```
observation                       stage         evidence
macro in the format string        preprocessor  the measurement label was placed into the text
buffer length                     compiler      element count 64
text of the format string         compiler      text is 9 characters
formatting function definition    linker        no definition in the source, the call was still resolved
format string selection           runtime       42
field width argument              runtime          42
truncated write                   runtime       wanted 13 written 7
reading from text                 runtime       assigned fields 1 value 21
non-matching read                 runtime       assigned fields 0

preprocessor  1
compiler      2
linker        1
runtime       5
total observations: 9
```

**BS14.** The `LABEL` macro was placed into the format string before the compiler ever
saw the source. To the preprocessor this is a text substitution; it does not know the
string is a format string, nor does it know the difference between `%s` and `%d`. This is
why a format string built with a macro passes the preprocessor stage without any trouble,
even if it does not match the argument list.

**BS15.** The buffer's length is known at compile time: `sizeof buffer / sizeof
buffer[0]` is a constant expression and gives `64`. That an array's length is known at
compile time was measured in the Arrays and Strings lesson; here the same information is
used as a write call's **bound**. The format string's text falls into the same class —
`value: %d` is nine characters, and this number is read from the source, not from
runtime.

**BS16.** The formatting function's definition is not in the source. Only its
declaration, coming from a header file, is present, and yet the call was resolved
regardless: the **linker** found the definition. This is the same as the function in
another unit line in the Functions lesson; the only difference is that this time the
definition sits not in a separate source unit, but in the language's standard library.

**BS17.** The remaining five observations fall to **runtime**, and what they all share is
this: the result can change without the source text changing. The format string was
chosen by a condition (`value < 100` is true, so the bracket-free form was used), the
field width came from an argument (`"four"` opened a five-character field), a write was
truncated, a number was read from a piece of text, and one read was unable to assign any
field at all.

## What the Return Value Says

**BS18.** The truncated write line shows what the return value is: `wanted 13 written
7`. A write function returns not the number of characters it managed to fit into the
buffer, but how many characters it **would have written** had it been able to fit them
all. When a thirteen-character piece of text is placed into an eight-byte buffer, seven
characters and a terminator were written, but the return value stayed at thirteen. This
divergence between the two numbers is the only deterministic way of telling whether
truncation occurred: if the return value is smaller than the buffer's length, the write
is complete.

**BS19.** On the read side, the return value counts something else: **how many fields
were assigned**. The same format string assigned one field in one piece of text, zero in
the other. The second piece of text was also valid text and the program did not crash —
only the `number` variable stayed **untouched**. A read's failure is reported not with an
error value but with a **count**, and a source that does not read that count assumes an
unassigned variable is assigned.

## The Bounding Measurement

**BS20.** The language itself does not guarantee the match between a format string and
its arguments. In the measurement above, the format string came, at one point, from a
variable — the `format` pointer, chosen by a condition. In that call, the compiler had no
information available to check the arguments against, by looking at the string's
contents; the string was chosen at runtime. When a format string is a constant written in
the source, the compiler does have the information to check the match, and common
compilers offer this as an optional warning. The check itself is not a guarantee of the
language: the standard classifies an argument mismatching its format specifier directly
as undefined behavior and prescribes no check at all. When the string comes from a
variable, even that information is gone — the check **drops**, and the responsibility
passes entirely to the writer.

This is the sharpest result of the course's four-stage question: the same call shifts
from the **compiler** class to the **runtime** class depending on where the format string
comes from. The source text is almost identical; the only thing that changes is at which
stage a piece of information is available.

## Closing Table

The decisions measured across thirteen lessons are gathered below. The stage cell is
taken from what is written in each lesson's own text; lessons measuring more than one
stage carry both.

| Lesson | Decision measured | Stage | Bounding measurement |
|---|---|---|---|
| The Place of C | The distribution of the core's fourteen observations | all four | Four stages are not a complexity, they are four separate checkpoints |
| Compilation Stages | The side-by-side run of one line from each stage | all four | Stages run in sequence and do not go back; the compiler does not correct the text the preprocessor produced |
| Header Files | A declaration copied as text | preprocessor · linker | A header file is not a contract; the language does not check that a declaration matches its definition |
| The Preprocessor | A macro placing text without knowing type | preprocessor | A macro is not a function; its argument can be substituted more than once |
| Compiler Warnings and Standards | At which stage a warning is born | compiler | A warning is not an error, and a program that compiles without warnings is not thereby correct |
| Data Types | A type's range and integer promotion | compiler | Overflow behavior depends on a type's sign: defined for unsigned, undefined for signed |
| Variables, Scope and Storage Duration | A name's visibility and how long a value lives | compiler · runtime · linker | The same name can be two separate objects in two units; it is the linker that determines this |
| Operators | An operator changing meaning by type | compiler | Precedence determines grouping, it does not always determine evaluation order |
| Control Flow | Which branch is followed | runtime · preprocessor | Which branches exist can be determined by the preprocessor rather than runtime |
| Functions | A call bound to a definition | linker | Neither the compiler nor the linker checks a mismatch between declaration and definition across units |
| Arrays and Strings | Array length versus string length | compiler · runtime | An array's length is known at compile time, but this information does not pass to a function |
| Structs, Unions and Enumerations | Field layout and constants' values | compiler | A union does not track on its own which field was last written |
| Input and Output | A format string's interpretation | runtime | The compiler's match check drops once a format string comes from a variable |

## Summary

- A single write call can carry all four stages at once: the macro was decided at the
  **preprocessor**, the buffer length and the string's text at the **compiler**, the
  function's definition at the **linker**, the string's meaning at **runtime**.
- Of the nine observations, one was measured at the preprocessor, two at the compiler,
  one at the linker, five at runtime; input and output is where the course's four classes
  mix most densely.
- A write function's return value is not the number of characters written but the number
  **wanted**: thirteen were wanted, seven were written, the return stayed at thirteen.
- A read function's return value is the number of **assigned fields**; in a read that
  returns zero, the target variable stays untouched, and the only way to notice this is
  to read that count.
- While a format string is a constant in the source, the compiler can check argument
  agreement; once the string comes from a variable, the same call shifts to the runtime
  class and the check drops.

## Course Wrap-Up

One single question was asked across thirteen lessons: at which stage is this decision
made. The common definition's fourteen observations had been distributed as 4
preprocessor, 4 compiler, 3 linker, 3 runtime, and this distribution never changed
throughout the course — what changed was which mechanism fell into which class.

**The Language and Compilation topic** put the question to the path along which a source
turns into a program: to the existence of the four stages, to declarations being shared
as text, to the preprocessor not knowing type, and to where a warning is born. **The
Basic Syntax topic** carried the same question into the language's own structures: to the
value a type carries, to a name's visibility and a value's storage duration being
separate questions, to an operator changing meaning by type, to where a call is resolved,
to when a length is known, and to how a record type's fields are laid out.

The course's single most repeated finding is this: **the same source text behaves
differently depending on at which stage a piece of information is available.** An
array's length is known at compile time, a string's at runtime; a macro does not know
type, a compiler does; a name is written the same way in two units but may not be the
same object.

The next course looks at memory and pointers: where a value sits, who allocates it, and
when it is released. In C, all of these questions are written in the source, and none of
them is checked on its own.
