Frontend Labs
⋯

13. Multi-file programs: headers, static, translation units, and make

13.1 static at file scope: C's only real access-control keyword

// counter.c
#include "counter.h"

static int count = 0;              // static at file scope = "private to this .c file";
                                    // no other translation unit can link against `count`
static void log_change(void) {     // static function = private helper, invisible outside this file
    // ...
}

int counter_increment(void) {
    count++;
    log_change();
    return count;
}
// counter.h
#ifndef COUNTER_H
#define COUNTER_H
int counter_increment(void);
#endif

This static-for-privacy convention is exactly how C fakes "encapsulation": the header exposes only what callers need (counter_increment), everything else is invisible outside the .c file.

13.2 A real multi-file make setup

project/
├── Makefile
├── include/
│   └── counter.h
└── src/
    ├── counter.c
    └── main.c
CC      = clang
CFLAGS  = -std=c17 -Wall -Wextra -Wpedantic -Werror -g -Iinclude
SRCS    = $(wildcard src/*.c)
OBJS    = $(SRCS:src/%.c=build/%.o)
BIN     = build/app

all: $(BIN)

$(BIN): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

build/%.o: src/%.c | build
	$(CC) $(CFLAGS) -c $< -o $@

build:
	mkdir -p build

clean:
	rm -rf build

.PHONY: all clean

Each .c file compiles to its own .o independently (that's a "translation unit") — the linker then combines them, resolving calls between them by symbol name. This is why one file's compile error doesn't require recompiling everything else, and why static symbols in one file are invisible to the linker when resolving another.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36