---
title: 'C Fundamentals'
source: 'https://academia.sh/en/courses/c-fundamentals'
language: en
updated: '2026-09-26T03:02:41+00:00'
license: 'CC BY-SA 4.0'
lessons: 13
outcomes:
    - 'Distinguishes the four compilation stages and measures which one a decision is made in'
    - 'Uses header files and preprocessor directives in their proper place'
    - 'Writes programs with basic types, operators and control flow'
    - 'Uses arrays, strings, structs and input/output mechanisms together'
---

# C Fundamentals

In C a source becomes a program in four steps rather than one, and where a decision was made cannot be read without knowing this: the course measures every mechanism with a single question — is this decision made in the preprocessor, in the compiler, in the linker, or at runtime? Compilation stages, header files, basic types, operators, functions, arrays and input/output all pass through the same frame.

## Language and Compilation

Which design decisions arise from the language's closeness to hardware, the four stages a source passes through on its way to becoming a program observed separately, declarations shared through header files, the text-level preprocessor knowing no types, and which standard compiler warnings are born from.

### The Place of C

Source: https://academia.sh/en/courses/c-fundamentals/the-place-of-c

In C, a source turning into a running program is not a single step: it passes through four stages, and each stage looks at a separate question. A three-file measurement core shows these four stages in a single run and writes 4 of fourteen observations to the preprocessor, 4 to the compiler, 3 to the linker, and 3 to runtime.

The Rust curriculum closed with five courses, and its last course asked a single question:
where does the guarantee the compiler gives end (M10/K05). In C there is a question that comes
before that one. How many times does a source file change hands before it becomes a running
program? The answer is not one step but four — and the single question asked throughout this
course is which hand a decision is made in.

In Rust, a question of this kind was asked in exactly one place: does a line written in the
source produce a value (M10/K01). In C the same kind of question is asked four times, because
the source passes through four separate hands before it becomes a program, and each hand makes
its own decision by looking only at what it knows. This lesson names those four hands and shows
the difference between them by compiling and running a single program.

#### The Four Stages

As a C source turns into a program, it passes through four hands in sequence. These four names
do not change throughout the course:

| Class | Definition |
|---|---|
| preprocessor | Settled at the text level, before the compiler ever sees the source |
| compiler | Settled while a single unit is translated, from its types and constants |
| linker | Settled when units come together and it becomes clear which definition is used |
| runtime | Settled only once the program runs, by looking at data |

What these four names share is that each answers the same question differently: what
information does it take to make this decision? The preprocessor looks only at text and knows
nothing called a type. The compiler sees a single translation unit, but knows nothing outside
that unit. The linker brings multiple units together, but does not interpret the content of any
of them — it only matches names. Runtime looks only at data; it no longer even has the source
itself in hand.

This measure is not confused with the question Build, Testing and the Ecosystem (M08/K05)
asked — which classpath a type must be found on — or the question Production Go: Testing,
Tooling and Performance (M09/K05) asked — what a piece of information's source is (source,
tool, runtime). What was measured there was where a dependency is looked for, or whom a piece
of information comes from; what is measured here is which stage a decision is made at, and the
number of stages is four.

#### Why the Stages Are Visible

These four stages being visible is a consequence of the language's closeness to hardware. C
hides none of the steps on the path from source to a running binary from the programmer: the
text the preprocessor produces is a readable intermediate output, the object file the compiler
produces can be examined, and which definition the linker chose can be traced. This visibility
is not a shortcoming but the language's own design decision — and it will come up again and
again through the rest of the course.

The C standard (C17) divides the path from source to binary into eight translation phases. This
course groups those eight phases into three classes — preprocessor, compiler, linker — and adds
runtime on top as a fourth class; runtime is not among the standard's translation phases, it is
defined separately, as the execution environment. The decisions the programmer makes are
gathered at these four points. The distinctions among the standard's eight phases — for
instance, decoding the text encoding and joining line endings are separate phases — do not
affect the decisions this course measures; one name is enough for all of them: preprocessor.

#### The Measurement Core

The core used through the rest of the course is three files. Since measuring the linker class
requires more than one unit, the core is multi-unit from the start: one header file, two source
files. The first file carries only declarations — it contains no definitions.

```c
// stage.h — declarations of names defined in other units
#ifndef STAGE_H
#define STAGE_H

/* These two functions are defined in another unit: their calls are resolved at link time. */
int identity(void);
const char *unit_name(void);

/* This variable is also defined in another unit. */
extern int unit_counter;

#endif
```

The second file carries what these declarations correspond to. `choice.c` is a unit that can be
compiled on its own and knows nothing of any line in `stage.c`.

```c
// choice.c — the second unit: definitions live here, calls resolve at link time
#include "stage.h"

int unit_counter = 2;

int identity(void) {
    return 7;
}

const char *unit_name(void) {
    return "choice";
}
```

The third file carries the actual measurement. It writes fourteen separate observations to a
table, adding beside each row which stage settled it.

```c
// stage.c — the core that measures at which stage a decision is made
#include <stdio.h>
#include <string.h>
#include "stage.h"

#define FACTOR 3
#define PASTE(a, b) a##b

enum Color { RED = 1, GREEN = 2, BLUE = 4 };

static int PASTE(add, _three)(int x) {
    return x + 3;
}

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

int main(void) {
    char buffer[64];
    int preprocessor = 0, compiler = 0, linker = 0, runtime = 0;

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

    /* --- preprocessor --- */

    snprintf(buffer, sizeof buffer, "FACTOR placed as the text %d", FACTOR);
    observe("macro value", "preprocessor", buffer);
    preprocessor++;

#ifdef VERBOSE
    observe("conditional body", "preprocessor", "the verbose body entered compilation");
#else
    observe("conditional body", "preprocessor", "the short body entered compilation");
#endif
    preprocessor++;

    snprintf(buffer, sizeof buffer, "the generated name returned %d", PASTE(add, _three)(4));
    observe("token pasting", "preprocessor", buffer);
    preprocessor++;

    observe("header text", "preprocessor", "declarations copied into the source as text");
    preprocessor++;

    /* --- compiler --- */

    snprintf(buffer, sizeof buffer, "sizeof(char) == %d", (int)sizeof(char));
    observe("type size unit", "compiler", buffer);
    compiler++;

    int constant_array[2 * 3 + 1];
    snprintf(buffer, sizeof buffer, "element count %d", (int)(sizeof constant_array / sizeof constant_array[0]));
    observe("constant folding", "compiler", buffer);
    compiler++;

    snprintf(buffer, sizeof buffer, "BLUE constant %d", (int)BLUE);
    observe("enumeration constant", "compiler", buffer);
    compiler++;

    int whole = 7 / 2;
    double fractional = 7 / 2.0;
    snprintf(buffer, sizeof buffer, "whole %d fractional %.1f", whole, fractional);
    observe("operator choice by type", "compiler", buffer);
    compiler++;

    /* --- linker --- */

    snprintf(buffer, sizeof buffer, "the call returned %d", identity());
    observe("function in another unit", "linker", buffer);
    linker++;

    snprintf(buffer, sizeof buffer, "unit declared as %s", unit_name());
    observe("name in another unit", "linker", buffer);
    linker++;

    snprintf(buffer, sizeof buffer, "counter %d", unit_counter);
    observe("variable in another unit", "linker", buffer);
    linker++;

    /* --- runtime --- */

    int data[] = {4, 8, 15, 16};
    int sum = 0;
    for (size_t i = 0; i < sizeof data / sizeof data[0]; i++) {
        sum += data[i];
    }
    snprintf(buffer, sizeof buffer, "sum %d", sum);
    observe("value computed from data", "runtime", buffer);
    runtime++;

    const char *text = "hello, world!";
    snprintf(buffer, sizeof buffer, "length %d", (int)strlen(text));
    observe("string length", "runtime", buffer);
    runtime++;

    const char *verdict = (sum > 40) ? "large" : "small";
    observe("data-dependent branch", "runtime", verdict);
    runtime++;

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

```
observation                   stage         evidence
macro value                   preprocessor  FACTOR placed as the text 3
conditional body              preprocessor  the short body entered compilation
token pasting                 preprocessor  the generated name returned 7
header text                   preprocessor  declarations copied into the source as text
type size unit                compiler      sizeof(char) == 1
constant folding              compiler      element count 7
enumeration constant          compiler      BLUE constant 4
operator choice by type       compiler      whole 3 fractional 3.5
function in another unit      linker        the call returned 7
name in another unit          linker        unit declared as choice
variable in another unit      linker        counter 2
value computed from data      runtime       sum 43
string length                 runtime       length 13
data-dependent branch         runtime       large

preprocessor  4
compiler      4
linker        3
runtime       3
total observations: 14
```

#### Reading the Measurement

The table's four sections show four separate points of view. The four rows in the preprocessor
section know no types, only text: the macro value was placed into the source as text, which
conditional body would enter compilation was settled before the compiler ever saw the source, a
new name was produced from two pieces, and the header file's declarations were copied into the
source. The four rows in the compiler section decide by looking at only a single unit: the unit
of a type's size, the folding of a constant expression, the value of an enumeration constant,
and which meaning the division operator carries were all resolved inside `stage.c` itself —
none of them looked at `choice.c`.

The three rows in the linker section show the exact opposite: all three call a name defined in
`choice.c`, and all three only take on a meaning once the two units come together. `stage.c` on
its own can compile these three rows, but it cannot run them — the missing definitions are
completed only at the linking stage. The three rows in the runtime section look only at data:
the sum, the string length, and the data-dependent branch — none of them sits in the source as a
fixed number, all three are computed while `main` runs. If the same source were run with a
different `data` array, the result of these three rows would change; that is not possible in
the previous three sections, because their result is already known from the source itself —
before it ever runs.

**LC1.** The `observe` function, the `buffer`, and the four counters (`preprocessor`,
`compiler`, `linker`, `runtime`) are the core's own design decision, not a mechanism of C. C
itself has no function that asks "which stage does this line belong to"; what decides which
class a line falls into is the line's position and form in the source, and the `observe` call
only writes this decision onto a line and makes the fourteen observations comparable in a
single run.

**LC2.** All four counters are ordinary `int` variables and are incremented by hand. C's
runtime carries no record of which stage a line belonged to while it was compiled — that
information is already in the past by the time runtime starts. The only way to count the
distribution is to add the right counter by hand next to each observation; the counting
therefore lives in the core's source, in the text the preprocessor and compiler will see.

Four of the fourteen observations were settled at the preprocessor, four at the compiler, three
at the linker, three at runtime, and the output prints this distribution directly. These
numbers are binding for the rest of the course: no lesson changes this distribution, each only
takes its own slice of it.

#### The Same Decision, Four Places

The core's fourteen lines were not chosen at random; each answers the same kind of question at
a different stage: what a value is. The value of the `FACTOR` constant is settled at the
preprocessor, because `#define` is a text-replacement rule applied before the compiler ever
sees the source. The value of the `BLUE` constant is settled at the compiler, because an
enumeration (`enum`) member is a compile-time constant resolved by looking at a single unit. The
value of the `unit_counter` variable is settled at the linker, because the variable is defined
in `choice.c` and `stage.c` reaches it only through an `extern` declaration — the two share the
same address only once linked. The value of the `sum` variable is settled at runtime, because
it cannot be computed without running a loop over an array's elements.

**LC3.** Giving the `enum Color` members explicit values of `1`, `2`, `4` is also a decision
made in the source, not something the language requires. Had no value been given, C would have
incremented the members one by one starting from zero (`0`, `1`, `2`); here the members were
deliberately chosen as powers of two so that each could represent a separate bit. This concerns
not the four stages but a single decision — a constant's numeric value — and that decision, too,
is made at the compiler stage.

All four answer the question "what is a value," but all four answer it in a different place.
What decides which stage answers each time you ask the same question is how you write that
value in the source: write a `#define` and the preprocessor answers, write an `enum` and the
compiler answers, use a name defined in another unit and the linker answers, walk an array and
runtime answers. The programmer can choose among these four; the language itself imposes none
of them.

This pattern will come up again in other forms in later lessons of the course. A computation
that does the same work can be written as a macro in The Preprocessor lesson, or as a declared
call the linker resolves in the Functions lesson; both answer the question "what is this
value," but one at the text level, the other once units come together. The question asked
throughout the course does not change: which hand is this decision, written in the source, made
in?

#### The Bounding Measurement

The existence of four stages is not a complexity but four separate points of inspection. The
same kind of mistake can surface in four separate places, and all four do not carry the same
weight. If the wrong number is written into the `FACTOR` macro, the text is produced wrong and
this spreads silently into the rest of the source — the preprocessor performs no inspection, it
only substitutes. If the `BLUE` constant is given a wrong value, the compiler can see it,
because it is an inconsistency that stays inside a single unit. If a definition in `choice.c`
is deleted, the error surfaces not at the compiler but at the linker, because `stage.c`
continues to compile on its own — what is missing becomes visible only once the units come
together. If a wrong number is added to the `data` array, no stage catches it; the result only
surfaces once it is run with that data.

All four can be called a "wrong value," but where each of the four is caught differs, and none
carries the same force. A preprocessor or compiler error can stop the program from compiling; a
linker error can stop the program from being linked; a runtime error becomes visible only once
that data is encountered, and none of the previous three stages can see it. The abundance of
stages is therefore not a burden but an advantage: where the same class of error gets caught
depends on how you write the source.

This bound should also be read as a warning for the rest of the course: something one stage
does not inspect does not mean the next stage will. An error the preprocessor lets through is
not left for the compiler; an error the compiler lets through is not left for the linker; an
error the linker lets through is not left for runtime — each stage looks only at its own input
and does not question what the one before it did. The sum of the four stages is therefore not a
chain of inspection that completes itself, but four separate viewpoints, each working within its
own bounds.

#### Summary

- A C source passes through four stages on its way to becoming a program: preprocessor,
  compiler, linker, runtime; these four names do not change throughout the course.
- The three-file measurement core classified fourteen observations in a single run: 4
  preprocessor, 4 compiler, 3 linker, 3 runtime.
- The same kind of decision (what a value is) can be made at any of the four stages; which one
  is chosen depends on how the source is written.
- The existence of four stages is not complexity but a multiplicity of inspection points: the
  same kind of error can be caught in four separate places, with four separate weights.

#### Next Step

This lesson named the four stages and showed them side by side in a single run. The next lesson
returns to the same core, but this time takes up the four classes one at a time: which input
each stage looks at when it makes its decision, and why the stages run in an order with no way
back.

### Compilation Stages

Source: https://academia.sh/en/courses/c-fundamentals/compilation-stages

Each of the four stages looks at a different input: the preprocessor only text, the compiler a single unit, the linker multiple units, and runtime only data. A small measurement excerpt runs all four side by side; a separate pair of programs also shows how one line of the same source enters two different compilations, through a decision made in the source beforehand.

The previous lesson named the four stages: preprocessor, compiler, linker, runtime; it ran the
three-file core in a single pass and distributed fourteen observations across these four
classes. This lesson deepens the same four names with the question of which input each looks at
when it makes its decision. The four stages look at the same source, but none of them sees the
same thing — one sees only text, one a single file, one the intersection of multiple files, and
one only the data it holds at that moment. This difference is not an abstract classification;
each stage genuinely has nothing in hand but that input, and this lesson will show it by
measuring.

#### The Four Inputs

What each stage has in hand when it makes its decision differs:

| Class | Input it looks at |
|---|---|
| preprocessor | Only the source's text; none of the concepts of type, constant, or unit exist yet |
| compiler | The entirety of a single translation unit; it does not look outside that unit |
| linker | The definitions multiple units produce; it does not interpret their content, only matches names |
| runtime | The program's own data; the source's text is no longer in hand |

This ordering is also an order of narrowing. The preprocessor holds the widest information — it
can access the entire source text, but understands none of the structure inside it. The compiler
is responsible for only one unit of this text, but it now sees structures such as types and
expressions. The linker does not even see a single unit; it sees only the names units expose
outward. Runtime sees no part of the source at all, only the data the program holds at that
moment.

This narrowing is one-directional: a stage cannot recover information the one before it lost. A
macro name turns into text at the preprocessor, and the compiler never sees that name — what it
sees is only the substituted text, and the information "this was a macro" leaves no trace in the
source. In the same way, a variable's type is reduced at the compiler to a binary arrangement (a
symbol in the object file); the linker matches this symbol only by its name, does not know and
has no need to know which type of variable that name belongs to — if there is a type mismatch,
the compiler has already caught it in its own stage. Every stage works assuming the one before
it has finished its job.

This chain of trust is not limited to the source itself. The linker also assumes that the
object files it takes as input came from a correct compiler; an object file produced by any
tool, as long as it carries the same format, is accepted the same way. This shows why the order
of the four stages depends not on an implementation but on a format — each stage checks not
which tool carried out the step before it, but whether the format it produced conforms to the
contract.

#### Four Classes, Four Lines

The excerpt below takes four lines from the previous lesson's core — one from each class — and
runs only these four side by side. Showing the linker class still requires two units.

```c
// stage.h — declaration of the one name defined in another unit
#ifndef STAGE_H
#define STAGE_H

int identity(void);

#endif
```

`stage.h` is neither compiled on its own nor counted as a unit — what makes a file a unit is
that it is given to the `cc` command as a separate source, not that it is copied into another
file with `#include`. Throughout this lesson `stage.h` will appear to the verifier only as
SOURCE: its content reaches the compiler only after it has mixed into the text of the unit that
`#include`s it. This sharing role of the header file is the subject of the next lesson.

```c
// choice.c — the second unit: the definition lives here, the call resolves at link time
#include "stage.h"

int identity(void) {
    return 7;
}
```

```c
// stage.c — small excerpt measuring the four classes side by side: one line per class
#include <stdio.h>
#include "stage.h"

#define FACTOR 3

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

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

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

    snprintf(buffer, sizeof buffer, "FACTOR placed as the text %d", FACTOR);
    observe("macro value", "preprocessor", buffer);

    int whole = 7 / 2;
    double fractional = 7 / 2.0;
    snprintf(buffer, sizeof buffer, "whole %d fractional %.1f", whole, fractional);
    observe("operator choice by type", "compiler", buffer);

    snprintf(buffer, sizeof buffer, "the call returned %d", identity());
    observe("function in another unit", "linker", buffer);

    int data[] = {4, 8, 15, 16};
    int sum = 0;
    for (size_t i = 0; i < sizeof data / sizeof data[0]; i++) {
        sum += data[i];
    }
    const char *verdict = (sum > 40) ? "large" : "small";
    observe("data-dependent branch", "runtime", verdict);

    return 0;
}
```

```
observation                   stage         evidence
macro value                   preprocessor  FACTOR placed as the text 3
operator choice by type       compiler      whole 3 fractional 3.5
function in another unit      linker        the call returned 7
data-dependent branch         runtime       large
```

**LC4.** This excerpt takes only four of the core's fourteen lines and carries none of the
`preprocessor`, `compiler`, `linker`, `runtime` counters — the goal here is not to recount the
distribution but to place the four classes side by side in a single glance. The counting is
done exactly and only in the previous lesson, and the 4/4/3/3 distribution given there does not
change here.

