Multi-File Programming
Multi-file programming is like team collaboration — declarations are contracts (header files), implementations are the work (source files), extern is a cross-department reference, and Makefile is the project schedule.
Why Multi-File
A single .c file is enough for small programs, but real projects have large codebases that need to be split by functionality into multiple files:
- Clear logic with well-defined responsibilities
- Faster compilation — changing one file only recompiles that file
- Code reuse — common modules can be shared across projects
- Team collaboration — different people own different modules
Writing Header Files
Header files (.h) contain declarations, and source files (.c) contain definitions. This is the most fundamental modularization rule in C.
Header File Content
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
double average(int *arr, int n);
#endif
Source File Content
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
double average(int *arr, int n) {
if (n <= 0) return 0.0;
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
return (double)sum / n;
}
Main Program
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("3 + 5 = %d\n", add(3, 5));
printf("10 - 4 = %d\n", subtract(10, 4));
int scores[] = {80, 90, 75, 88};
printf("Average: %.1f\n", average(scores, 4));
return 0;
}
extern for Cross-File References
A global variable defined in one .c file can be accessed from other files using extern.
Definition (in some .c file)
int g_count = 0;
Declaration (in a header or another .c file)
extern int g_count;
extern tells the compiler "this variable is defined elsewhere — this is only a declaration," so no new space is allocated.
Example
Project structure:
project/
├── counter.h
├── counter.c
└── main.c
counter.h:
#ifndef COUNTER_H
#define COUNTER_H
extern int g_count;
void increment(void);
int get_count(void);
#endif
counter.c:
#include "counter.h"
int g_count = 0;
void increment(void) {
g_count++;
}
int get_count(void) {
return g_count;
}
main.c:
#include <stdio.h>
#include "counter.h"
int main(void) {
increment();
increment();
increment();
printf("count = %d\n", get_count());
printf("g_count = %d\n", g_count);
return 0;
}
count = 3
g_count = 3
extern declaration. extern int g_count = 0; may become a definition on some compilers, causing duplicate definition errors.
Multi-File Compilation Commands
Compile Individually, Then Link
gcc -c counter.c -o counter.o
gcc -c main.c -o main.o
gcc counter.o main.o -o myapp
The -c flag compiles without linking, producing .o object files. All .o files are then linked into the final executable.
Compile All at Once
gcc counter.c main.c -o myapp
Simple, but changing any file requires recompiling everything — not suitable for large projects.
main.c, you only need gcc -c main.c and then link, without recompiling counter.c.
Makefile Basics
Makefile automates compilation rules and only recompiles files that have been modified.
CC = gcc
CFLAGS = -Wall -g
myapp: main.o counter.o
$(CC) $(CFLAGS) main.o counter.o -o myapp
main.o: main.c counter.h
$(CC) $(CFLAGS) -c main.c -o main.o
counter.o: counter.c counter.h
$(CC) $(CFLAGS) -c counter.c -o counter.o
clean:
rm -f *.o myapp
Usage:
make
make clean
Makefile rule format:
target: dependencies
command
- Target: the file to generate
- Dependencies: files the target depends on — if any dependency is newer than the target, the command is re-executed
- Command: how to generate the target from the dependencies
Preventing Duplicate Inclusion
A header file may be included by multiple source files, and the same source file may indirectly include it multiple times. Without protection, duplicate declarations cause errors.
Approach 1: Include Guard
#ifndef MYHEADER_H
#define MYHEADER_H
void my_func(void);
#endif
On first inclusion, MYHEADER_H is undefined, so the content is processed and the macro is defined. On subsequent inclusions, #ifndef evaluates to false and the entire content is skipped.
Approach 2: #pragma once
#pragma once
void my_func(void);
Concise and supported by most compilers, but not part of the standard — theoretically less portable than include guards.
#pragma once is more concise; include guards are more standard. Pick one and use it consistently.
Header File Organization Principles
- Each
.cfile should have a corresponding.hfile with the same name .hfiles should only contain declarations, not definitions- The first line of a
.cfile should include its own.h(to verify declarations match definitions) - Headers must include duplicate-inclusion protection
externglobal variable declarations go in.h; definitions go in a specific.cfile
❓ FAQ
📖 Summary
- Header files (.h) hold declarations, source files (.c) hold definitions — this separation is the foundation of modularization
externdeclares cross-file global variables and functions, telling the compiler "defined elsewhere"- Multi-file compilation should use "compile individually with -c, then link" — only recompile modified files
- Makefile automates compilation dependencies, avoiding the need to type long commands manually
- Prevent duplicate inclusion with include guards or
#pragma once
📝 Exercises
- Create a 3-file project:
string_utils.h,string_utils.c, andmain.c. Implement a string reversal function and a character-count function, then call and test them in main - Write a Makefile for exercise 1 that supports
maketo compile andmake cleanto clean up - Intentionally omit duplicate-inclusion protection in a header file, then
#includethe same header twice in main.c and observe the compilation error



