Working with Multiple Files in C
Table of contents
When programs get bigger, keeping everything in one
.cfile gets messy. Splitting code into logical modules (each with a.hheader and a.csource) makes it easier to read, test, and reuse.
Introduction
For tiny programs, a single file with main() and a few helper functions is fine. For anything larger, split the project into blocks of functionality (modules). Each block usually has:
- A header (
.h) with declarations (function prototypes, constants,typedefs). - A source (
.c) with the definitions (the actual code).
main.c becomes the “glue” that includes the headers and calls functions across modules. This modularisation is also how we build libraries you can reuse across projects: the header defines the interface or API (Application Programming Interface) and the source implements it.
Headers and Interfaces
- Header (
.h): what a module offers (function prototypes and public constants). - Source (
.c): how that module works (function bodies). - Use
#include "mylib.h"for your headers, and#include <stdio.h>for system/standard headers. The angle brackets<>tell the compiler to search system include paths first; quotes search the current directory before system paths.
Think of the header as the contract. If
main.cincludes the header, the compiler knows the function names, parameter/return types, and can type‑check calls, even though the code lives in another file.
Example (C version): A Tiny Maths “Library”
Lets make a minimal library with a single function sum(int, int) and call it from main.c. The header advertises the function; the source implements it.
main.h
#ifndef MAIN_H
#define MAIN_H
/* Pull in the maths module’s interface */
#include "maths.h"
#endif /* MAIN_H */
maths.h
#ifndef MATHS_H
#define MATHS_H
/* Function takes two integers and returns their sum */
int sum(int a, int b);
#endif /* MATHS_H */
maths.c
#include "maths.h"
int sum(int a, int b) {
return a + b;
}
main.c
#include <stdio.h>
#include "main.h" /* brings in maths.h via main.h */
int main(void) {
int total = sum(5, 6);
printf("Sum = %d\n", total);
return 0;
}
Compile (small projects):
gcc -std=c11 -Wall -Wextra -Werror main.c maths.c -o main.out
You don’t list headers on the command line, the .c files include them. If your headers are in the current directory and the compiler can’t find them, add -I. to the compile command to tell gcc to search “.” for headers.
Makefiles (Why and How)
Typing long compile commands gets old fast! A Makefile saves the build steps into named targets so you can just run make. The default file name is Makefile; if you use another name, run make -f MyMakefile. We have been using these in the Unit 1 Activities
Small, single‑rule Makefile
# Makefile
main.exe: main.c maths.c
@gcc -std=c11 -Wall -Wextra -Werror main.c maths.c -o main.out
- First line: target (
main.exe) and its dependencies (main.c maths.c). - Second line (tab, not spaces!) is the build command. If nothing changed,
makewill say the target is up to date and do nothing.
Faster rebuilds with object files
For larger projects, compile each source to an object (.o) and link them. Then, only the changed .c files rebuild. make can also use variables so you dont have to keep repeating the compiler and flags and filenames.
# Makefile (object-based)
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Werror -c
LDFLAGS =
OBJS = main.o maths.o
all: main.exe
main.exe: $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $@
main.o: main.c main.h maths.h
$(CC) $(CFLAGS) main.c
maths.o: maths.c maths.h
$(CC) $(CFLAGS) maths.c
clean:
@rm -f *.o main.exe
- First build: compile all
.c→.o, then link. - Subsequent builds:
makerecompiles only what changed, then links. make cleanremoves build artefacts before sharing or zipping your project.
Common Pitfalls (and Fixes)
-
Putting definitions in headers
Don’t put function bodies or non‑externglobal variables in.hfiles; you’ll get duplicate definition errors when multiple.cfiles include the header. Keep headers to declarations; put code and single definitions in one.c. -
Including
.cfiles
Never do#include "module.c"from another.c. Include headers; compile each.cand link them. Otherwise you’ll pull the same definitions in twice and the linker will complain about duplicates. -
Forgetting include guards
Always guard headers with#ifndef/#define/#endifto prevent multiple inclusion.
The extern Keyword (Sharing a Global Across Files)
Global variables are often discouraged, but they do appear in embedded C (e.g., a status flag or a hardware handle). If multiple files need to use the same global, do it like this: put an extern declaration in the header, and a single definition in one .c file. This fixes both “not declared” and “duplicate symbol” errors seen when naively declaring a global in a header.
main.h
#ifndef MAIN_H
#define MAIN_H
/* This declares that a variable exists somewhere (no storage here). */
extern int global_variable;
/* Function in another module we'll call */
void change_global_variable(void);
#endif /* MAIN_H */
extern_demo.c (the other module)
#include "main.h"
void change_global_variable(void) {
/* Use the variable declared 'extern' in main.h
and defined once in main.c */
global_variable = 0;
}
main.c
#include <stdio.h>
#include "main.h"
/* Exactly one definition (with storage) in the whole program */
int global_variable = 99;
int main(void) {
change_global_variable();
printf("Global variable = %d\n", global_variable); /* prints 0 */
return 0;
}
Build:
gcc -std=c11 -Wall -Wextra -Werror -c main.c
gcc -std=c11 -Wall -Wextra -Werror -c extern_demo.c
gcc main.o extern_demo.o -o extern_demo
Why not put
int global_variable = 99;in a header? Because every.cthat includes it would get its own definition → the linker sees duplicate symbols.externin the header declares the name; one.cfile provides the single definition.
Summary
- Split growing programs into modules:
.hfor interface,.cfor implementation. - Include headers (not
.cfiles) and compile each.c, then link. - Use Makefiles so
makerebuilds only what changed. - Share globals across files with
externin headers and one real definition in a.c.
These practices keep projects organised and scale from student labs to real embedded products.
Quick Compile Cheat‑Sheet
# One-shot build (small demos)
gcc -std=c11 -Wall -Wextra -Werror main.c maths.c -o main.exe
# Separate compilation (good habit)
gcc -std=c11 -Wall -Wextra -Werror -c main.c
gcc -std=c11 -Wall -Wextra -Werror -c maths.c
gcc main.o maths.o -o main.exe
(Use -I. if headers live in the current directory and gcc can’t find them.)