Each of the four lines confirms the "input it looks at" definition from the previous section.
The `macro value` row was produced without ever seeing `FACTOR`'s numeric value, only by
substituting the text `3` — the preprocessor does not even know that `FACTOR` is an integer
constant. The `operator choice by type` row was produced by looking at the type of two
variables inside `stage.c` itself — whether `choice.c` exists makes no difference. The
`function in another unit` row shows the exact opposite: `identity`'s body does not exist at
all in `stage.c`, only a declaration does; the evidence appears only once it is linked with
`choice.c`. The `data-dependent branch` row also carries no fixed number in the source; `large`
or `small` is computed at runtime by looking only at the content of the `data` array.

The order in which the four lines are produced also reflects the order of the stages. By the
time the program starts running, `stage.c` has already been compiled and linked with
`choice.c`; runtime only proceeds inside this finished program. Even though the order of the
`observe` calls in the source matches the order of the output lines, this is an observation,
not a requirement: which stage a line's decision was made at is independent of what order it
stands in in the output.

Two lines can stand close to each other and still fall into different classes. In the previous
lesson's full core, `macro value` and `enumeration constant` seem at first glance to do the
same job — both produce a "constant value." But because `FACTOR` is a `#define`, its value is
placed into the source as text, and the preprocessor does this without the compiler ever
getting involved; because `BLUE` is an `enum` member, its value is computed as a compile-time
constant expression, and this computation stays within `stage.c`'s own unit bounds. Both look
like constants in the source, neither changes at runtime, but one is finished at the text
level, while the other passes through the compiler's type and expression rules. This
difference shows why the assumption "everything that looks constant falls into the same class"
is wrong: what determines the class is not appearance but how it is written in the source.

#### The Stages' Closure to One Another

One stage being closed to another means there is no channel of communication between them. The
compiler does not ask why the preprocessor produced a text the way it did; it only accepts what
reaches it. The linker does not know how the compiler checked a type; it only looks at the name
of the symbol produced. Runtime no longer carries which line of the source something came from;
all it has in hand is machine instructions and the data itself.

This closure has a concrete consequence: a stage can leave only its own output for the next
one, it cannot carry any extra information about its own process. The preprocessor does not
tell the compiler that `FACTOR` was a macro, because it has no channel to tell it with — what
the compiler sees is only the number `3`, as if it had always been written that way in the
source. Nor does the compiler "tell" the linker that `identity` is defined in another unit; it
only leaves an unresolved name in the object file it produces, and the linker looks for that
name by its own method. At every handoff, the previous stage's process is lost, only its result
remains.

This also explains why the four classes never mix. To understand which class an observation
falls into, it is enough to ask "which stage could have made this decision": if it is a text
substitution, the preprocessor; if it is a type or constant expression, the compiler; if it is
a name across units, the linker; if it is a condition that looks at data, runtime. This
criterion will work the same way not only in this lesson but through the rest of the course.

The linker's "only looking at names" is also a consequence of the same closure. The name
`identity` stands on `stage.c`'s side only as a declaration, and on `choice.c`'s side as a
definition; the linker's job is to match these two sides through the same name. In doing so, it
never looks at what `identity` does or what its body contains — it only checks whether the name
exists on both sides and, if so, how many times it is defined. How a declaration and a
definition are kept apart, and how the linker brings the two together, is the subject of the
next lesson.

The runtime class's closure is the most thorough of all. By the time the program starts
running, no trace of the source remains in its hands; there is no mechanism that knows which
source line the `data-dependent branch` row came from, the running program only looks at its
own data and the results that data produces. This is why "tracing back" a runtime decision to
find its counterpart in the source is not as direct as it is for the other three stages — the
link between the source and runtime is established once, irreversibly, during compiling and
linking.

#### The Bounding Measurement

The stages run in sequence and do not reverse: if the text the preprocessor produces is wrong,
the compiler does not fix it, it only sees the result. The most direct way to show this is to
compile the same conditional body twice — with the only difference being one line added to the
source.

The source choosing between different bodies with `#ifdef` is a common pattern, and the choice
is always settled at the preprocessor, before compilation even begins. The `FACTOR` macro and
the `PASTE` token pasting from the previous lesson belonged to the same class: both reached a
result at the text level before the compiler ever saw the source. The difference is this —
`FACTOR` always expanded to the same text, but whether `VERBOSE` is defined determines which
branch of the source enters compilation. This is a stronger example than a single macro value:
what is chosen here is not a number but a part of the source itself.

```c
// view.c — body choice is settled in the source beforehand by a #define
#include <stdio.h>

/* VERBOSE is not defined here: the preprocessor sends the short body into compilation. */

int main(void) {
    printf("conditional body: ");
#ifdef VERBOSE
    printf("the verbose body entered compilation\n");
#else
    printf("the short body entered compilation\n");
#endif
    return 0;
}
```

```
conditional body: the short body entered compilation
```

```c
// view.c — the same conditional block, with one line added that changes the body choice
#include <stdio.h>

#define VERBOSE

int main(void) {
    printf("conditional body: ");
#ifdef VERBOSE
    printf("the verbose body entered compilation\n");
#else
    printf("the short body entered compilation\n");
#endif
    return 0;
}
```

```
conditional body: the verbose body entered compilation
```

**LC5.** Both files carry the same name (`view.c`), and the second block replaces the first one
on disk; this is to show that these are two separate compilations, not two different runs of the
same program. The only difference is a single `#define` line added at the top of the second
file — the rest of the body is identical, letter for letter.

`#ifdef` here is just one member of the conditional compilation family; the same family
includes `#ifndef`, which tests that a name is not defined (this is also the directive
`stage.h` uses to prevent double inclusion), `#if`, which tests the value of an expression,
`#elif`, which adds another branch, and `#else`, which covers the case where none of them is
true. All of them fall into the same class, because all of them decide without the compiler
getting involved, looking only at the names the preprocessor has defined.

The source of both compilations, looked at line by line, carries both branches of the `#ifdef`
block. But the compiler never sees both branches: the preprocessor checks whether `VERBOSE` is
defined and removes the branch not chosen from the source entirely — in the text that goes to
the compiler, that branch no longer exists, not unparsed or skipped, but as if it had never
been written. The compiler therefore does not say "it eliminated one of two options"; the
source it sees already has only a single body. Once the next stage (the compiler) is reached,
the decision the preprocessor made can no longer be questioned; the only way is to change the
source and start over from the beginning — and that is exactly the difference between the two
blocks.

This shows why the stages are not a chain but a one-directional sequence. In a chain, each link
can inspect the one before it; here, no stage can undo a decision the one before it made, it can
only take over its result. This is also where the name of the Compilation Stages lesson comes
from: a stage is not a step that can be reversed, it is a sequential, one-directional order.

This one-directionality has a practical consequence: when looking for the wrong result a
program gives, asking first which stage settled that result narrows the search. If the
`conditional body` row is seen to be wrong, the place to look is not the compiler or the linker
but the `#define` line in the source — because the text that reaches the compiler has already
been reduced to a single body, and the compiler has done nothing but interpret that text
correctly. The same logic holds for the other three classes: behind the result a class produces
lies only the input that class looked at, not what came before it.

#### Summary

- The four stages look at the same source but see different inputs: the preprocessor text, the
  compiler a single unit, the linker multiple units' definitions, and runtime only data.
- A small excerpt ran one line from each of the four classes side by side; each line confirmed
  the input its own class looks at.
- The stages run in sequence and do not reverse: a branch the preprocessor eliminates never
  reaches the compiler, and the compiler sees it not as "eliminated" but as if it never existed.
- The only way to change a stage's decision is to change the source and run the chain from the
  beginning; no stage receives feedback from the one after it.
- Knowing which stage an error belongs to narrows the search: behind the result lies only the
  input that stage looked at, not what happened before it.

#### Next Step

The linker class was shown in this lesson with only a single call: `identity`'s declaration was
in one file, its definition in another, and `stage.h` was the tool that carried this declaration
between the two. The next lesson looks at how this sharing is set up — how a header file carries
a declaration into multiple units, why a declaration and a definition must be kept separate, and
how far mixing the two up stays harmless.

### Header Files

Source: https://academia.sh/en/courses/c-fundamentals/header-files

A header file does not carry a name, it carries that name's text, and the preprocessor copies it separately into every unit that uses it. This makes it possible for the same declaration to enter two units at once and for both to compile; the measurement proves this by running it, and separately shows what the include guard prevents.

The previous lesson showed the linker class with only a single call: `identity`'s declaration
was in one file, its definition in another, and `stage.h` built the bridge between them. This
lesson looks at that bridge itself. A header file is neither a unit nor compiled on its own; it
only carries text, and that text is copied separately into every unit that `#include`s it.

This single sentence summarizes a fact that will be recalled often through the rest of the
course: in C, a unit is the union of its own source and everything it `#include`s. What the
compiler sees is not the form `stage.c` takes on screen, but the single piece of text the
preprocessor produces after it has resolved every `#include`. A header file determines where it
stands in this union, and this lesson shows why that position must be reserved for declarations
alone.

#### Declaration and Definition

In C, a name has two different appearances in the source. A declaration says that a name
exists, its type, and how it is called, but gives no body. A definition gives what that name
corresponds to — a function's body, a variable's allocated place. A name can be declared in more
than one place, but can be defined in only one place; this distinction is the reason header
files exist.

A unit does not have to see the definition of every name it calls; seeing only its declaration
is enough — this is why a single header file can be shared among dozens of units. To produce a
call, the compiler must know the name's type, how many parameters it takes, and what it
returns — all of this is in the declaration. Where the name's actual counterpart is located is
a question that does not concern the compiler; the linker will answer that question. Because of
this distinction, a unit can call a definition it has never seen — as long as it has a correct
declaration.

```c
// stage.h — declarations of names defined in other units
#ifndef STAGE_H
#define STAGE_H

/* These two functions are defined in another unit: their calls are resolved at link time. */
int identity(void);
const char *unit_name(void);

/* This variable is also defined in another unit. */
extern int unit_counter;

#endif
```

`stage.h` declares three names, it defines none of them. `identity` and `unit_name` have no
body; the `extern` standing in front of `unit_counter` says exactly this — "this name is
defined elsewhere, it is only declared here." The definitions themselves stand in the second
unit.

```c
// choice.c — the second unit: definitions live here, calls resolve at link time
#include "stage.h"

int unit_counter = 2;

int identity(void) {
    return 7;
}

const char *unit_name(void) {
    return "choice";
}
```

The third file includes `stage.h` on its own, unaware of `choice.c`, and calls all three.

```c
// stage.c — excerpt linked to the second unit through the declarations the header carries
#include <stdio.h>
#include "stage.h"

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

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

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

    observe("header text", "preprocessor", "declarations copied into the source as text");

    snprintf(buffer, sizeof buffer, "the call returned %d", identity());
    observe("function in another unit", "linker", buffer);

    snprintf(buffer, sizeof buffer, "unit declared as %s", unit_name());
    observe("name in another unit", "linker", buffer);

    snprintf(buffer, sizeof buffer, "counter %d", unit_counter);
    observe("variable in another unit", "linker", buffer);

    return 0;
}
```

```
observation                   stage         evidence
header text                   preprocessor  declarations copied into the source as text
function in another unit      linker        the call returned 7
name in another unit          linker        unit declared as choice
variable in another unit      linker        counter 2
```

**LC6.** `stage.h` itself never appears with the `preprocessor` label in any observation; what
appears in its place, the `header text` row, is a result, not `stage.h` itself. What the
preprocessor does amounts to nothing more than copying text: the line `#include "stage.h"` is
replaced with the content of `stage.h` before the compiler ever gets involved. The
`identity(void)` declaration seen in `stage.c` was therefore never hand-typed into `stage.c`'s
source — it arrived there by being copied. The same line was also copied into `choice.c`'s
source; the two copies are independent of one another, and both became part of their own unit.
The `#include` line seen while editing a file is only a summary of the compiled text — the real
source is the expanded text that appears after the preprocessor runs, never seen with the eye.

`choice.c` also includes `stage.h`, and the same three declarations are copied into its source
too. Both units compile with these declarations, and both compile without a warning — the same
declaration entered two separate places and neither was troubled by it, because a declaration
makes a promise, it does not reserve a place. The definition, on the other hand, exists only
once, in `choice.c`; `stage.c` uses all three names but carries the definition of none of them,
borrowing the definitions at the linking stage.

The four rows in the output show how this borrowing works. The `header text` row is an
observation about the source itself, because it proves that a single piece of text carries the
declaration — but what matters is that `stage.c` can use these three names. The `function in
another unit` and `name in another unit` rows reach `choice.c`'s function definitions; the
`variable in another unit` row reaches a variable definition. All three use the same bridge: the
declaration in `stage.h` tells `stage.c` "this name exists, here is its type"; its actual
counterpart arrives only at linking time. While `stage.c` compiles on its own, it does not even
know that `unit_counter`'s value is `2` — all it knows is that some integer like this exists
somewhere.

#### What a Header Shares, What It Does Not

`stage.h` carrying only declarations is not a choice, it is a necessity. Since a header file can
be included in multiple units, if a function definition — together with its body — were put
into it, that body would be copied separately into every unit that includes it. Both `stage.c`
and `choice.c` include `stage.h`; if `identity`'s body stood there, both units would have
defined the same name, and this would produce a "same name defined twice" conflict at linking.
This is why the definition must stand only in one unit, not in the header — the include guard
prevents this within a single unit, but it does not prevent a definition from spreading across
multiple units; what prevents that is never putting the definition in the header at all.

This also explains why the line `extern int unit_counter;` is not written as
`int unit_counter = 2;`. The `extern` keyword tells the compiler "do not reserve a place here,
this name already exists elsewhere"; a variable written without it is actually a definition and
reserves a place. Had a variable without `extern` been put in the header, every unit that
includes it would reserve its own copy — `stage.c` and `choice.c` would end up with two separate
`unit_counter`s carrying the same name but standing at different addresses, and this would
produce either a conflict or a silent inconsistency at linking.

This rule does not apply directly to function declarations; a function declaration does not
reserve a place on its own to begin with, it only plays the same role as an `extern` variable
declaration. The essence of the rule is this: every line put in a header must be interpreted the
same way by every unit that reads it. A declaration naturally satisfies this condition, because
it makes the same promise everywhere it is copied; a definition does not, because it demands a
separate place everywhere it is copied.

#### The Include Guard

A header file can enter the same unit through more than one path — directly, or from inside
another header file. This does not have to be as obvious a mistake as including a single file
twice; it often arises on its own when two separate headers both depend on a third, shared
header at the same time. When a unit includes both of them, the shared header ends up entering
that unit twice. The `#ifndef` / `#define` pair at the top of `stage.h` exists exactly to cover
this case. The small example below shows this with a header independent of `stage.h`.

```c
// boundary.h — a small header declaring one type; carries an include guard
#ifndef BOUNDARY_H
#define BOUNDARY_H

typedef struct {
    int row;
    int column;
} Position;

#endif
```

```c
// entry.h — an intermediate header pulling in boundary.h through one path
#include "boundary.h"
```

```c
// exit.h — a second intermediate header pulling in boundary.h through another path
#include "boundary.h"
```

```c
// context.c — boundary.h enters the same unit through two separate paths
#include <stdio.h>
#include "entry.h"
#include "exit.h"

int main(void) {
    Position p = {3, 7};
    printf("position: row %d column %d\n", p.row, p.column);
    printf("header pulled in through two paths, type defined once\n");
    return 0;
}
```

```
position: row 3 column 7
header pulled in through two paths, type defined once
```

**LC7.** `context.c` never includes `boundary.h` directly — it pulls it in indirectly, through
two separate paths, via `entry.h` and `exit.h`. The preprocessor processes both `#include`s in
order: in the first, since `BOUNDARY_H` is not yet defined, the `Position` type is copied into
the source and `BOUNDARY_H` is defined; in the second, `#ifndef BOUNDARY_H` is now false, and
everything up to the `#endif` is skipped — `Position` is not copied again. Without the guard,
`Position` would be defined twice in the same unit, and this would not compile. The struct here
is untagged: C counts every untagged struct declaration as a separate type, while a `typedef`
name can only be rebound to the same type. The second copy tries to bind `Position` to a
second, different anonymous type, and the rule is violated. A definition, unlike a declaration,
must be singular.

This shows that the `#ifndef` family solves a problem tied not to declarations but to
definitions. `identity`'s declaration could be copied into two units, because a declaration can
be duplicated. `Position`'s definition cannot be copied twice into the same unit, because a
definition is singular — just like a function's body. The include guard protects a header's own
definitions under this singularity rule.

**LC8.** The name `BOUNDARY_H` itself has no special meaning; it only serves as a marker
answering the question "has this header already been copied." What the preprocessor sees is not
a file name or a path, only whether this name is defined — from the moment the line
`#define BOUNDARY_H` runs, every subsequent `#ifndef BOUNDARY_H` in the same unit gives a false
result. The guard therefore rests not on the file itself but on this single mark left in the
source; if another header with a colliding name used the same mark, the guard would still work,
but this time it would leave out the wrong header. Making the name unique and specific to its
header is therefore the programmer's responsibility, not a guarantee of the language.

`entry.h` and `exit.h` themselves carry no new declaration; both are only bridges to
`boundary.h`. This mirrors, at small scale, a pattern common in real header trees: a header
includes whatever other headers it needs, and the unit that uses it does not have to be aware of
these internal dependencies. `context.c` reached the `Position` type without even knowing
`boundary.h` existed — both `entry.h` and `exit.h` carried this dependency on their own.

#### The Bounding Measurement

A header file is not a contract. The declaration in `stage.h` matches the definition in
`choice.c` exactly because, in this core, the two were kept consistent by hand — but the
language itself does not check this consistency. If `stage.h` mistakenly declared
`int identity(int);` instead of `int identity(void);`, the definition in `choice.c` would still
be `int identity(void) { return 7; }`. The compiler compiling `stage.c` looks only at the
declaration in `stage.h` and produces the call accordingly; the compiler compiling `choice.c`
looks only at its own definition. Each is consistent within its own unit, each compiles on its
own without a warning — because the compiler cannot see two units at once, a bound known since
this course's first lesson.

A mismatch can only surface once the two units come together, that is, during linking. But a
classic C linker checks not with which signature a name was declared, only whether the name
exists — it looks up the name `identity` in the symbol table, finds it, matches the address.
Whether the signature matches is often something the linker cannot see either; the error is not
caught there either, it falls into undefined behavior. In the core, the situation is not like
this: the declaration in `stage.h` and the definition in `choice.c` match exactly, and both runs
in this lesson have already proven it — both the `identity` call and the `Position` struct gave
the expected values. A header file does not provide this consistency, it only carries it if it
has already been provided by hand.

This bound is another appearance of the previous lesson's finding, "the stages do not reverse."
The preprocessor, while copying a header, does not ask whether the text is correct, it only
copies. The compiler says nothing as long as it finds the copied text consistent within its own
unit. The inconsistency becomes visible only once the two units are linked — and even then,
only in some cases. This is why a header file's correctness is not something the language
checks, it is something its writer maintains through their own discipline: when the declaration
changes, the definition must change too, even if the two stand in separate files.

