Skip to content
VLSI Mentor

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.

Azvya Education Pvt. Ltd.VLSI Mentor
stats.h
#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
Azvya Education Pvt. Ltd.VLSI Mentor
stats.c
#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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
report.h
#ifndef MYPROJ_REPORT_H
#define MYPROJ_REPORT_H
#include <stddef.h>
void report_print(const int *v, size_t n);
#endif
Azvya Education Pvt. Ltd.VLSI Mentor
report.c
#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));
}
Azvya Education Pvt. Ltd.VLSI Mentor
main.c
#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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.00

Four 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

Azvya Education Pvt. Ltd.VLSI Mentor
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.a

Four 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.00

4. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ touch stats.c && make
cc ... -c stats.c -o stats.o
cc main.o report.o stats.o -o prog

One compile, one link. main.o and report.o were untouched.

Change a header:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 prog

Both 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    report.o: report.c report.h           /* stats.h omitted */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ touch stats.h && make
cc ... -c stats.c -o stats.o
cc main.o report.o stats.o -o prog

report.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 #include is a line somebody has to remember to update.

The fix is to stop writing them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.h

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ar rcs libreport.a report.o stats.o
$ ar t libreport.a
__.SYMDEF SORTED
report.o
stats.o
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
libreport.a    2256 bytes
report.o        888 bytes
stats.o        1064 bytes

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc main.o libreport.a -o prog_lib
$ ./prog_lib
n=6 sum=108 mean=18.00

So 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

  1. A project is headers declaring interfaces, source files defining them, compiled to objects and linked once.
  2. Building by hand does not scale and, critically, records no staleness information.
  3. A make rule is target, prerequisites, recipe. A target is rebuilt when any prerequisite is newer.
  4. Measured: touch stats.c rebuilt one object; touch stats.h rebuilt both objects that include it; no change produced "is up to date".
  5. Measured: with stats.h omitted from a prerequisite list, report.o was not rebuilt even though report.c includes it — silently, because the symbols still match.
  6. A stale object linked against fresh ones can disagree about a structure's layout, with no diagnostic.
  7. cc -MM prints exactly the prerequisite line needed; -MMD -MP writes it during the normal compile.
  8. Let the compiler generate dependencies. A hand-maintained list is correct until the next #include.
  9. When a build behaves inexplicably, do a clean build first.
  10. A static library is object files plus a symbol index — measured: __.SYMDEF SORTED, report.o, stats.o.
  11. It adds one name, selective extraction, and a distributable interface; it adds nothing at run time.
  12. 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 -MM generates the dependency line; -MMD -MP does 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.