---
title: 'The Place of C'
source: 'https://academia.sh/en/courses/c-fundamentals/the-place-of-c'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines and distinguishes the four compilation stages (preprocessor, compiler, linker, runtime)'
    - 'Verifies the distribution of fourteen observations across the stages by running the three-file measurement core'
    - 'Shows through examples at which stage the same kind of decision is made in the source'
---

# 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.