How this discipline is maintained is a separate subject — build systems and dependency tracking
are the subject of later curricula — but this lesson's bound says this: `identity`'s signature
does not stay correct automatically just because `stage.h` exists, it can only be changed from a
single place. What a header file actually earns is not a guarantee of correctness, but this
singularity — that when a declaration changes, every unit that uses it sees the same text.

#### Summary

- A name appears in the source in two separate forms: a declaration only says it exists, a
  definition gives its body or its place.
- A declaration can be copied into multiple units; a definition can exist in only one unit.
- A header file is not a unit; it only carries text, and it is copied separately into every unit
  that includes it.
- The include guard (`#ifndef` / `#define`) prevents the same header from entering a unit
  through more than one path and copying a definition twice.
- A header file is not a contract: if the declaration and the definition disagree, the compiler
  cannot see it, because it looks at only one unit at a time; the linker, too, looks only for the
  name, not the signature.
- Putting a declaration rather than a definition in a header is a necessity more than a style: if
  the same definition were copied into every unit that includes it, a conflict would arise when
  these units are linked.

#### Next Step

This lesson opened up the mechanism behind the `header text` row: the preprocessor copies a
file, but the rest of the source has no way of knowing that this copying happened. The
difference between the `#include "stage.h"` line standing in the source and the expanded text
the compiler sees is the shared property of all of the preprocessor's decisions: none of them
leaves a trace in the source, each exists only through its result. The next lesson returns to
the preprocessor's remaining three decisions — macro value, conditional body, and token
pasting — and shows, with a deeper look than the previous lesson, why each of them works
entirely independently of the compiler at the text level.

### The Preprocessor

Source: https://academia.sh/en/courses/c-fundamentals/the-preprocessor

The preprocessor's four different directives — macro value, conditional body, token pasting, inclusion — are in fact four appearances of a single job: text substitution. Because a macro knows no types, it produces the same expression so that it gives the correct result with two different types; but because it can substitute its argument more than once, it can run a call with a side effect a different number of times than a real function would.

The previous lesson opened up the mechanism behind the `header text` row and showed that the
preprocessor copies a file, but the rest of the source stays unaware of this copying. This
lesson carries the same observation to the preprocessor's other three decisions — macro value,
conditional body, token pasting — and reveals that all four are in fact the appearance of a
single job: replacing a text in the source with another text, without the compiler ever
knowing.

These four decisions were seen one by one since the course's first lesson: the `FACTOR` text,
the `#ifdef VERBOSE` choice, the `PASTE(add, _three)` name, the copying of `stage.h`. In that
lesson all four appeared side by side, as part of a single run. This lesson returns to each one
separately and shows their shared ground: all four work on pure text, without the compiler ever
getting involved.

#### All Four Are Text Substitution

All the preprocessor's directives do a single job: replace a text in the source with another
text.

| Directive | What it replaces, with what |
|---|---|
| `#define NAME value` | Every place `NAME` appears, with `value` |
| `#define NAME(x) body` | The call `NAME(actual)`, with `body` with its `x`s replaced by `actual` |
| `a##b` | Two tokens, with a single new token |
| `#ifdef` / `#if` | A block of code, with either itself or empty text |
| `#include "d.h"` | One line, with the entire content of the file |

All five fall into the same class, because all five change the source's text without the
compiler getting involved. The only difference among them is what they replace with what — a
name, a call, two tokens, a block of code, or an entire file.

This shared ground has a consequence: for the compiler, the text the preprocessor produces is
indistinguishable from text hand-written in the source. The compiler does not know, and has no
need to know, whether a line came from a macro, was copied from a header, or was written
directly — whatever text is in front of it, it compiles that. This invisibility is the source of
both the preprocessor's power and its limits: on one hand it can produce source in repetitive
forms a programmer would never write by hand, on the other it does not itself check the
correctness of the text it produces.

The table's fifth row — `#include` — was the entire subject of the previous lesson, but there it
was seen as a sharing tool. Here the same directive is looked at as a member of the substitution
family: the line `#include "stage.h"` puts another text in its own place, just like a macro
call. The only difference is scale — a macro replaces a few tokens, while an `#include` replaces
an entire file. From the preprocessor's point of view there is no difference between the two;
both are applications, at different scales, of the same mechanism: reading a text and replacing
it with another text.

#### A Macro Knows No Types

A macro is not a function even if it takes a parameter; here a parameter declares no type, it is
only a placeholder showing where it will be pasted inside the body. This is why the same macro
can produce a correct result with arguments of different types. A real C function cannot behave
this way: every parameter of a function is declared with a single type, and the argument given
at the call is converted to that type — the conversion is sometimes silent, sometimes lossy. A
macro's parameter is entirely exempt from this conversion, because there is no type to convert
to begin with.

```c
// compute.c — the same macro is called with two different types
#include <stdio.h>

#define SQUARE(x) ((x) * (x))

int main(void) {
    int whole = SQUARE(4);
    double fractional = SQUARE(2.5);

    printf("whole: %d\n", whole);
    printf("fractional: %.2f\n", fractional);

    return 0;
}
```

```
whole: 16
fractional: 6.25
```

**LC9.** `SQUARE(4)` turns at the preprocessor into the text `((4) * (4))`, and `SQUARE(2.5)`
into the text `((2.5) * (2.5))` — both were produced from the same body, only the single token
substituted for `x` changing. While making this substitution the preprocessor never knows that
`4` is an integer and `2.5` is a floating-point number; what it sees is only two separate pieces
of text. The compiler decides whether the result will be `int` or `double` — but this decision
is made after the macro's expansion, exactly as if `((4) * (4))` and `((2.5) * (2.5))` had been
hand-typed into the source.

This also explains why `SQUARE` takes no type parameter: it cannot, because it has no concept of
type to begin with. The same body fits every type, because it was written specific to no type at
all — it was only pasted into text.

A real function cannot do the same job — at least not with a single definition. If
`int square(int x)` is written, it works only with `int` arguments; a call resembling
`SQUARE(2.5)` would silently truncate `2.5` to `int` and give the square of `2`, which is `4`.
The `SQUARE` macro giving a correct result with the same body for both `int` and `double` is
therefore not a convenience but the direct consequence of type not existing at all at the
preprocessor stage. The same flexibility carries a cost: a macro cannot check what type its
argument will be, because there is no type system to check with — even if `SQUARE("text")` is
written, the preprocessor does not reject it, it only produces the text `(("text") * ("text"))`
and hands the problem over to the compiler.

#### Conditional Body: One Step Further

The previous lessons chose the conditional body by looking only at whether a name is defined
(`#ifdef`). `#if` goes one step further than this: it computes the value of an integer constant
expression at the preprocessor. This shows that the preprocessor does not have an entirely
empty grasp — while reading the text it can also carry out some simple computations, but this
computation is a far narrower arithmetic than the compiler's rich type system.

```c
// threshold.c — #if evaluates an integer constant expression at the preprocessor
#include <stdio.h>

#define THRESHOLD 5

#if THRESHOLD > 3
static const char *result = "threshold greater than 3";
#else
static const char *result = "threshold less than or equal to 3";
#endif

int main(void) {
    printf("%s\n", result);
    return 0;
}
```

```
threshold greater than 3
```

**LC10.** Although `#if THRESHOLD > 3` carries the same syntax as the place where the compiler
uses the `>` operator, it does not use the same calculating machine. The preprocessor has its
own arithmetic: every integer is widened to a single broad type, there is no floating-point
number at all, and an undefined name is read as `0`. `THRESHOLD`'s value first expands to the
text `5`, then `5 > 3` is computed by the preprocessor's own arithmetic, and the result is
reduced to true or false. The compiler never sees this computation; what reaches it is only the
branch chosen — here the line
`static const char *result = "threshold greater than 3";`.

This distinction also clarifies the difference between `#ifdef` and `#if`. `#ifdef NAME` looks
at whether `NAME` is defined — it never looks at its value; `#ifdef NAME` is true even if `NAME`
is defined as `0`. `#if NAME`, in the exact opposite way, computes `NAME`'s value with the
preprocessor's own arithmetic; `#if NAME` is false if `NAME` is `0`. Confusing these two
directives in a source can silently select the wrong branch while working with a constant that
is defined but zero — this lesson does not use these two directives in place of one another, it
chooses knowing the difference between them.

Thanks to its own arithmetic, `#if` can do one more thing: combine multiple conditions in a
single line with `&&`, `||`, and comparison operators, or use `#elif` to choose the first true
one among alternatives listed in sequence. `#ifdef` cannot do this — it can only ask whether a
single name is defined, it cannot ask which of two defined names corresponds to a larger number.
This shows once more why the two directives do not substitute for one another: one asks about
existence, the other computes a value.

#### Token Pasting: Different Names from the Same Body

In the previous lesson `##` was seen in a single example: it pasted two fixed tokens (`add` and
`_three`) together and produced a single name. Its real power was not visible there, because
that call happened only once. `##`'s real power is that the same macro body can produce
different names in different calls — a macro body, written only once in the source and called
many times, can turn into a new and independent name each time.

```c
// generate.c — the same macro body turns into two different functions with two different calls
#include <stdio.h>

#define MAKE_FUNCTION(name) int make_##name(int x) { return x + 1; }

MAKE_FUNCTION(a)
MAKE_FUNCTION(b)

int main(void) {
    printf("make_a(5) = %d\n", make_a(5));
    printf("make_b(5) = %d\n", make_b(5));
    return 0;
}
```

```
make_a(5) = 6
make_b(5) = 6
```

**LC11.** The line `MAKE_FUNCTION(a)` turns at the preprocessor into the text
`int make_a(int x) { return x + 1; }`, and the line `MAKE_FUNCTION(b)` into the text
`int make_b(int x) { return x + 1; }`. The compiler never sees a name called `MAKE_FUNCTION`;
what it sees in the source are two separate function definitions, both standing as if
hand-written. The same macro body can produce as many different functions as wanted by changing
its argument — but each produced name goes to the compiler as a single piece of text along with
the rest of the body; no link, no "came from the same macro" information, is carried between
them.

`PASTE(add, _three)` in the course's first lesson did the same job, but with a single call. The
difference here is that the same generating body can be called more than once —
`MAKE_FUNCTION` was used twice and two different functions were born. Had a third line,
`MAKE_FUNCTION(c)`, been added to the source, the compiler would see a third function as well;
the amount of text token pasting produces depends on how many times the macro is called, not on
the length of the source.

#### The Bounding Measurement

This lesson's four sections all showed that a macro knows no types, that a conditional body
decides with its own arithmetic, that token pasting can produce new identifiers — the three of
them together were telling of the preprocessor's power. This final section shows a cost of that
same power. A macro is not a function; its argument can be substituted more than once. In
`SQUARE(x)`'s body, `x` appears twice — this means that if the argument is an expression with a
side effect, it will be run twice. In a real function that does the same work, the argument is
always computed once.

```c
// compare.c — the macro runs its argument twice, the function runs its argument once
#include <stdio.h>

#define SQUARE(x) ((x) * (x))

static int counter = 0;

static int increment(void) {
    counter++;
    return counter;
}

static int square(int x) {
    return x * x;
}

int main(void) {
    counter = 0;
    int macro_result = SQUARE(increment());
    int macro_counter = counter;

    counter = 0;
    int function_result = square(increment());
    int function_counter = counter;

    printf("macro: result %d, counter incremented %d times\n", macro_result, macro_counter);
    printf("function: result %d, counter incremented %d times\n", function_result, function_counter);

    return 0;
}
```

```
macro: result 2, counter incremented 2 times
function: result 1, counter incremented 1 times
```

**LC12.** `SQUARE(increment())` turns at the preprocessor into the text
`((increment()) * (increment()))` — the call `increment()` is now written twice in the source.
While the program runs, both of these calls actually happen: `counter` starts at zero, rises
first to `1`, then to `2`, and the product gives `1 * 2 = 2`. Which call runs first depends on
the compiler, and this lesson makes no claim about that — the only claim made is that both calls
happen and `counter` is incremented twice. In the `square` call, on the other hand,
`increment()` is written only once and runs only once: the argument is computed first, its
result is passed to `square` once, and `x`, however many times it appears in the body, is now a
fixed value.

This difference shows why calling a macro like `SQUARE` with an argument that has a side effect
is risky: the macro's writer knows how many times the argument will be used by looking at the
body, but the caller can call it like an ordinary function without seeing this. `SQUARE`'s name
and manner of use are the same as a function's, but its contract is different — the argument
must be an expression free of side effects.

The parentheses around `(x)` in `SQUARE(x)`'s body have nothing to do with this risk; they solve
a separate problem. A body without parentheses (`x * x`) would expand `SQUARE(1 + 2)` into the
text `1 + 2 * 1 + 2`; because multiplication is processed before addition, this expression gives
`1 + 2 + 2 = 5`, whereas the expected result — the square of `3` — is `9`. Multiplication's
precedence mistakes the pieces of the addition for its own argument. The parenthesized body
prevents this: `((1 + 2) * (1 + 2))` first evaluates each of the two parenthesized groups as its
own whole, then multiplies, and the result correctly comes out as `9`. But these parentheses do
not change how many times the argument is written — `((1 + 2) * (1 + 2))` still carries `1 + 2`
twice. Both problems come from the same root — the argument interacts with what surrounds it
when it is substituted into the text — but their solutions are separate: parentheses preserve
the order of precedence, while keeping the argument free of side effects is the caller's
responsibility.

#### Summary

- All the preprocessor's directives do a single job: replacing a text in the source with another
  text; macro value, conditional body, token pasting, and inclusion are four appearances of
  this.
- A macro knows no types: the same body, with arguments of different types, can produce a
  correct result while the preprocessor's work never changes.
- `#if` computes an integer constant expression with the preprocessor's own arithmetic; this
  arithmetic is not the same as the compiler's type system.
- The same macro body can produce different names in different calls through token pasting; the
  compiler does not know these names come from a common source.
- A macro can substitute its argument more than once; an argument with a side effect can
  therefore run a different number of times than it would with a real function.
- Parentheses in a macro body preserve the order of precedence, but do not change how many times
  the argument is written — two separate solutions to two separate problems.

#### Next Step

Through these four lessons, no code block left behind a single warning — but this was not a
coincidence, it was a consistency provided by hand: every macro was carefully parenthesized,
every conditional branch was kept at a consistent type, every token pasting resulted in a valid
identifier. The preprocessor checks none of this consistency; what checks it is the stage right
after it. The next lesson looks at this checking itself: which stage a warning is born at, what
a compiler warning flag makes visible, and which behavior the standard guarantees and which it
leaves to the implementation. This topic's last lesson returns here to the question this course
asked at its opening: how each of the four stages makes visible the error that falls to it.

### Compiler Warnings and Standards

Source: https://academia.sh/en/courses/c-fundamentals/compiler-warnings-and-standards

A warning can only be born at the compiler stage, because only that stage sees the types and expressions inside a single unit. What the standard guarantees is set side by side with what it leaves to the implementation — sizeof(char) == 1 is a guarantee, other sizes are not. Nor can a program that compiles without a warning be said to be correct; to show this, a computation that prints no warning but produces a wrong result is run.

The previous lesson deepened the preprocessor's four decisions and at its close brought out one
observation: through these four lessons no code block left behind a warning, but this was not a
coincidence, it was a consistency provided by hand. This lesson looks at how that consistency is
checked — at which stage a warning is born, and what the standard guarantees and what it leaves
to the implementation. At its opening, the course named the four stages; this final lesson uses
those four names once more, but this time not for a "decision" but for a "check": only one of
the four stages, staying within the bounds of its own input, can think that the source is
probably wrong. What this lesson measures is where that thought is born and how reliable that
thought is.

#### Where a Warning Is Born

A warning can only be born at the compiler stage. Because the preprocessor knows no types, it
cannot notice that a type is used incorrectly — all it has in hand is text. The linker only
matches names, it never examines the content of an expression. Runtime, in turn, no longer
carries the source itself; all it has in hand is data, there is nothing for it to warn about.
The compiler does exactly the opposite of this — it sees the type of every expression, the use
of every variable, and which value every control path returns, all inside a single unit, and as
a result of this seeing it can think that something is suspicious.

This is an extension of the observation in the Compilation Stages lesson: each of the four
stages looked only at its own input, and where an error would be caught was bound by that input
as well. A warning is exactly a check that stays within the bounds of the compiler's own
input — it flags something it can notice by looking at only a single unit. If an error spreads
across multiple units (like the declaration/definition mismatch in the Header Files lesson), it
is not possible for the compiler to catch it with a warning either — because that error lies
entirely outside the compiler's input.

Being suspicious is not the same thing as being wrong. A warning typically points to one of
these patterns: a constant value assigned directly to a type too narrow to hold it, a variable
defined but never used, a signed number compared directly with an unsigned number, or a function
that never returns a value on some paths although it is expected to. All four carry the same
property: the source is in a compilable state, but the compiler, by seeing the types and flow
inside a single unit, has derived a risk from the pattern it saw.

What these four patterns share is that none of them is an error on its own. An unused variable
does not stop the source from running; comparing a signed number with an unsigned one is a
valid C expression; a function not returning a value on some paths does not make the source
uncompilable. The compiler rejects none of these patterns, it only flags them — because each can
be a sign of something the programmer did not actually want, but does not have to be. This also
explains why a warning must be turned on with an optional flag (like `-Wall`, `-Wextra`): the
standard does not determine which patterns the compiler will count as suspicious, it determines
only which patterns the program can remain compilable with.

Every code example in this course avoided these four patterns, but avoiding them was a choice
made by hand — the language itself kept no example from falling into these patterns. If all
twenty-one code blocks in this topic stayed free of warnings, the reason is not that the
compiler was lenient, but that every variable was used, every comparison was made with types of
the same sign, and every function returned a value on every path.

Maintaining this consistency will remain the same obligation through the rest of the course.
When a warning appears, that warning points to a single unit, a single expression, in the
source — the first step in fixing the source is knowing which unit the warning belongs to. This
confirms once more why a warning is bound so tightly to the compiler class: a warning
disappears only once the unit that produced it is recompiled; no other stage has the power to
silence it.

#### What Is Guaranteed, What Is Not

The C standard (C17) guarantees some things with certainty and leaves others to the
implementation. The difference between the two can be shown by running it — but only the
guaranteed side, because a number that is not guaranteed is never printed anywhere in this
course.

This distinction is not arbitrary, it is a direct consequence of the language's closeness to
hardware. Four separate pieces of hardware can offer four separate integer widths; instead of
imposing a fixed width so that the language can run on every piece of hardware, the standard
imposes a lower bound and leaves the rest to the compiler, and therefore indirectly to the
hardware. How many bytes `int` is on this machine is therefore not a promise of the language but
a decision of the compiler — and it is an observation never printed anywhere through this
course.

