Header Files and Linking in C: Build a Multi-File Program Step by Step

Build and debug a three-file C program while learning where declarations, definitions, external symbols, and storage belong.

KnowledgeGate Team

Exam prep & CS education

Updated 18 Aug 20266 min read

Your one-file C program compiles, then the first multi-file project fails with an undefined reference or multiple definition. The missing piece is a clear model of preprocessing, compilation, and linking. Three files make that model concrete: stats.h carries the declarations, stats.c carries the definitions, and main.c calls across the boundary. Once you can name the stage that owns a symbol, undefined reference to 'sum' stops being a mystery and turns into a checklist.

Header files and linking in C: the build pipeline

A header is an interface shared by translation units, not a separately compiled program. After preprocessing, each .c file is one translation unit. The compiler produces an object file, then the linker combines objects and libraries into an executable.

This line is a declaration:

double mean(const int values[], size_t count);

This declaration gives the compiler a type contract. The body in stats.c is the definition, which supplies code for the linker. Tokenisation and parsing happen earlier. Lexical Analysis in Compiler Design: Tokens, Patterns and Lexemes explains that stage, but lexical analysis does not perform linking.

A safe C header: declarations, guards, and include style

Create stats.h exactly like this:

#ifndef STATS_H
#define STATS_H

#include <stddef.h>

int sum(const int values[], size_t count);
double mean(const int values[], size_t count);

#endif

#ifndef STATS_H tests the guard name, and #define STATS_H records this inclusion. <stddef.h> provides size_t. The prototypes publish the interface, and #endif closes the condition. The guard stops repeated processing inside one translation unit. It cannot stop two object files from defining the same external symbol.

Use angle brackets for implementation or configured-path headers, as in #include <stddef.h>. Use quotes for project headers, as in #include "stats.h". Avoid machine-specific absolute paths.

Put in a header

Put in a .c file

Function and type declarations

Ordinary function bodies

Macros

Storage definitions

Carefully chosen static inline helpers

Private implementation details

If these language foundations still feel new, the Coding & Skill Development Courses category includes a dedicated C learning track.

Save the implementation as stats.c:

#include "stats.h"

int sum(const int values[], size_t count) {
    int total = 0;
    for (size_t i = 0; i < count; ++i) {
        total += values[i];
    }
    return total;
}

double mean(const int values[], size_t count) {
    return count == 0 ? 0.0 : (double) sum(values, count) / count;
}

Save the caller as main.c:

#include <stdio.h>
#include "stats.h"

int main(void) {
    const int scores[] = {72, 85, 91, 68};
    const size_t count = sizeof scores / sizeof scores[0];

    printf("sum=%d\n", sum(scores, count));
    printf("mean=%.2f\n", mean(scores, count));
    return 0;
}

Check the calculation before running it: 72 + 85 = 157, 157 + 91 = 248, 248 + 68 = 316, and 316 / 4 = 79.00. The output is:

sum=316
mean=79.00

Build through the compiler driver:

cc -std=c17 -Wall -Wextra -pedantic -c stats.c -o stats.o
cc -std=c17 -Wall -Wextra -pedantic -c main.c -o main.o
cc main.o stats.o -o score_report
./score_report

The -c option means compile, do not link. The third command performs the link.

Build pipeline: stats.h feeds main.c and stats.c, each compiled to an object file, then linked into the score_report executable.

extern, static, and linkage across C files

Linkage decides whether two translation units may refer to the same name. Storage Classes in C (auto, static, extern, register) sets out the scope and lifetime rules behind these keywords. At link time the question narrows to four cases.

Code case

Meaning

extern int processed;

External-linkage declaration; it allocates no storage here

int processed = 0; at file scope

One definition with external linkage

static int cache_hits = 0; at file scope

Definition visible only in that translation unit

Automatic block variable

Local object with no linkage

One definition and many declarations is the pattern that keeps a shared variable legal. counter.h contains guarded declarations:

#ifndef COUNTER_H
#define COUNTER_H

extern int processed;
void add_record(void);

#endif

counter.c owns the definition:

#include "counter.h"

int processed = 0;

void add_record(void) {
    ++processed;
}

The new main.c uses that shared object:

#include <stdio.h>
#include "counter.h"

int main(void) {
    add_record();
    add_record();
    add_record();
    printf("processed=%d\n", processed);
    return 0;
}

All external declarations refer to the single storage definition in counter.c, so the output is processed=3.

Symbol view: counter.h declares extern processed, counter.c owns the single processed box, and three add_record calls raise it to 3.

What the C linker resolves in the worked build

Object

Defines

Needs

main.o

main

sum, mean, printf

stats.o

sum, mean

None from this program

Standard C library

printf

Nothing from this program

Calls in main.o leave relocation entries that the linker patches once it knows the addresses of sum and mean. That patching step is why a visible prototype compiles happily while a missing definition breaks the link. Use the compiler driver because direct ld calls can omit startup objects and library wiring.

Separate compilation pays off the moment one file changes. Refactor mean to use if (count == 0) { return 0.0; }, then return (double) sum(values, count) / count;. Rebuild stats.o and relink with cc main.o stats.o -o score_report. Because stats.h is unchanged, there is no need to recompile main.o. The output stays sum=316 and mean=79.00. Syntax-directed translation and code optimization covers compiler work before linking.

Header and linker errors: cause, evidence, and repair

Case

Evidence

Repair

Link with cc main.o -o score_report

Undefined references to sum and mean

Add stats.o

Define int processed = 0; in counter.h and include it twice

Multiple definition of processed

Keep extern int processed; in the header and one definition in counter.c

Declare double mean(...) but define int mean(...)

Compiler diagnostic for conflicting types

Make both signatures identical

Define static int sum(...) after declaring external int sum(...)

Compiler diagnostic for conflicting linkage

Remove static from the public function

Include guards prevent repeated inclusion within one translation unit, not duplicate definitions across object files. Also, compiling every .c file successfully does not prove that the complete program will link.

Diagnose in order: identify compiler or linker, copy the exact symbol, find every declaration and definition, then inspect the link command for omitted objects or libraries.

Header files and linking questions used in exams and interviews

Examiners and interviewers keep returning to one boundary: a missing declaration is a compile-time error, a missing definition is a link-time error. Around that they ask you to split code between header and source, to say how many external definitions a name may have across a whole program (exactly one), and to predict what static at file scope hides from the linker.

Prompt A: shared.h contains int total = 0;, included by a.c and b.c. Both objects define total, so linking should fail. Put extern int total; in the header and one int total = 0; in a source file.

Prompt B: utils.c defines static int helper(void) { return 7; }. main.c declares int helper(void); and prints it. Both compile, but helper has internal linkage in utils.o, so linking reports an undefined external helper. Remove static and declare it in utils.h.

Now try three runnable exercises:

  1. Add maximum() to stats.h and stats.c. For {72, 85, 91, 68}, expect 91.

  2. Delete stats.o from the link command. The missing user-defined symbols are sum and mean.

  3. Add one more add_record() call. Predict processed=4, then run it.

Header files and linking in C: the short version

Headers publish declarations. Each external object or function has one program-wide definition. Each .c file compiles independently, then the linker resolves names across objects and libraries.

Repeat the score-report loop: edit one implementation, rebuild one object, relink, and run. Omit stats.o once so you recognise an undefined reference later. For broader foundations, continue with the C Language course.