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.