```c
// guarantee.c — what the standard guarantees and what it leaves, side by side
#include <limits.h>
#include <stdio.h>

int main(void) {
    printf("sizeof(char) == 1: %s\n", (sizeof(char) == 1) ? "true" : "false");
    printf("CHAR_BIT >= 8: %s\n", (CHAR_BIT >= 8) ? "true" : "false");
    printf("INT_MAX >= 32767: %s\n", (INT_MAX >= 32767) ? "true" : "false");
    printf("LONG_MAX >= 2147483647: %s\n", (LONG_MAX >= 2147483647L) ? "true" : "false");
    return 0;
}
```

```
sizeof(char) == 1: true
CHAR_BIT >= 8: true
INT_MAX >= 32767: true
LONG_MAX >= 2147483647: true
```

**LC13.** All four lines print the result of a comparison, none prints a raw number.
`sizeof(char)`, `CHAR_BIT`, `INT_MAX`, `LONG_MAX`'s own values can vary by implementation — the
standard guarantees only a lower bound, it leaves the upper bound free. How many bytes `int`
actually is, how many bits `long` actually carries, was never printed anywhere in this course
and never will be; what is printed is only the true/false answer to the question "is this value
at least this much," because this answer is the same in every C17 compiler that conforms to the
standard.

All four of these lines came out true, because all four are genuinely guaranteed lower bounds.
What the standard does not guarantee never appears in the lines at all: `sizeof(int)`'s exact
value, whether `int` is actually wider than `short`, whether `char` is counted as signed or
unsigned. All of these are left to the implementation, and this is why none of them was run in
any lesson of this course.

This difference between the lower and upper bound resembles the "guarantee versus excerpt"
distinction the course saw earlier: a header file could share a declaration, but did not
guarantee that the definition would match it (the Header Files lesson); here too, the standard
gives a lower bound, but does not guarantee where within that bound the actual value will
stand. The same pattern repeats in both: the promise the language gives is always a
**minimum**, and what is added on top of it is left to the implementation, that is, to the
compiler and the hardware.

None of these four lines produced a warning either, and this is not a coincidence. A comparison
like `CHAR_BIT >= 8` is a comparison of two constant expressions, and the compiler sees no
pattern in it to find suspicious — both sides of the comparison belong to the same broad
integer class, there is no mixing between signed and unsigned, no value is narrowed. Measuring
the side the standard guarantees is therefore both correct and safe: it neither leaks an
environment-dependent number, nor falls into a pattern the compiler is suspicious of.

This distinction has a practical consequence: a source built only on the side the standard
guarantees gives the same result regardless of which compiler and which hardware it is compiled
with. `sizeof(char) == 1` is true everywhere, because the standard already makes this
mandatory. But had a source rested on an assumption like `sizeof(int) == 4`, that assumption
could turn out true on some implementations and false on others — and since the compiler cannot
see this assumption in the source, it would not print a warning where it turns out wrong
either. A source that rests only on the guaranteed side keeps its correctness regardless of
which compiler and which hardware it is compiled with; a source that rests on a side that is not
guaranteed can, even if the compiler prints no warning at all, silently give a wrong result on
different hardware.

#### The Bounding Measurement

A warning is not an error; the compiler continues compiling the program even after it prints a
warning, and the program runs. The mirror image of this is also true: a program that compiles
without a warning **cannot be said** to be correct. These two sentences together define not
what a warning is, but what it is **not**: a warning is neither proof of an error, nor is its
absence proof of correctness — it is only a piece of information about what the compiler
noticed, nothing more. The program below compiles without leaving a single warning, but its
second line gives an unexpected result.

```c
// ratio.c — a program that compiles without a warning does not guarantee it is correct
#include <stdio.h>

int main(void) {
    int part = 2;
    int total = 5;

    int integer_ratio = part / total * 100;
    double float_ratio = (double)part / total * 100;

    printf("ratio computed with integers: %d\n", integer_ratio);
    printf("ratio computed with floating point: %.1f\n", float_ratio);

    return 0;
}
```

```
ratio computed with integers: 0
ratio computed with floating point: 40.0
```

**LC14.** Both lines start from the same two numbers (`2` and `5`), and neither got a single
warning from the compiler. But `integer_ratio` gave `0`, because `part / total` was done
between two `int`s, and `2 / 5` rounded toward zero gives `0` — the multiplication by `100`
came after that, and multiplying a number that was already zero by a hundred still gives zero.
`float_ratio`, on the other hand, converted `part` to `double` first, so the division was done
in floating-point arithmetic and gave the correct ratio, `40.0`. The compiler finds neither of
these "wrong," because both are consistent with their own type: the result of an `int / int`
operation being `int` is a rule of C, not an error — this was already seen in this course's
second lesson, as the difference between `7 / 2` and `7 / 2.0`.

The way to fix `integer_ratio` in the source, just as with `float_ratio`, is to convert one of
the two sides to `double` before dividing — the operation the program runs stays the same, only
which arithmetic will be used is stated explicitly in the source. The compiler cannot suggest
this fix, because suggesting it would need the information "the programmer wanted a ratio
here," and this information is written nowhere. All the compiler can see is that both
expressions are consistent with their own type.

This separates a guarantee the language gives from a decision the programmer makes. The
language guarantees that `int / int` will give `int`, and applies this consistently — the
compiler violates nothing here. What is wrong is assuming that the division of two integers
will carry a **ratio**; this assumption is written nowhere in the source, it existed only in the
programmer's mind. The compiler cannot see what is in a mind, it sees only what is written in
the source — and what was written in the source was a completely consistent, completely
warning-free `int` division.

This is the final link of a pattern built up over four lessons: each stage looks only at its own
input and accepts everything that is consistent within that input. The preprocessor found the
text consistent, the compiler found the types consistent — but consistency is not the same thing
as correctness. The variable `integer_ratio` is flawless from the compiler's point of view; from
a programmer's point of view it is probably a mistake. The gap between these two is a gap no
stage can fill — only the one who wrote the source can fill it.

This is a conclusion that also holds true for the last lesson of a framework built up over five
lessons: none of the four stages proves that the source is **correct** — each checks only a
consistency specific to its own stage. The preprocessor checks the form of the text, the
compiler checks types and flow, the linker checks names, and runtime works only with the data it
is given. What is obtained once all four are passed is a running program — but no stage says
that a running program is the same as the **wanted** program.

#### Summary

- A warning can only be born at the compiler stage; only this stage sees the types and
  expressions inside a single unit and can find a pattern suspicious.
- A warning is not the same thing as an error: the program compiles and runs with the warning
  too.
- The standard guarantees some things with certainty (`sizeof(char) == 1`, `CHAR_BIT >= 8`), and
  leaves others to the implementation (actual type sizes); this course printed only what is
  guaranteed.
- A program that compiles without a warning does not show that it is correct: `int / int`
  division is a rule of the language, but taking its result to be a ratio is an assumption the
  programmer never wrote in the source.
- The promise the standard gives is always a minimum; what is added on top is left to the
  compiler and the hardware, which is why only the minimum, guaranteed side was run through this
  course.
- None of the four stages proves that the source is correct; each checks only a consistency
  specific to its own stage, and catching the difference between a running program and the
  wanted program is the job of the one who wrote the source, not the language.

#### Next Step

This is the last lesson of the Language and Compilation topic. Five lessons passed through the
same core, the same four classes, and each one lit up a separate corner. The Place of C ran the
core in full and named the four stages with the fourteen observations' 4/4/3/3 distribution.
Compilation Stages ran the same four classes side by side with a small excerpt, showed which
input each looks at, and proved that the stages run in an order with no way back. Header Files
separated declaration from definition and showed how a header carries the same declaration into
multiple units, and what the include guard prevents. The Preprocessor revealed that four
separate directives are in fact the appearance of a single job — text substitution — and proved
that a macro can substitute its argument more than once. This final lesson looked at the fifth
corner: that a warning can only be born at the compiler, and that compiling without a warning is
not a guarantee of correctness.

The five lessons' shared ground was that none of them measured again what another had already
measured — each used the same four names, but each asked its own question. This shared ground
will continue through the rest of the course: the eight lessons of the Basic Syntax topic will
also use the same four classes, but each will look at a different corner of C — types,
variables, operators, control flow, functions, arrays, record types, input and output.

The Basic Syntax topic does not change this framework, it adds C's own types, variables, and
operators to it; the first question is which stage settles the values a type carries.

## Basic Syntax

A type's carried value ranges and conversion rules, a variable's scope and storage duration being separate questions, operators changing meaning by type, control flow forms, a function's declaration being separated from its definition, an array and a string resting on the same mechanism, a record type's memory layout, and input and output running through format strings.

### Data Types

Source: https://academia.sh/en/courses/c-fundamentals/data-types

A type's value range, the signed–unsigned distinction and integer promotion being decided at the compiler stage; no size is printed except sizeof(char) == 1.

The Compiler Warnings and Standards lesson measured at which stage a warning arises and
what the standard does and does not guarantee: `sizeof(char) == 1` was a guarantee, other
sizes were not. This lesson builds on that distinction and measures a more fundamental
question: at which stage does a type determine the boundary of the values it can carry?

The answer is the compiler. When `int` is written in a source file, the range that name
can carry is fixed neither in the source itself nor at runtime; it is decided the moment
the unit is translated, together with the representation chosen for the target. **This
lesson is not confused with the question measured in M08/K05, "which class path a type
must be found on," nor with the question measured in M09/K05, "the source of a piece of
information"** — both were axes outside this course; what is measured here is at which
stage the value a type carries is decided, and where the boundaries of that decision come
from. The Programming Fundamentals course (M01/K02) already established the concepts of
variable and type with Python; there, the values a name could hold could change during
runtime, and a type's boundary was never asked separately. C separates these two
questions: the type itself carries a boundary, and that boundary is fixed at compile time
together with the type name written in the source. Once a variable is declared, the type
it carries never changes for the rest of the program; the value may change, but the type
that determines which range and which rule the value is interpreted by stays fixed. This
lesson measures what that fixedness means through four separate decisions — range,
promotion, sign and explicit conversion.

The size of this boundary varies by target, and this variability is deliberate: the
language lets the same type name be represented with different widths on different
hardware, letting an efficient mapping be built for each target. But this flexibility
should not make a lesson's measurements dependent on that target — if a measurement is
true on the machine it runs on here, it must stay true on another target five years from
now. For this reason, no real size value is printed in this lesson; instead, the relative
relationship between two types is measured. A relationship itself can also vary from
target to target, but for a given target the compiler's decision is fixed, and the same
binary reports the same relationship on every run.

#### Size Unit and Comparative Result

The size of the range a type carries depends on the target and is never printed anywhere
in this course — the exception is the one line the standard explicitly guarantees:
`sizeof(char)` is always `1`, because the "byte" unit of measure is already defined
through `char`. The measurement below reads this line and the relative size of two types
as a logical outcome — larger or smaller, `1`/`0`; no real size value is printed.

```c
// stage.c — the type carrying a value being decided at the compiler stage
#include <stdio.h>
#include <limits.h>

enum Color { RED = 1, GREEN = 2, BLUE = 4 };

static void observe(const char *name, int value) {
    printf("%-40s%d\n", name, value);
}

int main(void) {
    observe("type size unit", (int)sizeof(char));
    observe("enumeration constant", (int)BLUE);
    observe("is int wider than char", (int)(sizeof(int) > sizeof(char)));
    observe("is long wider than int", (int)(sizeof(long) > sizeof(int)));
    observe("is short narrower than int", (int)(sizeof(short) < sizeof(int)));

    char small_a = 100, small_b = 100;
    int promoted_sum = small_a + small_b;
    observe("promoted sum", promoted_sum);

    unsigned char wrapped = UCHAR_MAX;
    wrapped = (unsigned char)(wrapped + 1);
    observe("unsigned wraparound result", wrapped);

    int negative_one = -1;
    unsigned int threshold = 1;
    observe("is it less after converting to unsigned", (int)((unsigned int)negative_one < threshold));

    return 0;
}
```

```
type size unit                          1
enumeration constant                    4
is int wider than char                  1
is long wider than int                  1
is short narrower than int              1
promoted sum                            200
unsigned wraparound result              0
is it less after converting to unsigned 0
```

The type size unit line confirms the standard's guarantee: `char` is always a single
byte, and this will remain the only size value this course measures. The remaining three
comparison lines do not give a real number at all, only the result of a relationship —
they ask whether `int` is wider than `char` on the target, whether `long` is wider than
`int`, whether `short` is narrower than `int`, and all three come back with the answer `1`
(true). **BS1.** These comparisons themselves are also decided at the compiler stage: each
relationship can vary from target to target, but it is fixed while a given unit is being
translated and does not change during a run — the same binary produces the same `1` or
`0` value on every run.

The standard also already guarantees the relative order of these types: the width
between `char`, `short`, `int`, `long` and `long long` can never run backward — each one
is wide enough to carry every value the one before it could carry. This is not a
measurement but the language's own specification; this lesson only measures whether this
order turns out **strict** or **equal** on a particular target (here all three came out
strict). On some targets `int` and `long` may be the same width — the standard only
guarantees that `long` is **not narrower** than `int`, not that it is definitely
**wider**. The value `1` measured in this lesson is therefore an observation specific to
this target, not a promise the language itself makes.

#### Integer Promotion

The enumeration constant line is a slice of the `enum Color` constant introduced in the
Compilation Stages lesson: the value of `BLUE`, `4`, is produced at compile time from the
constant expression written in the source. In the same measurement, the promoted sum line
shows a different decision: `small_a` and `small_b` are each a `char`, but their sum is
computed not within `char`'s bound but within `int`'s. The C standard promotes narrow
types like `char` and `short` to `int` before they enter an operation — this is called
integer promotion. If `100 + 100` were carried out on a `char` (typically in the range
`-128`–`127`), the result would not fit; but because the operation runs at `int` width,
`200` comes out without any trouble. This promotion is invisible in the source — no
conversion was written — but the compiler moves every `char` operand to `int` before the
operation, and this decision comes entirely from the types, not from the data.

**BS2.** The promotion rule applies regardless of what the operand values actually are:
even if `small_a` and `small_b` were `10` and `20`, the sum would still be computed at
`int` width, even though the result would stay within `char`'s bound. Before running the
operation, the compiler decides which width it will run at by looking only at the
operands' **types**; this is the first instance of the principle that operators behave
according to type, not data, and it will be developed further in the Operators lesson.

