C Part I · Module 11 · Building Multi-File C Programs
Building a Project: Objects, Libraries and Make
Four of the previous chapter's link failures were not problems with anyone's C. A file left off the command line, an object listed twice, a library missing or in the wrong place — each is a statement about how the build was invoked, and every build in this module so far has been invoked by typing compiler commands by hand. That stops working at about three files, and it has already cost something: an object file is stale when any header it included has changed, and working that set out by hand is both tedious and wrong in a particular way that produces object files which disagree with each other and link anyway. This chapter assembles a real project, writes the rule that decides what to rebuild, measures what happens when that rule is incomplete, and then turns the result into something another project can use.
Intermediate19 min readCPart IFundamentalsBuild
Part I · Module 11 · Chapter 11.10 · Building a Project: Objects, Libraries and Make
1. A Real Project
Four files, following every rule from this module: a header declaring an interface, a source file defining it, and a second module that uses the first.
#ifndef MYPROJ_STATS_H
#define MYPROJ_STATS_H
#include <stddef.h> /* self-contained: it uses size_t */
int stats_sum(const int *v, size_t n);
double stats_mean(const int *v, size_t n);
#endif#include "stats.h" /* first — proves the header stands alone */
int stats_sum(const int *v, size_t n)
{
int s = 0;
for (size_t i = 0; i < n; i++) { s += v[i]; }
return s;
}
double stats_mean(const int *v, size_t n)
{
return n == 0 ? 0.0 : (double)stats_sum(v, n) / (double)n;
}#ifndef MYPROJ_REPORT_H
#define MYPROJ_REPORT_H
#include <stddef.h>
void report_print(const int *v, size_t n);
#endif#include "report.h"
#include "stats.h"
#include <stdio.h>
void report_print(const int *v, size_t n)
{
printf("n=%zu sum=%d mean=%.2f\n", n, stats_sum(v, n), stats_mean(v, n));
}#include "report.h"
int main(void)
{
int values[] = { 4, 8, 15, 16, 23, 42 };
report_print(values, sizeof values / sizeof values[0]);
return 0;
}Every decision there is a rule from an earlier chapter: prefixed guards and self-contained headers (11.3), the header included first in its own .c (11.3 §6), declarations in .h and definitions in .c (11.1, 11.3 §4), const on every read-only pointer parameter (7.11, 9.8), a pointer paired with a count (7.7), and sizeof v / sizeof v[0] used only in the scope that declares the array (10.6 §5).
2. The Build, by Hand
$ cc -std=c17 -Wall -Wextra -Wpedantic -O2 -c stats.c -o stats.o
$ cc -std=c17 -Wall -Wextra -Wpedantic -O2 -c report.c -o report.o
$ cc -std=c17 -Wall -Wextra -Wpedantic -O2 -c main.c -o main.o
$ cc stats.o report.o main.o -o prog
$ ./prog
n=6 sum=108 mean=18.00Four commands for three files, and the flags repeated three times. The problems are immediate: it does not scale, the flags drift apart, and — the one that matters — nothing records which objects are out of date.
3. A Makefile
CC = cc
CFLAGS = -std=c17 -Wall -Wextra -Wpedantic -Wundef -O2
OBJS = main.o report.o stats.o
prog: $(OBJS)
$(CC) $(OBJS) -o prog
main.o: main.c report.h
report.o: report.c report.h stats.h
stats.o: stats.c stats.h
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) prog libreport.aFour things are going on, and only the third is really the point.
A rule is a target, its prerequisites, and a recipe. prog: $(OBJS) says prog depends on the three objects; the indented line builds it. The indentation must be a tab.
%.o: %.c is a pattern rule with $< meaning the first prerequisite and $@ the target — so one recipe covers every object.
The three middle lines are the dependency list, and they are the chapter's subject. They say which headers each object depends on, which is exactly Chapter 11.8 §6's staleness rule written down.
CFLAGS is in one place, so every object gets the same flags — including -Wundef, which Chapter 11.7 §3 measured as catching a silently disabled feature and which is not in -Wall -Wextra.
$ make
cc -std=c17 -Wall -Wextra -Wpedantic -Wundef -O2 -c main.c -o main.o
cc -std=c17 -Wall -Wextra -Wpedantic -Wundef -O2 -c report.c -o report.o
cc -std=c17 -Wall -Wextra -Wpedantic -Wundef -O2 -c stats.c -o stats.o
cc main.o report.o stats.o -o prog
$ ./prog
n=6 sum=108 mean=18.004. What Gets Rebuilt
A target is rebuilt when any prerequisite is newer than it. That is the entire rule, applied recursively.
Change one source file:
$ touch stats.c && make
cc ... -c stats.c -o stats.o
cc main.o report.o stats.o -o progOne compile, one link. main.o and report.o were untouched.
Change a header:
$ touch stats.h && make
cc ... -c report.c -o report.o
cc ... -c stats.c -o stats.o
cc main.o report.o stats.o -o progBoth files that include stats.h were recompiled, and main.o — which does not include it — was not. This is Chapter 11.2 §4's "a header is text in every unit that includes it", enforced by the build.
Change nothing:
$ make
make: `prog' is up to date.5. The Dependency List Is Where Builds Go Wrong
Now the measurement that matters. Remove stats.h from one line — leaving a dependency list that is almost right:
report.o: report.c report.h /* stats.h omitted */$ touch stats.h && make
cc ... -c stats.c -o stats.o
cc main.o report.o stats.o -o progreport.o was not rebuilt, although report.c includes stats.h.
Nothing reported it. The link succeeded, because Chapter 11.9 §1 established that symbols match by name and report.o still wants and provides exactly the same names. If that header change had added a member to a structure, you would now have two object files disagreeing about its layout — Chapter 10.5's offsets computed two different ways in one program — and the only symptom would be wrong data.
A hand-written dependency list is a place where a correct program is silently turned into an incorrect one. It is also the part of a Makefile that rots fastest: every new
#includeis a line somebody has to remember to update.
The fix is to stop writing them:
$ cc -std=c17 -MM report.c
report.o: report.c report.h stats.h
$ cc -std=c17 -MM main.c
main.o: main.c report.hThe compiler emits exactly the line the Makefile needed — it knows, because it just preprocessed the file and saw every include. Real projects use -MMD -MP to write these to .d files during the normal compile and include them back, so the dependency list is regenerated every time the source changes and can never drift.
6. Static Libraries
A static library is an archive of object files with an index.
$ ar rcs libreport.a report.o stats.o
$ ar t libreport.a
__.SYMDEF SORTED
report.o
stats.olibreport.a 2256 bytes
report.o 888 bytes
stats.o 1064 bytesThe archive is its members plus a symbol index — __.SYMDEF, the table mapping each symbol to the member that defines it. r replaces members, c creates the archive, s writes the index.
Linking against it works exactly as linking the objects did:
$ cc main.o libreport.a -o prog_lib
$ ./prog_lib
n=6 sum=108 mean=18.00So what does it add?
One name instead of many. libreport.a travels as a unit. A consumer does not need to know it contains two object files, or which.
Selective extraction. The linker pulls in only the members that something actually wants. Link a program that calls stats_sum and never uses report_print, and report.o is not included in the executable. A directory of loose object files cannot do that — listing them all links them all.
A distributable interface. Headers plus an archive is how a library is shipped. The consumer gets the declarations and the compiled definitions, and never sees the source.
What it does not add: anything at run time. A static library's members are copied into your executable at link time, so the archive is not needed afterwards. Shared libraries behave differently and need an operating-system model this part of the curriculum does not build.
One honest note about link order. Chapter 11.9 §4 listed archive order as a cause of undefined symbols: a single-pass linker extracts only members that are already wanted when it reaches the archive, so a library listed before the object needing it can be skipped. On the machine used for these measurements, the "wrong" order linked anyway:
$ cc libreport.a main.o -o ok2
$ (linked)So this platform did not reproduce the restriction. It is real on other toolchains — notably GNU ld — and the convention costs nothing:
Put objects first and libraries last, with libraries ordered so that each comes after everything that uses it.
7. A Build Worth Copying
CC = cc
CFLAGS = -std=c17 -Wall -Wextra -Wpedantic -Wundef -O2 -MMD -MP
OBJS = main.o report.o stats.o
DEPS = $(OBJS:.o=.d)
prog: $(OBJS)
$(CC) $(OBJS) -o prog
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(DEPS) prog libreport.a
-include $(DEPS)Two changes from §3, and they remove the only part that could be wrong.
-MMD -MP added to CFLAGS. Each compile now writes a .d file containing the generated prerequisite list from §5.
-include $(DEPS) at the end. Those lists are read back in, so the hand-written dependency lines are gone entirely. The leading - means "do not complain if they do not exist yet", which is the state on a first build.
Three habits to go with it:
One set of flags, in one variable, so no file is compiled differently from its neighbours.
A clean target that removes everything the build produced, including the .d files.
Build with warnings on and treat the ones in this curriculum as errors to fix, not noise to scroll past. -Wall -Wextra -Wpedantic -Wundef is the set this module has justified by measurement.
8. What You Should Be Able to Say Now
- A project is headers declaring interfaces, source files defining them, compiled to objects and linked once.
- Building by hand does not scale and, critically, records no staleness information.
- A make rule is target, prerequisites, recipe. A target is rebuilt when any prerequisite is newer.
- Measured:
touch stats.crebuilt one object;touch stats.hrebuilt both objects that include it; no change produced "is up to date". - Measured: with
stats.homitted from a prerequisite list,report.owas not rebuilt even thoughreport.cincludes it — silently, because the symbols still match. - A stale object linked against fresh ones can disagree about a structure's layout, with no diagnostic.
cc -MMprints exactly the prerequisite line needed;-MMD -MPwrites it during the normal compile.- Let the compiler generate dependencies. A hand-maintained list is correct until the next
#include. - When a build behaves inexplicably, do a clean build first.
- A static library is object files plus a symbol index — measured:
__.SYMDEF SORTED,report.o,stats.o. - It adds one name, selective extraction, and a distributable interface; it adds nothing at run time.
- Objects first, libraries last — this platform linked either order, other toolchains do not.
9. Exercises
Exercise 1 — write the dependency lines. Given a.c including a.h and common.h, b.c including b.h and a.h, and main.c including a.h and b.h, write the three prerequisite lines. Then give the command that would have produced them for you.
Exercise 2 — predict the rebuild. Using Exercise 1's project, say what is recompiled when each of these changes: a.c, a.h, common.h, main.c, the Makefile's CFLAGS.
Exercise 3 — find the build bug. A project's Makefile lists parser.o: parser.c parser.h. parser.c also includes tokens.h. Describe what happens when a struct in tokens.h gains a member and only some files are rebuilt, which chapter explains why nothing warns, and two different fixes.
Exercise 4 — write a Makefile. For a project of app.c, net.c, net.h, log.c, log.h where net.c and app.c both use logging, write a complete Makefile using the §7 pattern. Include clean.
Exercise 5 — make the archive. Give the commands to build libnet.a from net.o and log.o, list its contents, and link app.o against it. Say what the linker does if app.c never calls anything in log.c.
Exercise 6 — order the link line. A program uses libapp.a, which uses libutil.a, which uses the maths functions. Give the link command in the correct order and say what symptom the wrong order produces on a toolchain that enforces it.
Exercise 7 — diagnose the impossible. A colleague deletes a function, rebuilds, and the linker still reports a duplicate symbol for it. Give the most likely cause, the one command that confirms it, and the chapter that explains why the symbol persisted.
Exercise 8 — justify the flags. For each of -Wall, -Wextra, -Wpedantic, -Wundef, name one defect from this module that it catches, with the chapter.
10. Summary
- A project is headers for interfaces, sources for definitions, objects, one link.
- A target is rebuilt when any prerequisite is newer — that is the whole rule.
- Measured:
stats.c→ one rebuild;stats.h→ both includers; no change → "up to date". - Measured: an omitted prerequisite left a stale object behind, silently — and a stale object can disagree about a structure's layout with no diagnostic.
cc -MMgenerates the dependency line;-MMD -MPdoes it during the normal compile. Never maintain it by hand.- Clean build first when a build behaves inexplicably.
- A static library is members plus an index — one name, selective extraction, a shippable interface, nothing at run time.
- Objects first, libraries last.
- One flag set in one variable, with
-Wall -Wextra -Wpedantic -Wundef.
11. Closing Module 11
This module answered a question the first ten never raised: how does a program made of many files become one program? The answer turned out to rest on a single distinction drawn in Chapter 11.1 — a declaration names, a definition creates — and almost everything after it was a consequence. Headers hold declarations because declarations may repeat. Definitions go in one .c because a definition may not. extern declares, static makes a definition private, and the whole header discipline exists to put a declaration and its definition into the same translation unit, where something checks them.
The thread running underneath is less obvious and more useful: each stage of the build knows less than the one before it, and an error can only be reported by a stage that had the information to notice it. That sentence explains the measurements that were otherwise just surprising — why a type mismatch across two files compiles twice and links, why an include guard does nothing about two translation units, why a dead #if branch can contain anything at all, and why a linker error names a symbol rather than a line. None of those is a tool being unhelpful. Each is a stage being asked about something it cannot see.
Three habits carry forward. Put every shared declaration in a header and include it in the defining file too, which is the only way to make a cross-file mistake visible to a compiler. Default to static, so the interface is what you chose rather than what you forgot to hide. And let the compiler compute your dependencies, because the one list a human maintains is the one that will be wrong.
Part I now has eleven modules and a conspicuous gap. Every program in this curriculum has printed with printf and allocated with malloc — two functions from a library that has rather more than two, and that you have been using since Chapter 1.4 without ever being told what is in it. That library is not an add-on. It is specified by the same standard as the language, it is the only portable way to do input, output, sorting, searching, time or string conversion, and a great deal of C that people write by hand is code the library already contains, tested more thoroughly than yours will be. The next module is a working tour of it: what is guaranteed to be there, what each header is for, which functions are worth knowing by name, and — in keeping with everything this module has been about — exactly what each one assumes about the arguments you hand it.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
