Multi-File Projects & Makefiles

Course 2 · Ch 5
Multi-File Projects & Makefiles
Resolving Chapter 1's own open question: what fills the gap where cargo would be

c1-1 named the gap directly — no official build tool. c1-5 introduced header files in passing. c2-4 explained exactly what #include does. This chapter puts all three together, for real, on a genuine multi-file project.

Why Split a Project Across Files

Organization at real scale, and something more practical: faster rebuilds. Recompiling one changed file is far cheaper than recompiling an entire project from scratch every time — but only if the build process actually takes advantage of that.

Header Files as Contracts

A header declares what a source file provides, without exposing how. Per c2-4's literal explanation, those declarations get pasted, verbatim, into every file that includes the header — giving each one enough information to call the functions without ever seeing their implementation.

// math_utils.h int add(int a, int b);

Separate Compilation

This is c1-1's compile-then-link model, made concrete across multiple files. Each .c file compiles independently into its own object file (.o); only afterward are all the object files linked into one executable.

A Worked Example

// math_utils.c #include "math_utils.h" int add(int a, int b) { return a + b; } // main.c #include <stdio.h> #include "math_utils.h" int main() { printf("%d\n", add(2, 3)); return 0; }
$ gcc -c math_utils.c -o math_utils.o $ gcc -c main.c -o main.o $ gcc math_utils.o main.o -o program $ ./program 5

The Linking Step, In Detail

main.o contains a call to add() with no body — an unresolved external symbol. math_utils.o contains add()'s actual definition. The linker's job is matching them up. Forget to link math_utils.o in at all, and the result is one of C's most recognizable real-world errors:

$ gcc main.o -o program undefined reference to `add' collect2: error: ld returned 1 exit status

A Real Makefile

make is the unofficial-but-standard answer to c1-1's own open question — targets, dependencies, and rules, rebuilding only what actually changed.

program: main.o math_utils.o gcc main.o math_utils.o -o program main.o: main.c math_utils.h gcc -c main.c -o main.o math_utils.o: math_utils.c math_utils.h gcc -c math_utils.c -o math_utils.o clean: rm -f *.o program

Run make, and it rebuilds only the files whose dependencies (listed after the colon) are newer than their target — editing only math_utils.c recompiles just that file and re-links, leaving main.o untouched.

ConceptRust / cargoC / make
Build toolofficial, built inunofficial — make/CMake fill the gap
Incremental rebuildsautomaticexplicit — dependency rules must be written correctly
Unresolved symbolcompile error — caught before linking exists as a concepta distinct linker error — "undefined reference"
Recognize "undefined reference" as a linker error
It means the declaration was found (the header compiled fine) but the definition was never linked in — a genuinely distinct failure mode from a compile error, worth recognizing immediately rather than re-reading the source for a typo that isn't there.
An incomplete dependency list silently builds stale files
If a Makefile rule doesn't list a header as a dependency, changing that header won't trigger a rebuild of files that include it — make will report success while quietly building against outdated object files. Always list every header a .c file includes as part of its rule's dependencies.

Coding Challenges

Challenge 1

Create the three files from this chapter's worked example (math_utils.h, math_utils.c, main.c), compile and link them manually with separate gcc -c and gcc linking commands, and run the result.

📄 View solution
Challenge 2

Deliberately compile only main.c into an executable, omitting math_utils.o from the final link command. Report the exact error and explain what it means.

📄 View solution
Challenge 3

Write a Makefile for the three-file project from Challenge 1, following the chapter's own pattern. Explain what happens, in terms of which files actually get recompiled, if you run make twice in a row with no changes in between.

📄 View solution

Chapter 5 Quick Reference

  • A header declares what a source file provides — a contract, not an implementation
  • gcc -c file.c -o file.o — compiles independently into an object file
  • Linking matches unresolved calls (in one .o) against real definitions (in another)
  • "undefined reference" — a linker error, distinct from a compile error; the definition was never linked in
  • make — the unofficial standard build tool, rebuilding only what its dependency rules say changed
  • Every header a source file includes must be listed as a dependency, or changes to it won't trigger a rebuild
  • Next chapter: File I/O — fopen/fread/fwrite/fclose, text vs. binary mode