Promotion is not limited to arithmetic operators; when a function takes a variable number
of arguments (`printf` is the standard library's example of this), `char` and `short`
arguments are also promoted to `int` before the call. This is called the default argument
promotion, and it works only for arguments falling into the `...` part — after the
prototype's last declared parameter. `observe` is different: it is a function with a
prototype and a fixed signature, so even though the `wrapped` variable is `unsigned
char`, its value is carried to `int` by an ordinary assignment conversion because the
second parameter is declared `int`; the `%d` in `observe`'s body matches this declared
type as well. The two mechanisms are separate: one works only at the tail of
variable-argument calls, the other works on every prototyped call. Printing a narrow type
with a directly mismatched format specifier (using `%d` instead of `%hhu` for `unsigned
char`, for instance) works on `printf`'s side in most cases thanks to default promotion,
but this is a fragile match, and the subject of format strings will be taken up
separately in the Input and Output lesson.

#### Narrowing with an Explicit Conversion

Promotion is an implicit rule — it works without leaving any trace in the source. But a
source writer may also want the exact opposite: to deliberately fit a wide value into a
narrow type. This is done with an explicit conversion (cast), and the decision itself is
again made at compile time, at the point where the type's name is written.

```c
// narrowing.c — an explicit conversion makes the narrowing visible in the source
#include <stdio.h>

static void observe(const char *name, int value) {
    printf("%-34s%d\n", name, value);
}

int main(void) {
    int wide_value = 300;
    unsigned char narrowed = (unsigned char)wide_value;
    observe("explicitly narrowed value", narrowed);

    int second_value = 600;
    unsigned char second_narrowed = (unsigned char)second_value;
    observe("second explicitly narrowed value", second_narrowed);
    return 0;
}
```

```
explicitly narrowed value         44
second explicitly narrowed value  88
```

The value `300` does not fit into an eight-bit unsigned type, but the conversion is still
defined: the same wraparound rule applies here too, giving `300 mod 256 = 44`. The same
rule applies for `600`, giving `88`. This is not a different event from the overflow in
the previous section, but the same rule triggered explicitly and deliberately in the
source. Implicit promotion was invisible in the source; an explicit conversion is visible
through the `(unsigned char)` notation — a reader can tell from the source that a
narrowing is intentional. In both cases, which rule applies is determined at compile time
by looking at the target type's sign, not the operand's value.

#### The Signed–Unsigned Distinction and Defined Wraparound

Whether a type is signed or unsigned determines where the range it carries starts, and
this too is a property decided at compile time. The unsigned wraparound result line
measures a variable of type `unsigned char` being incremented once after it reaches its
upper bound (`UCHAR_MAX`): the result is `0`. This is not an error — the standard makes
overflow **defined** for unsigned types: the result "wraps" by taking the modulus against
the number of values the type can represent. `255 + 1`, on an eight-bit unsigned type,
gives `256 mod 256 = 0`, and this produces the same result on every run.

The is it less after converting to unsigned line shows another face of the
signed–unsigned distinction: a signed integer with the value `-1` is converted to
unsigned before being compared with an unsigned type. This conversion is also defined —
when a negative value is converted to unsigned, the number of values the type can
represent is added, moving it to a large unsigned number. As a result, `-1` appears not
smaller than the `1` in the `threshold` variable, but far larger, and the comparison
returns `0` (false). The signs written in the source are reconciled this way by the
compiler during comparison; which side gets converted to which type is determined by
looking at the two operands' declared types, not the data.

This reconciliation rule also explains why unsigned types are so often preferred for
carrying counters and sizes: the standard library's `sizeof` operator returns `size_t`,
and this type is unsigned because a negative size is conceptually meaningless. But the
same choice also carries a trap. Writing a loop bound backward with an unsigned counter —
trying to bring a counter down below `0` — never drops the counter into the negatives,
because it is unsigned; it wraps from the upper bound instead, and the loop condition
stays true forever. This situation is not undefined; the wraparound rule works exactly as
seen in the measurement above. But because it would lead to an infinite loop, this lesson
does not run this scenario, and only notes how unsigned wraparound can turn into an
**unexpected** result at a boundary condition — the behavior itself is defined,
interpreting the result is the source writer's job.

#### The Bounding Measurement

Overflow behavior depends on a type's sign, and this dependency is not symmetric. For
unsigned types, overflow is defined as measured above, and wraparound always follows the
same rule. For a signed type, overflow is **undefined behavior**: incrementing an `int`
at the value `INT_MAX` drops into a state the standard gives no guarantee about — because
the compiler does not assume this situation can occur, optimizations may produce
unexpected results, the program may crash, or there may be no visible effect at all.
Under this course's general rule, this behavior is not tried in a code block.

The defined counterpart set alongside it is the `unsigned char` measurement above: the
same "crossing one past the upper bound" scenario, built with an unsigned type, gives a
fully predictable result. Both scenarios ask the same question — what happens when a
type's boundary is crossed — but the answer depends solely on the sign itself: an
arithmetic rule for unsigned, a gap for signed. Choosing a type as signed or unsigned is
therefore a decision that determines not only which values can be represented, but also
which category the program falls into when the boundary is crossed, and this decision is
made entirely at compile time, together with the type name written in the source.

The source of this asymmetry lies in how the two representations correspond in hardware.
An unsigned type reads its bits directly as a number's binary representation; wraparound
is a natural consequence of that representation's limited width, and it works the same
way on every target. In a signed type, the representation of negative values can be
target-dependent, and guaranteeing which result would arise on overflow for every
representation would have restricted certain optimizations the compiler could make for
values near the upper bound. For this reason, the language chose to leave signed overflow
undefined rather than defined — instead of fixing the behavior, it granted the compiler
the freedom to work under the assumption that "this will never happen."

The practical consequence of this asymmetry is that choosing between a signed and an
unsigned type requires asking not only "which values are needed" but also "what should
happen if the boundary is crossed." If a counter will never be negative, an unsigned type
both wins an extra bit of range at the upper bound and reduces overflow to a defined
wraparound. If a computation can temporarily drop negative in intermediate steps, a
signed type carries that transition naturally, but sits closer to undefined behavior in a
sum approaching the upper bound. The compiler does not make this choice in place of the
source writer; it only reads the type name written and fixes the boundary that name
carries and the rule for crossing it.

#### Summary

- A type's value range is decided at the **compiler** stage; in this course no size value
  is printed except `sizeof(char) == 1`, only comparison results are measured.
- Integer promotion carries narrow types like `char` and `short` to `int` before an
  operation; this decision is made by looking at the operand **types**, not the data
  being carried.
- Overflow in unsigned types is **defined** and wraps according to the type's number of
  values; overflow in signed types is **undefined** and is never run in any code block in
  this course.
- Converting a signed value to unsigned is also defined: a negative value is moved to a
  large unsigned number by adding the type's number of values.
- Enumeration constants (like `BLUE` inside `enum Color`) are produced at compile time
  from constant expressions in the source; this subject will be developed further in the
  Structs, Unions and Enumerations lesson.
- An explicit conversion (like `(unsigned char)`), unlike implicit promotion, is visible
  in the source and is subject to the same wraparound rule; which rule applies is again
  determined at compile time by looking at the target type's sign.

#### Next Step

Every decision measured in this lesson stayed within the boundary of a single unit: a
type's range, an operation's width, a conversion's direction — all decisions the compiler
alone could make by looking at the same file. The types measured in this lesson were
already bound to existing names; the next question concerns where that name itself comes
from and how long it lives. Which declaration a name is bound to and how long a value
lives are not always decided at the same stage. The Variables, Scope and Storage Duration
lesson separates these two: where a name is visible from is fixed at compile time, while
how long a value lives can only be observed at runtime — and the difference between the
two will also raise a third stage, one in which the same name can fall into two separate
objects in two separate units.

### Variables, Scope and Storage Duration

Source: https://academia.sh/en/courses/c-fundamentals/variables-scope-and-lifetime

Where a name is visible from being decided at compile time, and how long a value lives being decided at runtime; whether the same name falls into two separate objects in two units being resolved by the linker.

The Data Types lesson measured how the type a name already carries is decided. This
lesson takes a step back: from where is a name itself visible, and how long does the
value that name points to live? These two questions look alike, but they are not
answered at the same stage — one belongs to the source's structure, the other to the
flow of the run. A third question will be added to these: when a program is built from
several units, whether the same name falls into the same object or into two separate
objects across two units is also resolved at a separate stage, by a separate rule.

The Programming Fundamentals course (M01/K02) already established the concepts of scope
and lifetime with Python; there, a name disappeared once a function call ended, while a
module-level name lived until the program's end. C's counterpart is measured here: scope
and storage duration in C are separated not only conceptually but also in terms of the
stage at which they are decided — one is fixed at compile time, the other can only be
observed during a run.

#### Scope: Where a Name Is Visible From

Which declaration a name is bound to is determined at compile time by looking at the
source's structure. When the same name is redeclared in nested blocks, the innermost
declaration **shadows** the outer one — the outer name becomes temporarily inaccessible,
but it does not disappear.

```c
// scope.c — which declaration a name is bound to is settled at compile time
#include <stdio.h>

int main(void) {
    int value = 10;
    printf("%s %d\n", "outer value", value);
    {
        int value = 20;
        printf("%s %d\n", "inner value", value);
    }
    printf("%s %d\n", "value after the block", value);
    return 0;
}
```

```
outer value 10
inner value 20
value after the block 10
```

**BS3.** The `value` declaration inside the block only shadows the outer `value` within
the boundaries of its own block; once the block closes, the outer name becomes visible
again and its value has never changed — because the two `value` variables are actually
two separate objects, two different storage locations carrying the same name. Which
`value` is accessed is determined not by which line the program is on, but by which
block that line is written inside in the source; this information is written in full in
the source itself, and the compiler resolves it before a run ever starts. The same rule
applies to function parameters as well: a function's parameter name is a name declared in
the outermost block of the function body and is subject to the same shadowing rule.

The scope rule is not limited to blocks opened with `{ }`; a counter declared in a `for`
loop's header also carries its own scope, and once the loop ends that name is no longer
visible.

```c
// loop_scope.c — the declaration in a for header belongs to the loop's own scope
#include <stdio.h>

int main(void) {
    int i = 100;
    for (int i = 0; i < 3; i++) {
        printf("%s %d\n", "i inside the loop", i);
    }
    printf("%s %d\n", "i outside the loop", i);
    return 0;
}
```

```
i inside the loop 0
i inside the loop 1
i inside the loop 2
i outside the loop 100
```

The `i` in the `for` header shadows the outer-scope `i` only for the duration of the loop
body; once the loop ends, the outer `i` — untouched, at `100` — becomes visible again.
This is a special form of block shadowing: the loop header opens a scope of its own, and
this scope is determined solely from how the source is written, without looking at any
data.

#### Storage Duration: How Long a Value Lives

Unlike scope, how long a value lives is observed not by reading the source but by
running the program. The measurement below calls the same function three times and
counts how two different declaration forms behave across calls.

```c
// duration.c — how long a value lives can only be told by running the program
#include <stdio.h>

int automatic_counter(void) {
    int counter = 0;
    counter++;
    return counter;
}

int persistent_counter(void) {
    static int counter = 0;
    counter++;
    return counter;
}

int main(void) {
    for (int i = 0; i < 3; i++) {
        printf("%s %d\n", "call", i + 1);
        printf("  %s %d\n", "automatic counter", automatic_counter());
        printf("  %s %d\n", "persistent counter", persistent_counter());
    }
    return 0;
}
```

```
call 1
  automatic counter 1
  persistent counter 1
call 2
  automatic counter 1
  persistent counter 2
call 3
  automatic counter 1
  persistent counter 3
```

The `counter` inside `automatic_counter` is reborn on every call and dies when the
function returns — this is C's counterpart of the **storage duration** concept, and it is
not confused with M10/K02's **lifetime** term: what was measured there was when a value
becomes invalid in an ownership chain; what is measured here is how long a variable stays
allocated in memory. The `static` specifier changes this duration: the `counter` inside
`persistent_counter` is initialized once at the start of the program and does not
disappear each time the function returns — the next call finds the value the previous
call left behind. Across three calls, the automatic counter always stays at `1` while the
persistent counter advances `1`, `2`, `3`; this difference does not show up in the
source, it shows up in **how many times** the function was called — it cannot be told in
advance by reading the source, only a run reveals it.

**BS4.** The `static` specifier does not affect two different things at once here:
`counter`'s scope is still limited to the function body — it cannot be accessed directly
from outside — but its storage duration is now tied not to the function call but to the
whole program. That scope and storage duration are separate questions shows up exactly
here: in the same declaration, one can change while the other stays fixed.

A variable declared at a file's outermost level (outside any function body) already
lives for the whole program regardless — a file-scope name like `unit_counter` is
permanent in storage duration from the very start, whether or not `static` is written.
What the `static` specifier adds at file level is not duration but a **narrowing** of
scope: as will be measured below in the `scope_counter` example, it is closed off outside
its own unit. In a local variable, on the other hand, `static`'s effect works in the
opposite direction — it leaves the scope unchanged while extending its duration. The same
keyword affects two different decisions depending on where the declaration sits; this is
a sign that `static` does not have a single meaning, and can change either scope or
storage duration depending on its context.

#### The Independence of Scope and Storage Duration

Because scope and storage duration are separate questions, a single declaration can
carry different combinations of the two. A local variable (block scope, automatic
duration) and a local `static` variable (block scope, persistent duration) were measured
above — both are confined to the same block, but one is reborn on every call while the
other persists. A variable declared at file level shows the third combination:
`unit_counter` is file-scoped — visible from everywhere in the unit it is declared in —
and its duration is permanent from the start, whether or not `static` is written. The one
combination that does not appear in the source is file scope paired with automatic
duration: a name declared at file level never comes to life and dies bound to a function
call, because a file-scope declaration is never inside a call frame to begin with. These
three combinations show that scope and storage duration really are two independent axes:
one answers "where is it visible from," the other "how long does it live," and a
declaration's position on these two axes can be chosen separately.

#### The Linker: A Variable in Another Unit

Scope and storage duration were questions that stayed within a single unit. A third
question arises once several units come into play: if a name is defined in another unit,
when is access to it resolved? Answering this question requires separating declaration
from definition. A declaration only tells the compiler a name's type and that it exists —
it allocates no memory. A definition, on the other hand, actually creates a storage
location for that name. `extern int unit_counter;` is a declaration; `int unit_counter =
2;` is a definition. A name can be declared as many times as wanted across different
units — each unit keeps its own copy — but for a name with external linkage, exactly
**one** definition must exist; otherwise the linker runs into the contradiction of
allocating more than one storage location for the same name. The excerpt below is part of
the core introduced in the Compilation Stages lesson.

```c
// stage.h — declarations of names defined in other units (excerpt: variable declaration)
#ifndef STAGE_H
#define STAGE_H

/* This variable is defined in another unit: its name is declared here, its value is resolved at link time. */
extern int unit_counter;

#endif
```

```c
// choice.c — the second unit: the variable's definition lives here
#include "stage.h"

int unit_counter = 2;
```

```c
// stage.c — the linking of a variable in another unit (excerpt)
#include <stdio.h>
#include "stage.h"

int main(void) {
    printf("%s %d\n", "variable in another unit", unit_counter);
    return 0;
}
```

```
variable in another unit 2
```

`stage.c` knows the name `unit_counter` only from its declaration (`extern int
unit_counter;`); this declaration guarantees the name's type and existence, but not its
value. The value is defined in `choice.c`, and while `stage.c` is compiled it is not yet
bound to anything — the two units are translated separately. The step that finds the
name's real location and binds the reference to it is the **linker**: when the two
units' object files are brought together, the gap `stage.c` leaves is filled by the
definition `choice.c` supplies. This is a third decision separate from scope and storage
duration: scope determines a name's **visibility**, storage duration a value's **span**,
and binding determines which **definition** a name corresponds to.

This separation between declaration and definition also explains why header files carry
only declarations: `stage.h` can be included in more than one unit (both can write
`#include "stage.h"`), and each inclusion adds only a declaration, not a definition. If
the header file carried a definition like `int unit_counter = 2;`, every unit including
it would produce its own definition, and the linker would end up with more than one
storage location for the same name. Separating declaration from definition is therefore
not merely a syntactic detail but a precondition for a multi-unit program being
compilable at all.

#### The Bounding Measurement

A name being written the same way in two units does not mean that name corresponds to a
single object. An `extern` variable like `unit_counter` was deliberately designed to be
shared; but a `static` variable guarantees the exact opposite — it closes itself into its
own unit, and no other unit can access it directly. This means the same name can fall
into two separate objects in two separate units, and it is the linker that decides this.

```c
// unit_a.c — a scope counter defined in this unit, unreachable from outside
static int scope_counter = 11;

int read_from_a(void) {
    return scope_counter;
}
```

```c
// unit_b.c — carries the same name, but a separate object
static int scope_counter = 22;

int read_from_b(void) {
    return scope_counter;
}
```

```c
// bounding.c — the same name is two separate objects in two units, the linker decides
#include <stdio.h>

int read_from_a(void);
int read_from_b(void);

int main(void) {
    printf("%s %d\n", "read from unit a", read_from_a());
    printf("%s %d\n", "read from unit b", read_from_b());
    return 0;
}
```

```
read from unit a 11
read from unit b 22
```

`unit_a.c` and `unit_b.c` both define a variable named `scope_counter`, but the `static`
specifier closes both into their own unit. The linker never compares these two
definitions and reports no conflict — because neither ever leaves its own unit, neither
appears in the symbol table. `read_from_a` can see only its own unit's `scope_counter`,
`read_from_b` only its own, and the results confirm this: `11` and `22`, entirely
independent of each other. This is the exact opposite of an `extern` variable's behavior:
there, a single definition existed and the linker bound every call to that one
definition; here, two separate definitions exist and the linker leaves each in its own
unit without ever comparing them. Depending on the `static` specifier, the same name can
lead to two completely different outcomes from a linker decision.

The practical consequence of this third stage is that `static` can be used at file level
as a hiding tool. If a unit wants to keep a counter or a helper variable for its own
internal working and does not want any other unit to see it, it declares that variable
`static`; even if the name collides with the rest of the units — as in the
`scope_counter` example — the linker reports no conflict, because both stay within the
boundaries of their own unit. An `extern` variable, by contrast, sets up the exact
opposite contract: a name deliberately shared between units, with a single definition.
The source writer decides which name is to be shared and which is to stay in its own unit
through the presence or absence of the `static` keyword — and this choice has already
predetermined the rule the linker will follow when the units are linked.

#### Summary

- A name's **scope** — where it is visible from — is determined at **compile time** by
  looking at the source's block structure; in nested blocks, the same name shadows the
  outer one.
- A value's **storage duration** — how long it lives — can only be observed at
  **runtime**; an automatic local variable is reborn on every call, a `static` local
  variable persists across calls.
- Storage duration here is C's counterpart of storage duration; it is kept separate from
  M10/K02's **lifetime** term — one measures the span of allocation, the other the
  validity of ownership.
- An `extern` variable is bound to a single definition and shared across units; this
  binding is resolved at the **linker** stage, when the units come together.
- When the same name is defined `static` in two units, the linker leaves them as two
  separate objects without ever comparing them — because scope, storage duration and
  binding are three separate questions, all three can be decided at once, yet
  independently of each other.
- Scope and storage duration are independent axes: a block-scoped name can have automatic
  or persistent duration, while a file-scoped name always has persistent duration — the
  combination of file scope with automatic duration never appears in the source.

#### Next Step

None of the decisions measured in this lesson touched on what determines an expression's
result — only where a name is visible from, how long a value lives, and which unit a
definition sits in were measured. The Operators lesson adds a new question to this
ground: the same operator can produce a different result depending on the type of the
values it operates on, and this choice, too, is made not by looking at data but at the
types known at compile time. Even though `7 / 2` and `7 / 2.0` are written with the same
operator, which division runs comes from the operands' types — just as, in this lesson,
which declaration a name is bound to comes from the source's structure.

### Operators

Source: https://academia.sh/en/courses/c-fundamentals/operators

An operator's result being decided at compile time according to the operands' types; measured through precedence, short-circuit evaluation, compound assignment, and the defined–undefined distinction in evaluation order.

The Variables, Scope and Storage Duration lesson measured where a name is visible from
and how long a value lives — both were about the state of a *name*. This lesson moves one
step further: it measures what determines the result when two values are combined with an
operator. The answer is not the operands' value but their type — and this choice, just
like which declaration a name is bound to in the previous lesson, is decided at compile
time. This lesson measures four separate operator families — arithmetic, logical, bitwise
and assignment — with the same question: how much of an operator's result written in the
source, or which sub-operations will run to reach that result, is already known before a
run even starts?

The Programming Fundamentals course (M01/K02) already established arithmetic and
logical operators with Python; there, an operator's behavior was interpreted mostly by
looking at the operands' value at runtime. C's counterpart is measured here: which
operation an operator runs is fixed at compile time by looking at the type of the
operands written in the source, before the operation ever runs.

#### The Operator's Choice by Type

The same `/` operator runs two different operations depending on its operands' types:
integer division if both are integers, floating-point division if at least one is a
floating-point value. This choice is made not by looking at the data, but at the types
written in the source.

```c
// stage.c — operator choice by type (excerpt)
#include <stdio.h>

int main(void) {
    int whole = 7 / 2;
    double fractional = 7 / 2.0;
    printf("whole %d fractional %.1f\n", whole, fractional);
    return 0;
}
```

```
whole 3 fractional 3.5
```

**BS5.** The only difference written in the source is `2` versus `2.0` — one an integer
constant, the other a floating-point constant. This difference determines at compile time
which division operation runs: `7 / 2` runs integer division and drops the fractional
part, while `7 / 2.0` first promotes `7` to `double` and runs floating-point division. `7`
itself never changes; what changes is which hardware operation the same `/` operator
corresponds to — while the two-character operator symbol written in the source stays the
same, the machine instruction the compiler produces follows two different paths depending
on the operands' types. This is a decision in the same family as the integer promotion
measured in the previous lesson: the operator looks at the operands' declared type, not
their value at runtime.

This rule is not unique to the division operator; the rule the C standard calls the usual
arithmetic conversions works the same way for almost all binary arithmetic operators. If
the two operands' types differ, the "narrow" side is promoted to the "wide" one, and the
operation runs in the wide type — just as `char` operands were promoted to `int` in the
Data Types lesson, here too, when an `int` and a `double` are added, the `int` is
converted to `double` first. When an expression like `whole_count + ratio` is written in
the source, which operand is converted in which direction is determined at compile time
by looking at the two sides' declared types; which values are being added has no effect
at all on this decision.

#### Precedence: The Source's Structure Fixed at Compile Time

When an operator runs depends not only on type but also on the source's structure.
Precedence rules determine which of a parenthesis-free expression's sub-expressions is
evaluated before which, and this decision is written entirely in the source itself.

```c
// precedence.c — operator precedence fixes the source's structure at compile time
#include <stdio.h>

int main(void) {
    int multiplication_first = 2 + 3 * 4;
    int addition_first = (2 + 3) * 4;
    printf("%s %d\n", "multiplication first (no parentheses)", multiplication_first);
    printf("%s %d\n", "addition first (parenthesized)", addition_first);
    return 0;
}
```

```
multiplication first (no parentheses) 14
addition first (parenthesized) 20
```

Both expressions carry the same three numbers (`2`, `3`, `4`) and the same two operators
(`+`, `*`), but their results differ: `multiplication_first` runs multiplication before
addition (`2 + 12 = 14`), while `addition_first`, thanks to the parentheses, runs
addition first (`5 * 4 = 20`). This difference is resolved at compile time without
looking at any data, purely by the presence of the parentheses in the source — before a
run ever starts, which sub-expression is evaluated in which order is already known.

Precedence determines not only which operator runs first, but also, when operators of
the same precedence are written side by side, from which direction they group — this is
called associativity. Most binary operators (`+`, `-`, `*`, `/`) associate left to right:
the expression `10 - 3 - 2` groups as `(10 - 3) - 2`, not `10 - (3 - 2)`, and these two
groupings give different results (`5` versus `9`). The assignment operator, on the other
hand, associates in the opposite direction, right to left: the expression `a = b = 5`
groups as `a = (b = 5)`, `5` is assigned to `b` first, then the result of that assignment
is assigned to `a`. Both precedence and associativity are resolved at compile time,
purely by looking at the arrangement of operators in the source, before a run ever
starts.

#### Short Circuit: Which Operator Is Chosen Affects the Second Operand

Logical operators (`&&`, `||`) carry a guarantee: if the left operand is enough to
determine the result, the right operand is never evaluated. Bitwise operators (`&`, `|`)
do not carry the same guarantee — both operands are always evaluated. Which guarantee
applies is determined at compile time by looking at which operator is written in the
source.

```c
// short_circuit.c — which operator is chosen determines at compile time whether the second operand runs
#include <stdio.h>

static int call_counter = 0;

static int side_effecting(void) {
    call_counter++;
    return 1;
}

int main(void) {
    call_counter = 0;
    int and_result = (0 && side_effecting());
    printf("%s %d\n", "call counter after the and operator", call_counter);

    call_counter = 0;
    int bitwise_result = (0 & side_effecting());
    printf("%s %d\n", "call counter after the bitwise operator", call_counter);

    (void)and_result;
    (void)bitwise_result;
    return 0;
}
```

```
call counter after the and operator 0
call counter after the bitwise operator 1
```

**BS6.** Both calls use the same `side_effecting` function with the same `0` left
operand, but the result differs: with `&&`, because the left operand is already false,
the right operand is never run at all and `call_counter` stays at zero; with `&`, even
though the left operand's value is enough to determine the result, the operator itself
does not recognize such a shortcut, and `side_effecting` is called anyway. Which
guarantee applies is a decision that determines whether `call_counter` will actually be
`0` or `1` at runtime — but that decision itself is fixed at compile time, by looking at
which operator was chosen. Both operators would have produced the same logical result
(`0`) with the same left operand; the difference between them is not in the result, but
in whether the second operand runs.

This distinction becomes important when the goal is to make a call like
`side_effecting` run only when needed. If the left operand alone is enough to determine
the result (false for `&&`, true for `||`), the short-circuit guarantee makes sure the
right side never runs at all — this is a way of skipping an unnecessary piece of work,
obtained with a single line written in the source. Because bitwise operators do not carry
this guarantee, both operands run in every case; for this reason bitwise operators are
chosen not for a logical condition but when a genuine bit-level combination is needed.

#### Compound Assignment: Conversion by the Left Side's Type

A compound assignment operator (like `+=`, `|=`) always converts its result to the left
side's declared type — regardless of the right side's type.

```c
// assignment.c — the compound assignment result is converted at compile time by the left side's type
#include <stdio.h>

int main(void) {
    int whole_number = 5;
    whole_number += 2.7;
    printf("%s %d\n", "compound assignment on int", whole_number);

    unsigned char flag = 0xF0;
    flag |= 0x0F;
    printf("%s %d\n", "compound assignment on unsigned char", flag);

    int factor = 6;
    factor *= 2.5;
    printf("%s %d\n", "multiplicative assignment on int", factor);

    int counter = 10;
    counter -= 3;
    printf("%s %d\n", "subtraction assignment on int", counter);
    return 0;
}
```

```
compound assignment on int 7
compound assignment on unsigned char 255
multiplicative assignment on int 15
subtraction assignment on int 7
```

`whole_number += 2.7` works out to the same as `whole_number = whole_number + 2.7`: the
sum is first computed at `double` width (`5 + 2.7 = 7.7`), but because `whole_number`'s
type is `int`, the result is converted to `7` by dropping the fractional part. This
conversion is applied not by looking at the value of `2.7` on the right, but at the
declared type of `whole_number` on the left — the same truncation rule would apply even
if the right side were `2.1` or `2.9`. `flag |= 0x0F` is a bitwise combination: the
bit-level union of `0xF0` and `0x0F` is `0xFF`, `255` in decimal. `factor *= 2.5` confirms
the same rule once more: `6 * 2.5 = 15.0`, a result with an integer value at `double`
width, and because `factor`'s type is `int` it converts to `15` with no truncation loss
at all — showing that truncation does not always mean a loss, only that the result is
**forced** into the left side's type. `counter -= 3` is the plain case where both sides
are already the same type and the conversion is invisible. In all four examples, the
result's type is fixed by the left side's declared type; this shows that the assignment
operator, just like the division operator, works in a type-driven way.

#### The Bounding Measurement

Operator precedence determines in which order a source's sub-expressions group, but it
does not always determine in which order every sub-part of an expression is evaluated.
In a call like `f(a, b)`, which of `a` and `b` is computed first is unspecified; if `a`
and `b` modify the same variable through a side effect — say, both increment the same
counter — this is a situation the standard classifies as undefined behavior: the same
object is modified twice with no sequence point between them. Under this course's general
rule, such a call is not tried in a code block.

The defined counterpart set alongside it is the comma operator: two sub-expressions
separated by `,` are always evaluated left to right, with a full sequence point between
them — the right side does not start until the left side has completed.

```c
// ordering.c — the comma operator guarantees order, function arguments do not
#include <stdio.h>

int main(void) {
    int i = 0;
    int final_value = (i++, i++);
    printf("%s %d\n", "final value with the comma operator", final_value);
    printf("%s %d\n", "i with the comma operator", i);
    return 0;
}
```

```
final value with the comma operator 1
i with the comma operator 2
```

In the expression `(i++, i++)`, the left-side `i++` runs to completion first: it produces
`i`'s old value (`0`) and moves `i` to `1` — but this value is discarded by the comma
operator. Then the right-side `i++` runs: this time it produces `i`'s value (now `1`) and
moves `i` to `2`; the expression's overall result is the value the right side produced,
that is, `1`. The result is fully predictable and the same on every run: the comma
operator gives exactly the guarantee that a function call's argument list does not. If
the same `i++, i++` pattern were written as a function call's two arguments (like
`f(i++, i++)`), the order in which the compiler evaluates the arguments would be
unspecified, and modifying the same `i` twice would fall into undefined behavior — this
lesson does not try that call, it only measures the comma operator's guaranteed order.

