C Multiple Files — Multi-Translation-Unit Programs and Linkage
In this tutorial, you will learn about C Multiple Files. We cover key concepts, practical examples, and best practices to help you master this topic.
C programs spanning multiple files organize code into translation units (.c files compiled separately), with internal linkage (static) restricting symbols to their file, external linkage (extern) sharing them across files, and the linker resolving references into a single executable.
What You Will Learn
- Splitting a C program across multiple source files
- Internal linkage with static (file-scope functions and globals)
- External linkage with extern
- Header files as shared interfaces
- The compilation and linking Process
- Avoiding duplicate symbol errors
- Using forward declarations across files
Why It Matters
A single-file C program does not scale beyond a few thousand lines. Multiple files enable parallel compilation, logical organization by module, code reuse, and teamwork where each developer works on different files. Durga Antivirus Pro has 200+ .c files organized into modules: scanner, updater, quarantine, scheduler, ui, and utils. Each module compiles independently, and changes to one file trigger rebuilds of only that file.
Real-World Use
A team of 5 developers builds a text editor. Alice works on buffer.c (text storage), Bob on editor.c (cursor movement, screen rendering), Charlie on file_io.c (save/load), Diana on syntax.c (highlighting), and Eve on search.c (find/replace). Each compiles their file in seconds, and only the linker combines everything at the end.
Learning Path
flowchart LR A[Makefiles] --> B[Multiple Files\nYou are here] B --> C[Libraries] style B fill:#f90,color:#fff
Compilation and Linking Process
Each .c file is compiled independently into an object file (.o). Then the linker combines all .o files into an executable:
# Step 1: Compile each .c to .o (separate translation units)
gcc -c main.c -o main.o
gcc -c calc.c -o calc.o
gcc -c io.c -o io.o
# Step 2: Link all .o files together
gcc main.o calc.o io.o -o program
Example: Three-File Calculator
// calc.h
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
#endif
// calc.c
#include "calc.h"
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) {
if (b == 0) return 0;
return a / b;
}
// main.c
#include <stdio.h>
#include "calc.h"
int main() {
printf("5 + 3 = %d\n", add(5, 3));
printf("10 - 4 = %d\n", subtract(10, 4));
printf("6 * 7 = %d\n", multiply(6, 7));
printf("15 / 4 = %d\n", divide(15, 4));
return 0;
}
Internal Linkage with static
Functions and global variables declared static are visible only within their own file:
// helper.c
#include <stdio.h>
// Only visible inside helper.c
static int internal_counter = 0;
static void log_operation(const char *op) {
internal_counter++;
printf("[%d] %s\n", internal_counter, op);
}
// Externally visible
void helper_do_something(void) {
log_operation("do_something");
// ... actual work ...
}
// main.c
void helper_do_something(void); // External declaration
int main() {
helper_do_something(); // OK
// log_operation("test"); // ERROR: not visible here
return 0;
}
External Linkage with extern
Variables declared extern in a header reference a definition in another file:
// config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int debug_mode; // Declared here, defined elsewhere
extern const char *version; // Declared here, defined elsewhere
void set_debug(int level);
#endif
// config.c
#include "config.h"
int debug_mode = 0; // Single definition
const char *version = "2.1.0"; // Single definition
void set_debug(int level) {
debug_mode = level;
}
// main.c
#include <stdio.h>
#include "config.h"
int main() {
printf("Version: %s\n", version);
printf("Debug: %d\n", debug_mode);
set_debug(1);
printf("Debug after set: %d\n", debug_mode);
return 0;
}
Modular File Organization
project/
├── main.c # Entry point
├── parser.c/h # Input parsing
├── evaluator.c/h # Expression evaluation
├── symbol_table.c/h # Variable storage
├── error.c/h # Error handling
├── config.h # Shared configuration
└── Makefile # Build automation
Each module exposes a minimal API through its header and hides implementation details with static functions/variables.
Resolving Duplicate Symbols
// file1.c
int global_counter = 0; // Definition
void increment(void) {
global_counter++;
}
// file2.c
int global_counter = 0; // ERROR: duplicate symbol
void decrement(void) {
global_counter--;
}
Solutions:
- Move the variable to one file, use extern in the other
- Make it static in both files (each gets its own copy)
- Wrap it in a function (getter/setter pattern)
Static Functions for Encapsulation
// queue.c
#include "queue.h"
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
static Node *head = NULL; // Internal: not visible outside this file
static Node *tail = NULL; // Internal
static Node* create_node(int data) { // Internal helper
Node *n = malloc(sizeof(Node));
if (n) {
n->data = data;
n->next = NULL;
}
return n;
}
void queue_enqueue(int data) { // External API
Node *n = create_node(data);
if (!n) return;
if (tail) {
tail->next = n;
} else {
head = n;
}
tail = n;
}
int queue_dequeue(int *data) { // External API
if (!head) return -1;
Node *tmp = head;
*data = tmp->data;
head = head->next;
if (!head) tail = NULL;
free(tmp);
return 0;
}
Forward Declarations Across Files
// renderer.c
#include "renderer.h"
// Forward declare functions from other modules
void physics_update(double dt); // Defined in physics.c
void input_process_events(void); // Defined in input.c
void render_frame(double dt) {
input_process_events();
physics_update(dt);
// ... rendering ...
}
Alternatively, these declarations belong in headers (input.h, physics.h).
Common Mistakes
Duplicate definitions: Defining a non-static variable in a header results in multiple definitions when two .c files include the header. Use
externin headers and define in exactly one .c file.Missing extern for globals: If you declare
int counter;in a header withoutextern, each .c file that includes it gets a tentative definition. The linker may merge them (common symbols), but the behavior is unreliable. Always useextern.Static functions in headers: A header defining a
staticfunction creates a separate copy in every .c file that includes it. Code bloat and inconsistent behavior can result. Usestatic inlinefor small functions.Circular dependencies between files: A.c calls B.c functions and B.c calls A.c functions. This is fine as long as each .c file includes the other's header. But if A.h includes B.h and B.h includes A.h, you have a circular include problem.
Not compiling with -c when building objects: Running
gcc main.c calc.c -o programcompiles and links in one step, but only changed files are not tracked individually. Always compile each .c to .o separately (-c flag) and link at the end.
Practice Questions
- What does the
statickeyword mean when applied to a function defined at file scope? - Why do you need
externfor global variables but not for functions? - What happens if two .c files define a function with the same name and signature?
- How does the linker resolve symbol references across translation units?
- Challenge: Split a monolithic 500-line C program into 5 files: main.c, input.c, processing.c, output.c, and config.c. Each file should have its own header. Use static for internal functions. Use extern for shared configuration variables. The program reads numbers from stdin, sorts them, and writes them to an output file.
Mini Project
Build a modular text processing toolkit:
tokenizer.c/h: Splits text into tokens (words, punctuation, numbers)analyzer.c/h: Counts word frequency, sentence length, character frequencyformatter.c/h: Formats output as plain text, CSV, or JSONmain.c: Reads a file, processes with tokenizer and analyzer, outputs with formatter- All internal functions use static linkage
- Each module has a clean public API (3-5 functions)
- Shared types defined in a common
types.h - Use a Makefile for compilation
- Test with a sample text file
FAQ
What is Next
Proceed to Libraries to learn how to package code into static and dynamic libraries for reuse across projects. Then explore CMake for cross-platform build configuration.