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
- Follow along: Create the exact tree above and run
makeandmake runto verify the output. - Extend: Add a standard deviation function in
stats.c, append it toSRCS, and rebuild. - Debug: Remove header dependencies from the pattern rule and observe how
makefails to rebuild when you changestats.h. - Completion check: Manage a project with at least two
.cfiles usingmakefor build/run/clean.
Wrap-Up
Build automation is more than convenience—it shrinks the feedback loop between editing code and seeing results. Next up, we'll combine pointers, structs, files, and build tooling into a text-based system utility.
💬 댓글
이 글에 대한 의견을 남겨주세요