Another classic example in the same class is an expression like `i + i++`: there is no
ordering guarantee between the addition operator's two operands either, and while the
left-side `i` is read, whether the right-side `i++` modifies `i` before or after that
read is unspecified — the same object is being read and modified, with no ordering
guarantee between them. This is also not run under this course's general rule. The
common thread here is that most of C's operators give no ordering guarantee at all
between their operands — ordering is explicitly guaranteed only for a few special
constructs (the comma operator, `&&`, `||`, the condition part of `?:`). A source writer
who wants to use the same variable more than once in an expression, modifying it at
least once, must lean on one of these guaranteed constructs or on separate expressions;
otherwise, which order the compiler chooses stays unspecified.

All the decisions measured in this lesson tie back to the same question: is an
operator's result determined at compile time by looking at the operand types and the
source's structure, or at runtime by looking at the operands' actual value? For division,
precedence, short-circuit and compound assignment, the answer was the compiler — which
operation runs or which order is followed could be read from the source before the
program ever ran. The cases where evaluation order stays undefined are not an exception
to this rule but its boundary: where the compiler gives no guarantee, the source writer
must build a guarantee of their own.

#### Summary

- Which operation an operator runs is decided at **compile time** by looking at the
  operands' **types** written in the source, not their value at runtime — class
  **compiler**.
- Operator precedence determines which sub-expressions of a parenthesis-free expression
  group first, purely from the source's own structure, without looking at any data;
  associativity gives the direction of grouping when operators of the same precedence are
  written side by side.
- `&&` and `||` carry a short-circuit guarantee, `&` and `|` do not; which guarantee
  applies is fixed at compile time by looking at which operator is written in the
  source.
- Compound assignment operators always convert their result to the left side's declared
  type — this conversion is made by looking at the left side's type, not the right side's
  value.
- The evaluation order of function call arguments is unspecified, and modifying the same
  variable twice with no sequence point between them is undefined behavior; it is not run
  in this course. The comma operator, by contrast, carries a guaranteed left-to-right
  order and was run as the defined counterpart.

#### Next Step

All the decisions measured in this lesson stayed within the boundary of a single
expression: which operation an operator ran, which sub-expression grouped first, which
operand was evaluated. The common trait of these decisions was that the data producing
the result never affected the decision itself — only the types and structure written in
the source were decisive. The Control Flow lesson adds a new question to this ground:
which branch a program follows is now determined not by the source but by data produced
at runtime. In this lesson, which operation a result like `multiplication_first` came
from could be read from the source; in the next lesson, which code path a branch follows
can only be known once the program runs and produces the data — it cannot be told in
advance by reading the source.

### Control Flow

Source: https://academia.sh/en/courses/c-fundamentals/control-flow

Which code path a branch follows being determined by data produced at runtime; switch fall-through, loop forms, goto's limited place, and the distinction between which branches exist and which one is chosen.

The Operators lesson measured what determines an expression's result, and the answer was
always the same: the types and structure written in the source were already known before
a run ever started. Control flow reverses this order. Which branch a program follows, how
many times a loop runs — these can no longer be told in advance by reading the source;
they are known only once the program runs and produces the data.

The Programming Fundamentals course (M01/K02) already established `if`, `while`, and
similar control structures with Python; there is a superficial resemblance between that
syntax and this one, but this lesson measures not syntax but **at which stage a decision
is made**: whether a branch is chosen or not depends on data, while which branches exist
in the source can depend on a separate stage entirely.

This lesson is a turning point for M11's measurement axis. The Data Types, Variables, and
Operators lessons all pointed in the same direction: the types and structure written in
the source made the decision before a run ever began. In control flow this direction
reverses — whether a branch is chosen is now known not by reading the source but by
running the program and producing the data. But this reversal is not absolute: as will be
seen below, which branches even exist in the source can still be decided at a stage
before the compiler, at the preprocessor. Control flow is therefore a subject that sits
not in a single class, but at the intersection of two.

#### Data-Dependent Branching

The excerpt below is part of the core introduced in the Compilation Stages lesson: an
array's sum is computed, and a branch is chosen by looking at that sum.

```c
// stage.c — data-dependent branching (excerpt)
#include <stdio.h>
#include <stddef.h>

int main(void) {
    int data[] = {4, 8, 15, 16};
    int sum = 0;
    for (size_t i = 0; i < sizeof data / sizeof data[0]; i++) {
        sum += data[i];
    }
    const char *verdict = (sum > 40) ? "large" : "small";
    printf("%s %s\n", "data-dependent branch", verdict);
    return 0;
}
```

```
data-dependent branch large
```

**BS7.** Whether the `verdict` variable will be `"large"` or `"small"` is not written
anywhere in the source — it cannot be known until the four numbers in the `data` array
are summed. Because `sum` comes out `43`, the `"large"` branch is chosen, but the result
could change if a single element were added to the array or one element changed — with
the source code itself never changing. This is a decision diametrically opposed to the
operator choice measured in the previous lesson: there, types determined the result and
it was known before a run started; here, data determines the result and it can only be
known once the loop has completed.

This difference also shows up in the `?:` conditional operator itself. Which branch of
`(sum > 40) ? "large" : "small"` produces a value depends on the run, but that the
expression itself produces a value is already known at compile time — the compiler
derives a common result type from the two branches (here both are the same, so `const
char *`), and it knows one of the two will be chosen, only not which one. This shows a
general property of control flow: the source fixes at compile time which branches exist
and which type of result each branch will produce; only which one runs, decided by
looking at the data, is left to runtime. The same distinction holds for `if` bodies and
`switch` cases: every branch in the source is compiled and takes its place in the binary,
but which one actually executes on a given run can only be told after that run has
finished.

#### Switch Fall-Through

In a `switch` statement, consecutive `case` labels **fall through** into the same body
unless a `break` separates them — flow passes from one label to the next without being
interrupted. **Fall-through here corresponds to what was closed in M06/K05** — this usage
is entirely separate from that lesson's `drop` sense; C has no such concept as `drop` at
all.

```c
// fallthrough.c — switch fall-through binds consecutive labels to the same body
#include <stdio.h>

static void print_level(int score) {
    switch (score / 10) {
        case 10:
        case 9:
            printf("%s\n", "level: A");
            break;
        case 8:
        case 7:
            /* deliberate fall-through: 8 and 7 fall into a shared body */
            printf("%s\n", "level: B");
            break;
        default:
            printf("%s\n", "level: C");
            break;
    }
}

int main(void) {
    print_level(95);
    print_level(88);
    print_level(72);
    print_level(40);
    return 0;
}
```

```
level: A
level: B
level: B
level: C
```

When `score / 10` gives `10`, flow enters the bodiless `case 10` label, finds no body,
and falls through into `case 9`, running the body there; when it gives `9`, it jumps
directly into `case 9`. The two labels thus share a single body. The same arrangement
applies to `case 8` and `case 7`. Flow continuing when no `break` is written is C's
default behavior; every `case` that wants to exit a body must state so **explicitly**
with `break`. The four calls above confirm this: `95` matches the bodied `case 9` and
never falls through at all; `88` matches the bodiless `case 8` and falls through into
`case 7`'s body.

Fall-through itself is always present in the source regardless of which `case` matches —
the compiler produces a path flowing from every label without a `break` to the next one,
and this path sits in the binary. But whether this path is followed is determined at
runtime, purely by the value at the head of the `switch` statement: when `95` is entered,
flow does not start from `case 10` at all, it jumps directly to `case 9`, because
`switch` leaps to a matching label the instant it finds one and runs only from that point
onward, in sequence. Fall-through therefore does not mean "every label is tried in
sequence" — `switch` first finds the matching label, and fall-through only passes through
the labels **after** that point, bodiless, if no `break` separates them.

#### Loop Forms: When the Condition Is Tested

C carries three basic loop forms, and the difference between them is whether the
condition is tested **before** or **after** the loop body — and this leads to a
data-dependent difference in behavior.

```c
// loops.c — while and do-while interpret the same condition with a different run count
#include <stdio.h>

int main(void) {
    int counter = 5;
    int while_run_count = 0;
    while (counter < 5) {
        while_run_count++;
        counter++;
    }
    printf("%s %d\n", "while run count", while_run_count);

    int do_while_run_count = 0;
    do {
        do_while_run_count++;
    } while (counter < 5);
    printf("%s %d\n", "do-while run count", do_while_run_count);
    return 0;
}
```

```
while run count 0
do-while run count 1
```

Because `counter` starts at `5` and `while (counter < 5)` tests its condition **before**
the body, the condition is false from the very start and the body never runs at all:
`while_run_count` stays at zero. `do { ... } while (counter < 5)`, by contrast, runs the
body first and tests the condition **after**; even though the condition is again false,
the body has already run once, and `do_while_run_count` comes out `1`. Both loops carry
the same condition and run on the same data, but because the testing order differs,
their results diverge — this difference depends not on where the condition is written in
the source, but on which form the loop is declared in, and a `for` loop tests its
condition before the body just like `while` does.

All three forms have their body written only once in the source, but how many times it
runs depends on the data at runtime — if `counter`'s starting value were different, the
same three loops would run a different number of times. The difference between `while`
and `for` is purely a matter of notation: `for (start; condition; step)` gathers a
`while` loop's three parts onto a single line, and both follow the same testing order
(condition first). `do-while` is the one form that differs: when a loop's body must run
at least once regardless of the condition — showing a menu, prompting for input — this
form expresses that directly, without needing a separate flag variable or writing the
body twice.

#### Goto: A Limited and Accepted Use

`goto` jumps directly to a label from anywhere in the source, and its general use is
discouraged because it makes flow harder to follow. But in a narrow scenario like exiting
nested loops in a single step, `goto` offers a widely accepted solution, because `break`
can only break the nearest loop.

```c
// search.c — goto is a limited and accepted use for exiting a nested loop in one step
#include <stdio.h>

int main(void) {
    int grid[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int target = 6;
    int found_i = -1, found_j = -1;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (grid[i][j] == target) {
                found_i = i;
                found_j = j;
                goto found;
            }
        }
    }
found:
    printf("%s %d\n", "found row", found_i);
    printf("%s %d\n", "found column", found_j);
    return 0;
}
```

```
found row 1
found column 2
```

The value of `target` is found at `grid[1][2]`, and `goto found;` leaves both loops in a
single line. Code trying to achieve the same result with `break` would have to exit the
inner loop, then check a flag variable to break the outer loop separately as well —
`goto` does the same job here without needing an extra flag variable. That the `found`
label sits outside the loops, near the end of the function, is also part of the accepted
usage: `goto` only jumps forward, to a point where the function's natural flow ends, and
does not build a complex web of jumps in the middle of the source.

Whether the `found` label is actually jumped to is also a data-dependent decision: if the
value of `target` were never found inside `grid`, both loops would run to completion and
`goto` would never run at all, flow reaching the label only in sequence, at the loops'
natural end. In the source, the `goto found;` line always sits in the same place, but
whether that line runs — and if it does, at which loop step — is determined at runtime
by looking at `grid`'s contents and `target`'s value. `goto` therefore does not depart
from control flow's general rule: the jump itself is fixed in the source, whether the
jump is triggered depends on the data.

#### The Bounding Measurement

Which code path a branch follows depended on data, but which branches exist in the
source is not always determined by runtime — sometimes the preprocessor, before the
compiler ever sees the source, has already decided which `case`s will even exist.

```c
// boundary.c — which branches exist is settled at the preprocessor, which one is chosen at runtime
#include <stdio.h>

#define EXTRA_CASE

static void evaluate_code(int code) {
    switch (code) {
        case 1:
            printf("%s\n", "case 1 ran");
            break;
#ifdef EXTRA_CASE
        case 2:
            printf("%s\n", "case 2 ran (preprocessor confirmed its existence)");
            break;
#endif
        default:
            printf("%s\n", "unrecognized case");
            break;
    }
}

int main(void) {
    for (int code = 1; code <= 3; code++) {
        evaluate_code(code);
    }
    return 0;
}
```

```
case 1 ran
case 2 ran (preprocessor confirmed its existence)
unrecognized case
```

Because the `EXTRA_CASE` macro is defined, the `case 2` label is included into the text
by the preprocessor before the compiler ever sees the source. Had `EXTRA_CASE` not been
defined, that `case` block would have been erased from the source entirely — the compiler
would never see it, and flow would fall straight to `default` when `code == 2` arrived.
In this measurement, `case 2` exists because `EXTRA_CASE` is defined, and as `code`
advances from `1` to `3` in the loop, which case is chosen is determined at runtime at
every step by looking at the data (the loop counter). The two decisions are entirely
independent of each other: whether a branch exists is decided at the **preprocessor**,
whether that branch is chosen is decided at **runtime** — and if a branch never exists at
all, no data can choose it.

