After sharpening your low-level instincts with bitwise operations, it's time to split real projects into multiple files and automate the build process. Instead of typing clang main.c utils.c -o app every time, a Makefile (a file that automates your build commands) recompiles only the files that changed and shortens your command history.
New Terms in This Lesson
- Makefile: The script of build rules read by
make - Target: The thing you want to build (executables, object files, etc.)
- Dependency: The inputs required to build a target
- Rule: The set of commands that produce a target
Key Ideas
Study Notes
- Estimated time: Around 70 minutes
- Prereqs: Experience splitting headers/sources and running
clangorgccfrom a terminal- Goal: Design a simple project tree, write a Makefile, and understand incremental builds
As projects grow, splitting files by responsibility and minimizing repetitive work becomes crucial. Using directories like src, include, and build keeps collaboration organized, and a Makefile remembers how to rebuild only the necessary pieces.
Think of the workflow like this:
- Manually run
clang main.c stats.c -o apponce. - Get tired of repeating that command.
- Encode the steps in a Makefile so
makeruns them for you.
We'll walk through:
- Designing a baseline folder structure (
src,include,build) - Compiling individual
.cfiles into object files before linking - Understanding Makefile target/dependency/command syntax
- Using phony targets and a
cleanrule to tidy up
Code Walkthrough
Sample Project Layout
project/
├── include/
│ └── stats.h
├── src/
│ ├── main.c
│ └── stats.c
└── Makefile
stats.h keeps declarations and structs, stats.c holds the implementation, and main.c includes the header to use the functions.
include/stats.h
#ifndef STATS_H
#define STATS_H
#include <stddef.h>
typedef struct {
double min;
double max;
double average;
} Stats;
Stats calculate_stats(const int *values, size_t length);
#endif
src/stats.c
#include "stats.h"
Stats calculate_stats(const int *values, size_t length) {
Stats result = {0};
size_t i;
int sum = 0;
if (length <= 0) {
return result;
}
result.min = values[0];
result.max = values[0];
for (i = 0; i < length; i++) {
if (values[i] < result.min) {
result.min = values[i];
}
if (values[i] > result.max) {
result.max = values[i];
}
sum += values[i];
}
result.average = (double)sum / length;
return result;
}
src/main.c
#include <stdio.h>
#include "stats.h"
int main(void) {
int data[5] = {10, 40, 30, 25, 60};
Stats stats = calculate_stats(data, 5);
printf("min=%.0f max=%.0f avg=%.2f\n",
stats.min, stats.max, stats.average);
return 0;
}
Writing the Makefile
Makefile
CC = clang
CFLAGS = -Wall -Wextra -std=c11 -Iinclude
BUILD_DIR = build
SRCS = src/main.c src/stats.c
OBJS = $(SRCS:src/%.c=$(BUILD_DIR)/%.o)
TARGET = $(BUILD_DIR)/stats-app
.PHONY: all clean run
all: $(TARGET)
$(TARGET): $(OBJS)
@mkdir -p $(BUILD_DIR)
$(CC) $(OBJS) -o $(TARGET)
$(BUILD_DIR)/%.o: src/%.c include/stats.h | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR):
@mkdir -p $(BUILD_DIR)
run: $(TARGET)
./$(TARGET)
clean:
rm -rf $(BUILD_DIR)
SRCS:src/%.c=$(BUILD_DIR)/%.o rewrites src/foo.c into build/foo.o. This kind of % rule is called a pattern rule: it describes a reusable shape instead of one file at a time. The | $(BUILD_DIR) syntax states that the build directory must exist before the recipe runs, but it does not trigger rebuilds when the directory changes.
Whenever you see target: dependencies, read it as “To make this result, I need those files.”
Observing Incremental Builds
- Running
makecompilessrc/main.candsrc/stats.cintobuild/main.oandbuild/stats.o, then links them intobuild/stats-app. - If you edit only
src/main.cand rerunmake, it recompiles justmain.oand reusesstats.o. make runbuilds (if necessary) and executes the binary.make cleandeletes the entirebuilddirectory.
Why It Matters
- Separating files by role improves reuse and testing.
- Make tracks dependencies and rebuilds only what's needed, saving time.
- Variables like
CCandCFLAGSkeep the build configurable. - A disciplined structure prepares you for the capstone-sized example in the next part.
On real projects, you may later add -MMD -MP for automatic header dependency tracking. For now, it's enough to internalize that "Makefile remembers the build order."
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.
💬 댓글
이 글에 대한 의견을 남겨주세요