Multi-File Projects and Linking
C programs split across multiple .c files with shared declarations in .h header files; the linker combines compiled object files into one executable.
Introduction
In this lesson, you'll learn about multi-file projects and linking in C. Coming from JavaScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In JavaScript, you're familiar with c programs split across multiple .c files with shared declarations in .h header files; the linker combines compiled object files into one executable..
C has its own approach to c programs split across multiple .c files with shared declarations in .h header files; the linker combines compiled object files into one executable., which we'll explore step by step.
The C Way
Let's see how C handles this concept. Here's a typical example:
/* math.h — declarations (header guard prevents double inclusion) */
#ifndef MATH_H
#define MATH_H
#define PI 3.14159
int add(int a, int b); /* function prototype */
extern const char *VERSION; /* extern: defined elsewhere */
#endif /* MATH_H */
/* math.c — definitions */
#include "math.h"
const char *VERSION = "1.0";
int add(int a, int b) {
return a + b;
}
/* main.c — consumer */
#include <stdio.h>
#include "math.h"
int main(void) {
printf("%d\n", add(2, 3)); /* 5 */
printf("%.5f\n", PI); /* 3.14159 */
printf("%s\n", VERSION); /* 1.0 */
return 0;
}
/* Makefile (tab-indented) */
/*
all: program
program: main.o math.o
gcc -o program main.o math.o
main.o: main.c math.h
gcc -c main.c
math.o: math.c math.h
gcc -c math.c
clean:
rm -f *.o program
*/Comparing to JavaScript
Here's how you might have written similar code in JavaScript:
// math.js — module
export function add(a, b) { return a + b; }
export const PI = 3.14159;
// main.js
import { add, PI } from "./math.js";
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159You may be used to different syntax or behavior.
JS modules use import/export; C uses #include to paste header declarations, then the linker resolves references
You may be used to different syntax or behavior.
Header (.h) files contain declarations (prototypes, extern, #define, typedefs); source (.c) files contain definitions
You may be used to different syntax or behavior.
Include guards (#ifndef MATH_H … #define MATH_H … #endif) prevent a header from being included twice in one translation unit
You may be used to different syntax or behavior.
extern tells the compiler a variable/function is defined in another file — the linker will find it
You may be used to different syntax or behavior.
Makefile: each target has dependencies and a tab-indented shell command; make only rebuilds what changed
Step-by-Step Breakdown
1. Create the Header File
Put declarations in math.h: function prototypes, extern variable declarations, macros, and typedefs. Always wrap with include guards.
// math.js: export function add(a, b) { return a + b; }/* math.h */
#ifndef MATH_H
#define MATH_H
int add(int a, int b); /* prototype only */
extern const int MAX; /* defined in math.c */
#endif2. Implement in the Source File
math.c #includes its own header (so the compiler checks that the prototype matches the definition) and provides the actual implementations.
/* math.c */
#include "math.h"
const int MAX = 1000;
int add(int a, int b) {
return a + b;
}3. Use in main.c
#include the header in every file that uses the declarations. The compiler produces .o object files; the linker combines them.
// main.js
import { add } from './math.js';/* main.c */
#include <stdio.h>
#include "math.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}4. Build with gcc or make
Compile each .c file to an object file, then link. A Makefile automates this and only recompiles files whose source or headers changed.
# Manual:
gcc -c math.c -o math.o
gcc -c main.c -o main.o
gcc -o program main.o math.o
# Or with make:
make # builds 'all' target
make clean # removes *.o and programCommon Mistakes
When coming from JavaScript, developers often make these mistakes:
- JS modules use import/export; C uses #include to paste header declarations, then the linker resolves references
- Header (.h) files contain declarations (prototypes, extern, #define, typedefs); source (.c) files contain definitions
- Include guards (#ifndef MATH_H … #define MATH_H … #endif) prevent a header from being included twice in one translation unit
Key Takeaways
- Header files (.h) hold declarations; source files (.c) hold definitions — #include pastes the header text
- Include guards (#ifndef / #define / #endif) prevent duplicate declarations when a header is included from multiple files
- extern declares a variable defined in another translation unit — the linker resolves it at link time
- Compile each .c to .o with gcc -c, then link all .o files together; make automates incremental builds