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 |
|---|---|
Function and type declarations | Ordinary function bodies |
Macros | Storage definitions |
Carefully chosen | Private implementation details |
If these language foundations still feel new, the Coding & Skill Development Courses category includes a dedicated C learning track.
Multi-file C program: compile and link a score report
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.00Build 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_reportThe -c option means compile, do not link. The third command performs the link.

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 |
|---|---|
| External-linkage declaration; it allocates no storage here |
| One definition with external linkage |
| 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);
#endifcounter.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.

What the C linker resolves in the worked build
Object | Defines | Needs |
|---|---|---|
|
|
|
|
| None from this program |
Standard C library |
| 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 | Undefined references to | Add |
Define | Multiple definition of | Keep |
Declare | Compiler diagnostic for conflicting types | Make both signatures identical |
Define | Compiler diagnostic for conflicting linkage | Remove |
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:
Add
maximum()tostats.handstats.c. For{72, 85, 91, 68}, expect91.Delete
stats.ofrom the link command. The missing user-defined symbols aresumandmean.Add one more
add_record()call. Predictprocessed=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.