The order of these two stages cannot be reversed. The preprocessor runs first and either
erases `#ifdef` blocks from the source entirely or leaves them entirely; the compiler
already knows which `case`s exist in the text that reaches it and never makes a decision
like "let this case exist for this data." Runtime, in turn, works under the opposite
constraint: it cannot change which branches already exist in the binary it has, it can
only choose among the ones that exist. This distinction produces a practical consequence
when tracking down a bug: "this branch never ran" and "this branch does not exist in the
source at all" are two separate possible causes, and where each is checked also differs —
one is looked for in the runtime output, the other in the source's preprocessor
directives.

#### Summary

- Which code path a branch follows is determined by looking at data produced at
  **runtime**; the source code itself does not make this decision in advance — class
  **runtime**.
- `case` labels inside a `switch` with no `break` **fall through** into the next body;
  this fall-through is C's default behavior unless `break` is written explicitly in the
  source.
- `while`'s condition is tested before the body, `do-while`'s after; even with the same
  condition and the same data, this difference in order can change the run count.
- `goto`, in a narrow and accepted scenario like exiting nested loops in one step, does
  the same job without requiring an extra flag variable.
- Which branches exist in the source can be decided at the **preprocessor**, and which of
  the existing branches is chosen can be decided at **runtime** — both decisions can
  coexist within the same `switch` and are independent of each other.
- The `?:` conditional operator fixes at compile time that it produces a value and that
  value's type; the type is derived from the two branches, only which branch actually
  produces the value is determined at runtime.

#### Next Step

The branches measured in this lesson all stayed within the body of a function already
known — which branch was chosen depended on data, but which function was called was
always fixed in the source. The Functions lesson tests this last assumption: which
definition a call is bound to is resolved only at the linking step, after two units have
been compiled separately. A function like `evaluate_code` above was always defined in the
same file; in the next lesson, a function will be called in one unit while its definition
sits in another — and what determines the link between the two, and at which stage, will
be settled there. In this lesson, the result of a test like `sum > 40` depended only on
the `data` array; in the next lesson, a call's result will also depend on which
definition the called function reaches — a name written in the source cannot correspond
to two different bodies in two different units, but only the linker knows which body it
is bound to.

### Functions

Source: https://academia.sh/en/courses/c-fundamentals/functions

A call being bound to a definition in another unit being resolved at the linker stage; the declaration–definition distinction, parameter passing always being by copy, and the return value's type determining the caller side's interpretation.

The Control Flow lesson dealt with a question that always stayed inside the same
function: which branch gets chosen. This lesson lifts that boundary and measures how a
call crosses a function's **boundary**. When a function is called, three separate
questions are resolved at once: which definition the call is bound to, how the arguments
reach the function, and how the returned value is interpreted — all three decided at the
same time, but for different reasons. The first belongs to the linker, because it
requires more than one unit; the other two are decisions the compiler makes by looking at
a single unit — even without knowing exactly where a call will be bound, the compiler can
work out from the declaration how the arguments will be copied and how the return will be
interpreted.

The Programming Fundamentals course (M01/K02) already established the concept of a
function with Python; there, a function only needed to be defined before it could be
called, and there was no declaration–definition distinction. C's counterpart is measured
here: a call can be compiled by relying only on a declaration, without ever seeing the
definition — the definition itself can sit in another unit, and the step that binds the
two sides together belongs to a separate stage. The three decisions measured in this
lesson — binding, passing, return — are three separate components of a function's calling
surface, and each is fixed at its own stage, independently of the others.

#### The Linker: A Function in Another Unit

The excerpt below is part of the core introduced in the Compilation Stages lesson: a
function is called in one unit, while its definition sits in another.

```c
// stage.h — declarations of names defined in other units (excerpt: function declaration)
#ifndef STAGE_H
#define STAGE_H

/* This function is defined in another unit: its call is resolved at link time. */
int identity(void);

#endif
```

```c
// choice.c — the second unit: the function's definition lives here
#include "stage.h"

int identity(void) {
    return 7;
}
```

```c
// stage.c — the linking of a function in another unit (excerpt)
#include <stdio.h>
#include "stage.h"

int main(void) {
    printf("%s %d\n", "function in another unit", identity());
    return 0;
}
```

```
function in another unit 7
```

**BS8.** While `stage.c` is compiled, `identity`'s body appears nowhere — the compiler
sees only the declaration in `stage.h`, and from that declaration checks that the call
takes the right number of arguments of the right type, that the return type is `int`, and
leaves a "I need this name" note in the object file. `choice.c` is compiled separately
and writes `identity`'s real body into its own object file. When the two object files are
brought together, the linker matches the note `stage.c` left with the body `choice.c`
supplies — without this match, an executable program never comes into being at all.
Scope and storage duration were questions that stayed inside a single unit; which body a
call is bound to, on the other hand, is resolved only once units come together, at the
**linker** stage.

This three-stage flow — first the two units are compiled separately, then the linker
merges the two — is the general pattern measured in the Compilation Stages lesson,
applied to functions. Compiling `stage.c` proceeds unaware of whether `choice.c` even
exists; the compiler works only by looking at the declaration in `stage.h`, under the
assumption "a function with this name, with this signature, must be defined somewhere,"
and confirming that assumption is the linker's job. If `choice.c` had never been
compiled, or `identity`'s definition had been mistakenly deleted, compiling `stage.c`
would still complete without trouble — the error would only surface at the linking step,
when the linker cannot find the name `identity` in any object file. This shows why a
compile error and a link error can surface at different stages, at different times.

This mechanism belongs to the same family as the `extern` variable binding measured in
the Variables, Scope and Storage Duration lesson: there, which storage location a name
corresponds to, here, which body a name corresponds to, is resolved at the linker. The
difference is that functions have external linkage by default — while a variable needs
`extern` written to be shared, a function is already visible and callable across units
unless it is written `static`. Because the `identity` function is not `static`, its
definition in `choice.c` can be seen from `stage.c`; had the function been defined as
`static identity(void)`, it would close into its own unit, and compiling `stage.c` would
fail because the linker could not find the same name.

This confirms that the `static` keyword performs the same function for functions as it
does for variables: it narrows scope to the file boundary and prevents the linker from
exposing that name to other units. Declaring a function `static` explicitly marks in the
source that the function is only a helper belonging to its own unit's internal working,
not offered as an interface to the outside; this is the function-level counterpart of a
unit being able to hide its own internal details.

#### The Separation of Declaration from Definition

A declaration tells the compiler a function's signature (name, parameter types, return
type); a definition also includes the body. `int identity(void);` in `stage.h` is a
declaration — it ends with a semicolon, it carries no body. `int identity(void) { return
7; }` in `choice.c` is a definition. A function can be declared as many times as wanted
across different units (through the same header file), but it must have exactly **one**
definition — otherwise the linker finds more than one body for the same name, and this is
a contradiction. This separation is the function-level application of the
declaration–definition distinction measured for variables in the previous lesson: `extern
int unit_counter;` was a variable declaration while `int unit_counter = 2;` was the
definition; here the same pattern repeats, with a declaration carrying only the signature
in place of the body.

Without a function declaration, it is not possible to check whether a call is correct
just by looking at how it is called; the compiler learns how many parameters are expected
and of what type only from the declaration. `stage.h` being included by `stage.c`
provides exactly this: while `stage.c` is compiled, it never sees `identity`'s body, but
it knows it is a function taking no parameters and returning `int`, and it compiles the
call accordingly. A declaration is therefore not merely documentation — it is the minimum
information the compiler needs to produce the call correctly.

#### Parameter Passing: Always a Copy

In C, every argument passed to a function is **copied** into the function's own
parameter. A change made to the function's parameter never affects the caller side's
variable.

```c
// copy.c — parameter passing is always a copy, it never affects the caller's variable
#include <stdio.h>

static void double_value(int value) {
    value = value * 2;
    printf("%s %d\n", "value inside the function", value);
}

int main(void) {
    int number = 5;
    double_value(number);
    printf("%s %d\n", "number on the caller side", number);
    return 0;
}
```

```
value inside the function 10
number on the caller side 5
```

The measurement prints two lines: `value` inside the function, `10` after the
multiplication; `number` on the caller side is still `5`. When `double_value` is called,
`number`'s value (`5`) is copied into a new storage location named `value`; the change
inside `value` affects only that copy, it never touches `number` in `main`. When the
function returns, `value` disappears — an instance of the automatic storage duration
measured in the Variables, Scope and Storage Duration lesson — and `number` remains
untouched. This behavior is the same regardless of the type of parameter the function
takes (integer, floating point, struct): passing in C is always **pass-by-value**; the
source's own variable never passes directly into the function.

This copying works the same way regardless of how many parameters there are, and is
independent of the function body's length: the name `value` corresponds to a storage
location separate from both the caller's `number` and the function's own body, created
the instant the function is called and disappearing when it returns. This shows that the
automatic storage duration rule measured in the Variables, Scope and Storage Duration
lesson also applies to parameters: a parameter behaves just like an ordinary local
variable declared inside the body — only its initial value comes from the caller side. A
source writer who genuinely wants to modify the caller's variable does this in C not by
passing the value, but by passing that value's **address**; but the address itself is not
printed in this lesson, and this subject belongs to the pointers subject, to which an
entire course is devoted.

#### The Return Value's Type

A function's return type determines how the caller interprets that value, independently
of which expression is computed inside the body.

```c
// returns.c — the return type determines how the caller interprets the value
#include <stdio.h>

static int ratio(int numerator, int denominator) {
    double actual_ratio = (double)numerator / (double)denominator;
    return (int)(actual_ratio * 100);
}

int main(void) {
    int percentage = ratio(1, 3);
    printf("%s %d\n", "percentage with int return type", percentage);
    return 0;
}
```

```
percentage with int return type 33
```

Inside `ratio`, `actual_ratio` is a `double`, and `1 / 3` is computed as floating-point
division rather than integer division (`0.333...`). But because the function's return
type is declared `int`, the value in the `return` expression — `33.333...` — is converted
to `int` before it reaches the caller, and the fractional part is dropped: `33`. This
conversion is applied not by looking at where or which type is used inside the function
body, but solely at the function's **declared** return type — just as a compound
assignment's result was forced into the left side's type, here the result is forced into
the type written in the function's signature. Different calls like `ratio(1, 3)` and
`ratio(1, 4)` produce different actual ratios, but both go through the same `return`
expression, the same narrowing-to-`int` rule — regardless of which input it is called
with, the conversion rule itself never changes.

Where this conversion happens matters: the expression `actual_ratio * 100` is still
computed at `double` width, inside `ratio`'s body; narrowing to `int` is applied only as
the `return` expression's value leaves the function, exactly at the function's boundary.
The caller side (`main`) never knows and does not need to know which intermediate types
were used inside `ratio` — it only sees the declared return type (`int`) and assigns the
incoming value to a variable of that type (`percentage`). This shows that a function's
boundary provides a kind of **encapsulation**: whatever intermediate types the
computation inside the body uses, the only piece of information that leaks outward is the
return type written in the signature.

#### The Bounding Measurement

A call compiles and links safely as long as it is compatible with the called function's
declaration — the `correct.c` example below shows this.

```c
// stage.h — the full declaration telling the compiler the call's shape
#ifndef STAGE_H
#define STAGE_H

int identity(void);

#endif
```

```c
// choice.c — the definition carries the same signature as the declaration
#include "stage.h"

int identity(void) {
    return 7;
}
```

```c
// correct.c — when declaration and definition share the same signature, the call links safely
#include <stdio.h>
#include "stage.h"

int main(void) {
    printf("%s %d\n", "correctly declared call", identity());
    return 0;
}
```

```
correctly declared call 7
```

In this setting, a source trying to call a function whose declaration is never seen is
rejected directly by the compiler — the compiler needs at least a declaration in order to
check a call's shape, and stops compiling the source without one. But this does not mean
the compiler catches every mismatch. A declaration is checked only within the unit it
sits in; while the compiler compiles `stage.c` it never looks inside `choice.c`, and
there is no mechanism verifying that the real definition in `choice.c` genuinely matches
the declaration. C's linker also looks only at the symbol name, not the type — if two
units declare and define a function with the same name but different parameter types,
the linker still binds them to each other. Such a call falls into undefined behavior once
compiled, linked, and run, because the caller side prepares the arguments one way while
the called side reads them another; under this course's general rule, such a mismatch is
not tried in a code block. This lesson runs only the correctly declared setup above: the
declaration tells the compiler the call's shape, but what guarantees that it genuinely
matches the definition is the source writer's own consistency — neither the compiler nor
the linker checks this across units.

The practical consequence of this limit is why a header file is kept as a single
**source of truth**. As long as every unit calling `identity` includes `stage.h`, and
`choice.c` also includes the same header, declaration and definition come from the same
source and automatically stay consistent with each other — the source writer never has to
hand-write the same signature twice in two different places. If a unit did not include
the header and instead hand-wrote the function's signature in its own source, the two
declarations would sit separately in the source, and one could go stale unnoticed if the
other changed — exactly the source of the risk described above. Sharing the header file
does not eliminate this risk, but it at least guarantees the signature is written once,
and hence consistency coming from at least one source.

#### Summary

- Which definition a call is bound to is resolved by the **linker** once units come
  together; the compiler only checks the signature from the declaration, it never sees
  the body.
- A declaration carries only the signature (name, parameter types, return type); a
  definition also includes the body. A function can be declared many times, but is
  defined exactly once.
- Parameter passing is always pass-by-**value**: a change made inside the function never
  affects the caller's own variable.
- A function's return value is converted to the function's **declared** return type,
  independently of the types of the intermediate computations inside the body.
- The match between a declaration and that name's real definition is checked across
  units by neither the compiler nor the linker; keeping this consistency is the source
  writer's job.
- A `static` function closes into its own unit and is exposed to no other unit by the
  linker; the same hiding rule from variables applies to functions as well.

#### Next Step

All the decisions measured in this lesson were about a function's calling **shape**:
which definition it is bound to, how its argument is passed, how its return value is
interpreted. The Arrays and Strings lesson adds to this ground the question of when a
piece of data's **length** is known: an array's element count can be produced at compile
time from a constant expression in the source, while a string's length can only be
learned at runtime by scanning until a terminating byte is reached — and the "parameter
is always a copy" rule measured in this lesson turns into an interesting exception once
an array is passed to a function: when an array name is passed to a function, what is
copied is not the array itself, but the address of the array's first element — and this
is also the source of why length information must be carried separately.

### Arrays and Strings

Source: https://academia.sh/en/courses/c-fundamentals/arrays-and-strings

An array's element count being determined at compile time through constant folding, and a string's length being determined at runtime by scanning until the terminating byte; the array–pointer relationship measured through element value and difference without printing an address.

The Functions lesson measured which definition a call is bound to, how an argument is
copied, and how a return is interpreted. This lesson returns to the transferred data
itself, comparing how two different data kinds carry their length: an **array's** length
and a **string's** length, even though both rest on the same memory layout, are decided
at two separate stages.

The Programming Fundamentals course (M01/K02) already established the list and string
concepts with Python; there, a list's length was information that came along on its own,
always askable with a call (`len`). C's counterpart is measured here: an array's length
is produced at compile time from a constant expression written in the source, while a
string's length is stored nowhere at all — it is learned only by scanning at runtime
until a terminating byte is reached. This lesson is the one that shows both ends of the
course's measurement axis at once: two neighboring values sitting in the same memory
layout give the answer "the source is enough" for one, and "only runtime is enough" for
the other.

#### Constant Folding: Array Length at Compile Time

The excerpt below is part of the core introduced in the Compilation Stages lesson: an
array's size is produced from a constant expression written in the source.

```c
// stage.c — constant folding (compiler) and string length (runtime) excerpt
#include <stdio.h>
#include <string.h>

int main(void) {
    int constant_array[2 * 3 + 1];
    printf("%s %d\n", "constant folding", (int)(sizeof constant_array / sizeof constant_array[0]));

    const char *text = "hello, world!";
    printf("%s %d\n", "string length", (int)strlen(text));
    return 0;
}
```

```
constant folding 7
string length 13
```

**BS9.** The expression `2 * 3 + 1` is bound to no variable — it is built only from
constants, and the compiler **folds** this expression down to `7` without the program
ever running; `constant_array`'s element count is already written into the binary as `7`.
`strlen(text)` is the exact opposite: the byte sequence the `text` pointer points to is
scanned one byte at a time until `\0` is found, and the length (`13`) only emerges once
this scan is complete. Both measurements sit on the same line, in the same function, but
one confirms a number that could be read from the source, while the other produces a
number that can only be learned by running it.

Because `constant_array`'s size is embedded in the binary, the expression `sizeof
constant_array` itself requires no computation at runtime either — the compiler has
already replaced this expression with a constant. This shows that `sizeof`, in most of
its uses, is a **compile-time** operator: it produces a result by looking only at its
operand's type, never at its actual value. In a declaration like `constant_array[2 * 3 +
1]`, the expression inside the brackets falls into the same category — the expression
determining an array's size must be a **constant expression**; had a variable or a
function call entered it, the compiler could no longer know the size in advance, and this
would enter C's separate subject of variable-length arrays. The array measured in this
lesson always has a fixed length.

#### The Terminating Byte's Role: String Length at Runtime

A string's length is stored nowhere separately; `strlen` and similar functions
**compute** the length by scanning memory until they encounter a `\0` byte. When the
terminating byte's position changes, the same memory area reports a different length.

```c
// terminator.c — the terminating byte's position determines at runtime where a string ends
#include <stdio.h>
#include <string.h>

int main(void) {
    char short_string[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
    printf("%s %d\n", "length while the terminator is at the end", (int)strlen(short_string));

    short_string[2] = '\0';
    printf("%s %d\n", "length once the terminator is placed early", (int)strlen(short_string));
    return 0;
}
```

```
length while the terminator is at the end 5
length once the terminator is placed early 2
```

**BS10.** The `short_string` array allocates six bytes, the first five letters and the
sixth `\0`; `strlen` returns `5` because it encounters the terminator right after the
fifth byte. Once `short_string[2]` is changed to `'\0'`, the array itself is still six
bytes — no memory was reallocated, no element was removed — but `strlen` now returns `2`,
because the scan stops the instant it reaches the third byte. This shows that a string's
"length" is not actually a storage property but a **runtime result**: the same memory
layout can report a different length depending on the terminator's position, and that
position is an ordinary byte that can change at runtime.

This also explains why a string is not a type of its own in C: `char short_string[6]` is
a `char` array, and to the language's eyes carries nothing more than six separate `char`
values. "String" is only an interpretation built on a **convention** — that a `\0` byte
will be found somewhere inside it, and that byte marks the boundary — not a property the
language itself enforces. There is no such convention for an integer array (`int
array[]`) — there is no part of the language stating a contract for which element marks
the "end"; this is why an integer array's length is either determined by a constant
expression in the source (like `constant_array` above) or carried separately in a
variable. A string is a special case, only for `char` arrays, that carries its own length
within itself through the terminating-byte convention. Manually writing `'\0'` into
`short_string[2]` does not disturb the array's own memory layout at all; it only changes
where this convention stops. Functions like `strlen` **assume** this convention — they
trust that a `\0` really exists somewhere in the memory a pointer passed to them points
to; if the terminator is absent entirely, the scan runs past the array's allocated
boundary, and this is an undefined-behavior scenario this course's general rule does not
try.

#### The Array–Pointer Relationship: Measuring Without Printing an Address

In most contexts, an array's name **decays** into a pointer to its first element. This
relationship can be measured without printing an address, purely through element value
and difference.

```c
// relationship.c — the array–pointer relationship measured through value and difference, no address printed
#include <stdio.h>

int main(void) {
    int array[] = {10, 20, 30, 40};
    int *p = array;

    printf("%s %d\n", "value accessed via p's offset", *(p + 2));
    printf("%s %d\n", "value accessed via array indexing", array[2]);
    printf("%s %d\n", "are the two accesses the same", (int)(*(p + 2) == array[2]));
    printf("%s %d\n", "are the addresses the same", (int)((p + 2) == &array[2]));
    printf("%s %d\n", "difference in elements", (int)(&array[3] - &array[0]));
    return 0;
}
```

```
value accessed via p's offset 30
value accessed via array indexing 30
are the two accesses the same 1
are the addresses the same 1
difference in elements 3
```

**BS11.** With the assignment `p = array`, `p` takes the address of the array's first
element; `*(p + 2)` and `array[2]` produce the same value (`30`), because `array[i]` is
already shorthand for `*(array + i)` — both compute the same address and read the same
value. The comparison `(p + 2) == &array[2]` confirms this too: the two addresses are
equal. The difference `&array[3] - &array[0]` is not a raw byte difference but a
difference **in elements** — the result is `3`, because there are three `int`-widths of
stride between the two pointers; this is a behavior the difference operator has specially
defined for pointers in C, given already divided by the `int` width. No step printed a
real address; everything measured was a value, an equality, or an element difference.

That `array[2]` is only shorthand for `*(array + 2)` shows that C does not dedicate a
separate mechanism to array indexing: the square bracket is a syntactic convenience built
on top of pointer arithmetic. This is also why the notations `array[2]` and `2[array]`
mean the same thing — because addition is commutative, `array + 2` and `2 + array` give
the same address, and both notations go through the same translation. This lesson does
not measure that equivalence, but it does confirm that what underlies `array[i]` is
genuinely an addition and a **dereference** — not some special "array access"
instruction. This is a language-level equivalence: both notations compute the same
address and give the same value. How many steps the compiler's produced machine code
spends is a separate question — `array` is an array name, `p` is a variable that must be
read from memory — and this lesson does not measure that.

This equivalence leaves a fine distinction between the array itself and the pointer the
array decays into: the array name by itself carries the name of the whole array, while
`p` is a pointer variable and can be reassigned to point to another array — `p =
other_array;` is valid, but `array = other_array;` is not, because `array` is an array
declaration, not a reassignable pointer variable. As a rule, an array name decays into
the address of its first element whenever it occurs in an expression; a few cases are
exceptions to this, such as being the operand of `sizeof` or `&`. What decays is an
address value, not a reassignable pointer variable.

#### The Bounding Measurement

An array's length is known at compile time, but this information does not pass to a
function. A function taking an array as a parameter receives not the array itself but
only the address of its first element, and cannot know the length on its own.

```c
// length.c — array length information does not pass to a call, the receiving side must be given it separately
#include <stdio.h>
#include <stddef.h>

static int sum_assuming_four(int data[]) {
    /* this function does not know the array's real size: it assumes a count of 4 */
    int sum = 0;
    for (int i = 0; i < 4; i++) {
        sum += data[i];
    }
    return sum;
}

static int sum_with_length(int data[], size_t length) {
    int sum = 0;
    for (size_t i = 0; i < length; i++) {
        sum += data[i];
    }
    return sum;
}

int main(void) {
    int large[] = {1, 2, 3, 4, 5, 6};
    printf("%s %d\n", "sum with an assumed fixed count", sum_assuming_four(large));
    printf("%s %d\n", "sum with a given length", sum_with_length(large, 6));

    int small_array[] = {10, 20, 30};
    printf("%s %d\n", "sum with a given length, different size", sum_with_length(small_array, 3));
    return 0;
}
```

```
sum with an assumed fixed count 10
sum with a given length 21
sum with a given length, different size 60
```

Even though `int data[]` is written in `sum_assuming_four`'s source, it only receives an
`int *` when called — it carries no information at all about the array's real size. The
function has written its loop with a fixed bound of `4` and sums `large`'s first four
elements, giving `10` — short of the array's real sum, `21`, because the last two
elements are never seen. `sum_with_length`, by contrast, takes its length as a separate
parameter (`length`) and produces the correct sum (`21`); the same function, called with
a differently sized array like `small_array`, also gives the correct result (`60`),
because it takes its loop bound from the caller instead of writing it as a fixed value in
the source. This shows how the "a parameter is always a copy" rule measured in the
Functions lesson works for an array: what gets copied is not the array itself, but the
address of its first element — and alongside that address, the array's element count is
lost unless it is carried separately.

The `int data[]` written in `sum_assuming_four`'s declaration is interpreted by the
compiler as `int *data` — an array declaration in a parameter list is always converted to
a pointer declaration in C. This is why writing `sizeof(data)` inside the function body
would not give the array's size but a pointer's size; since no size value is printed in
this course, this lesson does not measure that error, but it does explain why
`sum_assuming_four` **had** to assume a fixed number (`4`): the function can in no way
derive the array's real length from the pointer it is given. This is a direct
consequence of the array–pointer relationship measured above: when an array passes to a
function, only the address of the first element remains, no metadata belonging to the
array itself (such as element count) travels along with that address. This is also the
same constraint behind why the standard library's string functions (`strlen` included)
encode length information with a terminating byte: if only a starting address can be
passed to a function, where the data ends must be carried along with that data, as a
mark.

#### Summary

- An array's element count is produced at **compile time** from a constant expression in
  the source (constant folding); class **compiler**.
- A string's length is stored nowhere; it is computed at **runtime** by scanning until
  the terminating `\0` byte is reached; class **runtime**. This lesson thus carries two
  classes at once.
- The terminating byte's position can change at runtime; the same memory area can report
  a different string length depending on the terminator's placement.
- The array–pointer relationship can be measured without printing an address: element
  value (`*(p + i)` and `array[i]` are the same), equality, and difference in elements
  are enough.
- An array's length information does not pass to a function; the receiving side cannot
  know the array's real size on its own unless it takes the length as a separate
  parameter.
- An array declared as a parameter is converted by the compiler to a pointer; the
  string's terminating-byte convention exists precisely for this reason — so that
  functions forced to work with only a starting address can carry the length along with
  the data.

#### Next Step

The two data kinds measured in this lesson — array and string — always stayed within the
boundary of a single variable. The Structs, Unions and Enumerations lesson measures three
new mechanisms that gather several values under a single name: how a struct's fields are
laid out, how a union shares the same memory, and how an enumeration constant turns into
a compile-time constant. This lesson measured that an array's length comes from a
constant expression in the source; in the next lesson, the same constant-expression idea
will explain how an enumeration value is produced. Arrays and strings here always carried
a single-type sequence of values — all `int` or all `char`; structs, by contrast, will
gather values of different types under a single name, and how those values are laid out
in memory will again be a question decided at compile time.

### Structs, Unions and Enumerations

Source: https://academia.sh/en/courses/c-fundamentals/structs-unions-and-enums

A struct's fields being laid out at compile time, a union sharing the same memory, and enumeration constants being compile-time constants; showing with a tag field that the language does not track which member of a union is valid.

The Arrays and Strings lesson measured how values of the same type are ordered within an
array. This lesson measures how values of different types are gathered under a single
name: a **struct** places different types side by side, a **union** has the same memory
shared by different interpretations, an **enumeration** produces named constants — all
three mechanisms decided at compile time from the declaration written in the source.

The Programming Fundamentals course (M01/K02) already established record-like data
grouping with Python; there, fields were names inside a dictionary or object, addable and
removable at runtime. C's counterpart is measured here: which fields a struct will carry,
in what order those fields sit in memory, and an enumeration constant's value — all fixed
at compile time from the declaration written in the source, without the program ever
running. This is the last stop on the course's measurement axis: the question opened in
The Place of C lesson, that a source turns into a program by passing through four stages,
closes here by asking at which stage three new mechanisms are decided.

#### Struct: Fields Laid Out at Compile Time

A struct declaration fixes every field's name and type in the source; after this
declaration, every variable of that struct type carries the same field layout. While an
array, as measured in the Arrays and Strings lesson, orders values of the same type, a
struct gathers values of different types under a single name — like a point's `x` and `y`
coordinates, values that carry meaning together but can be of different types. Assigning
one struct variable to another copies every field at once.

```c
// struct.c — a struct's fields are laid out at compile time, assignment copies every field
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point a = {3, 4};
    struct Point b = a;
    b.x = 99;

    printf("%s %d\n", "a.x", a.x);
    printf("%s %d\n", "a.y", a.y);
    printf("%s %d\n", "b.x", b.x);
    printf("%s %d\n", "b.y", b.y);
    return 0;
}
```

```
a.x 3
a.y 4
b.x 99
b.y 4
```

**BS12.** The `struct Point` declaration fixes at compile time that every `Point`
variable will carry an `x` and a `y` field, and these fields' names and types — this
struct decides which fields it will carry not at runtime, but in the source. The
assignment `b = a` copies both of `a`'s fields into `b`; the subsequent change to `b.x`
then affects only `b`'s own copy, `a.x` is still `3`. This shows how the
parameter-copying rule measured in the previous lesson works for a struct: when a struct
variable is assigned to another or passed to a function, what is transferred is a copy of
**every field** — unlike an array, a struct does not decay into the address of its first
element when passed to a function, it is copied whole, with all its fields.

The order of fields in memory is also determined by the declaration order in the source:
`struct Point`'s `x` field is always laid out before `y`, because the field written first
in the declaration comes first. A struct can also hold another struct as a field — like a
`struct Line { struct Point start; struct Point end; };` carrying `struct Point` fields —
and this nesting carries the same rule in layers: each inner struct's own fields are laid
out, according to declaration order again, inside the outer struct's own field. This
layout decision, just like an array's element count, is fixed by the compiler without the
program ever running — which field sits at which address is determined purely from the
declaration itself, without ever looking at data at runtime. No real address or byte
offset is printed in this course, but that it is a consequence of declaration order shows
why a struct definition determines both the data's **meaning** and its **layout** in
memory at the same time.

#### Union: Sharing the Same Memory

A union defines several fields in the **same** memory area — each field corresponds to
the same address, under a different type interpretation. The union itself does not track
which field is "valid"; this information must be carried separately, with a **tag**
field.

```c
// union.c — a union sharing the same memory, used safely with a tagged read
#include <stdio.h>

enum Kind { INTEGER, DECIMAL };

struct TaggedValue {
    enum Kind kind;
    union {
        int whole;
        float decimal;
    } value;
};

static struct TaggedValue make_integer(int v) {
    struct TaggedValue t;
    t.kind = INTEGER;
    t.value.whole = v;
    return t;
}

static struct TaggedValue make_decimal(float v) {
    struct TaggedValue t;
    t.kind = DECIMAL;
    t.value.decimal = v;
    return t;
}

static void print_value(struct TaggedValue t) {
    if (t.kind == INTEGER) {
        printf("%s %d\n", "read as an integer", t.value.whole);
    } else {
        printf("%s %.2f\n", "read as a decimal", t.value.decimal);
    }
}

int main(void) {
    struct TaggedValue first = make_integer(42);
    struct TaggedValue second = make_decimal(3.5f);
    print_value(first);
    print_value(second);
    return 0;
}
```

```
read as an integer 42
read as a decimal 3.50
```

**BS13.** `whole` and `decimal` inside the `union` share the same memory area — once one
is written, the other's old value becomes meaningless, because both read the same bytes
through a different type lens. `make_integer` writes only `value.whole`, `make_decimal`
only `value.decimal`; `print_value` always looks at the `kind` field to learn which
interpretation is valid and reads only that field. Thanks to this discipline, the lesson
never reads the field that was **not** written — `first` always reads `whole`, `second`
always reads `decimal`. The `union` inside `TaggedValue` (the `value` field) does not
know on its own which member was last written; carrying this information is the `kind`
field's job, and keeping that field correct is the source writer's job.

How much space a union will occupy is also fixed at compile time: the measure the
standard sets is that enough space is allocated to carry the union's **largest** field —
because any field must be writable at any moment. Since no real size is printed in this
course, this lesson does not measure this, but the difference between how the two
declarations grow must be noted: a struct lays its fields one after another, a union
overlaps them. This is why adding a field smaller than the largest to a union generally
does not grow the total space; adding one to a struct grows it unless the field fits into
existing padding. In both cases, alignment has the final word: the standard states that
unnamed padding may appear at the end of both a struct and a union, so the total space
cannot be read directly from the fields' sizes.

#### Enumeration: A Compile-Time Constant

An enumeration declaration produces named integer constants; these constants can be used
everywhere a constant expression written in the source can be used — for instance, to
determine an array's length.

```c
// stage.c — enumeration constant (excerpt), used as a compile-time constant
#include <stdio.h>

enum Color { RED = 1, GREEN = 2, BLUE = 4 };

int main(void) {
    printf("%s %d\n", "enumeration constant", (int)BLUE);

    int colors[BLUE];
    printf("%s %d\n", "element count of the array built with the constant",
           (int)(sizeof colors / sizeof colors[0]));
    return 0;
}
```

```
enumeration constant 4
element count of the array built with the constant 4
```

The value of `BLUE` comes from the constant `4` written in the `enum Color` declaration,
and this value is known without the program ever running. The declaration `int
colors[BLUE];` confirms this: `BLUE` is an expression of the kind constant folding, as
measured in the Arrays and Strings lesson, accepts — had it been a variable or a function
call, the array's size could not have been determined at compile time. The `colors`
array's element count, without any real size being printed, is again measured with the
`sizeof` ratio and comes out `4` — the exact same number as `BLUE`, because both derive
from the same compile-time constant.

Explicit values like `RED = 1, GREEN = 2, BLUE = 4` in the `enum Color` declaration are
not mandatory; an enumeration can also be declared with no value written at all, in which
case the first name starts from `0` and each subsequent name takes a value one greater
than the one before. The declaration here writes its values explicitly, because `RED`,
`GREEN` and `BLUE` represent separate bit positions (`1`, `2`, `4`), which lets colors be
combined with a bitmask — an expression like `RED | BLUE` can produce the value `5`,
representing both colors at once. This usage, too, is a property fixed at compile time:
which constant corresponds to which bit position comes from the `=` sign written in the
source, with no data at runtime looked at.

#### The Bounding Measurement

A union does not track on its own which of its fields was last written — carrying this
information is not the language's job, but the source writer's. The `TaggedValue` struct
above resolves this with a tag field: `kind` tells which member of `value` is valid, and
`print_value` always looks at `kind` first.

If this discipline were abandoned — if `print_value` never looked at `kind` and, say,
always read `value.decimal` — then for `first` (which `make_integer` had written only
`value.whole` into), the byte pattern read would have been written as an `int` and read
as a `float`. This shows that the only thing a union guarantees is **sharing the same
address**, not which type interpretation is meaningful — the result of a mismatched
interpretation is an unspecified bit pattern, and this course's general rule does not try
it in a code block. The defined counterpart set alongside it is the tagged read above
itself: every read learns which member was last written to that union from an
independent field (`kind`) and reads only that member, and this measurement produces the
same two lines every time it is run.

For the tagged read to work, the tag must always be updated together with the
corresponding write — both `make_integer` and `make_decimal` set `kind` before writing
their own field. If a third function wrote `value.whole` and forgot to update `kind`,
`print_value` reading that variable would still look at the stale `kind` value and read
the wrong member — a union can neither produce nor prevent this inconsistency on its own,
because the union itself only provides memory sharing, not staying in sync with a tag.
This reflects a deliberate design decision in C's union mechanism: the language does not
take on the cost of tracking field selection — it does not separately record which field
is active on every write — and leaves this responsibility, just as with array length
information, entirely to the source writer.

This also shows that a struct and a union are two complementary mechanisms: a struct
allocates separate memory for each field and makes all of them valid at once, while a
union shares a single memory area and makes only one valid. `TaggedValue` combines the
two — the outer struct carries both the tag and the shared field, and thanks to the tag
field, the information a union does not track ends up kept at the struct level.

This combination forms the basis of a widespread pattern in C called a "tagged union": a
source writer who wants to model a value carrying only one of several possible shapes
uses these three mechanisms — struct, union and enumeration — together. The enumeration
expresses which shape is valid with named constants; the union has those shapes share a
common memory area; the struct carries the two together as a single value. The three
mechanisms measured in this lesson are therefore not three independent subjects, but
three components of the same pattern.

#### Summary

- A struct's fields, together with their names and types, are fixed at **compile time**;
  when a struct variable is assigned to another, every field is copied at once.
- A union defines several fields at the same memory address; the language does not track
  which field was last written, and the source writer must carry this information
  separately (with a tag field).
- Enumeration constants (like `BLUE` inside `enum Color`) are **compile-time constants**;
  they can be used everywhere a constant expression is required, such as determining an
  array's length — class **compiler**.
- Reading a member not written into a union (untagged, under a wrong type assumption)
  reinterprets the bytes as another type; C does not forbid this, but the value of bytes
  not corresponding to the last-written member is unspecified, and if the pattern read is
  a trap representation, the behavior is undefined. This is not run in this course; the
  defined counterpart is the tagged read.
- A struct keeps every field in separate memory and makes all of them valid at once,
  while a union shares a single memory area and makes only one valid — two complementary
  mechanisms.
- When used together, enumeration, union and struct form the "tagged union" pattern: the
  enumeration names which shape is valid, the union shares the shapes' memory, the struct
  combines the two into a single value.

#### Next Step

Every decision measured in this topic stayed within a single run of a program: a type's
range, a name's scope, an operator's choice, a branch's chosen path, a call's bound body,
an array's length, a union's valid field — all of it could be read either from the source
or from that run's own data. The Input and Output lesson adds one last question to this
ground: at which stage is the data a program takes in from outside, or writes out to the
outside, interpreted? Every placeholder in a format string must match the type of its
corresponding argument, but the compiler can only catch this match under certain
conditions — the language itself does not guarantee it. In the `TaggedValue` example
measured in this lesson, keeping the tag correct was the source writer's job; in the next
lesson, keeping the format string consistent with its arguments will likewise be the
source writer's responsibility.

The seven lessons measured in this topic each touched every one of the four classes
(preprocessor, compiler, linker, runtime) at least once: Data Types and Operators at the
compiler, Control Flow at runtime, Variables and Functions at the linker, Arrays and
Strings at both, and this lesson landed at the compiler once again. The closing lesson
gathers the decisions these seven lessons measured into a single table, and adds input
and output's own share to that table — format strings being interpreted at runtime.

### Input and Output

Source: https://academia.sh/en/courses/c-fundamentals/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.

