Multi-File Programs
Splitting C code across .h/.c files, extern, linkage, and basic Makefile
Introduction
In this lesson, you'll learn about multi-file programs in C. Coming from Java, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Java, you're familiar with splitting c code across .h/.c files, extern, linkage, and basic makefile.
C has its own approach to splitting c code across .h/.c files, extern, linkage, and basic makefile, which we'll explore step by step.
The C Way
Let's see how C handles this concept. Here's a typical example:
// Project layout:
// calculator.h
// calculator.c
// main.c
// Makefile
// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b);
int subtract(int a, int b);
extern int operation_count; // declare global in header
#endif
// calculator.c
#include "calculator.h"
int operation_count = 0; // define global here
int add(int a, int b) { operation_count++; return a + b; }
int subtract(int a, int b) { operation_count++; return a - b; }
// main.c
#include <stdio.h>
#include "calculator.h"
int main(void) {
printf("%d\n", add(1, 2));
printf("%d ops\n", operation_count);
return 0;
}
// Makefile (tabs required — not spaces!)
// CC = gcc
// CFLAGS = -Wall -Wextra -std=c11
// TARGET = app
// OBJS = main.o calculator.o
//
// $(TARGET): $(OBJS)
// $(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
//
// %.o: %.c
// $(CC) $(CFLAGS) -c $<
//
// clean:
// rm -f $(OBJS) $(TARGET)Comparing to Java
Here's how you might have written similar code in Java:
// Java project layout
// src/
// com/example/
// Main.java
// math/
// Calculator.java
// io/
// FileReader.java
// Calculator.java
package com.example.math;
public class Calculator {
public int add(int a, int b) { return a + b; }
}
// Main.java
package com.example;
import com.example.math.Calculator;
public class Main {
public static void main(String[] args) {
Calculator c = new Calculator();
System.out.println(c.add(1, 2));
}
}
// Build: javac src/**/*.java -d out/
// Run: java -cp out com.example.Main
// Maven: mvn compile && mvn exec:javaYou may be used to different syntax or behavior.
C splits into .h (interface) + .c (implementation) files — Java uses one .java per class
You may be used to different syntax or behavior.
extern declares a variable defined in another file — Java has no equivalent (use class fields)
You may be used to different syntax or behavior.
Each .c file compiles to a .o object file; linker combines them — Java uses classpath
You may be used to different syntax or behavior.
Makefile describes build rules; Maven/Gradle handle Java builds
You may be used to different syntax or behavior.
static at file scope = file-private in C — Java uses package-private or private
Step-by-Step Breakdown
1. Header Guards and Declarations
Every .h file starts with include guards. It contains only declarations — never definitions (unless inline/static).
// Java: one Calculator.java with full implementation// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b); // declaration only
#endif2. extern for Shared Globals
To share a global variable across files: declare with 'extern' in the .h, define (without extern) in exactly one .c file.
// Java: class fields, static fields — no global variables// calculator.h: extern int count; // declaration
// calculator.c: int count = 0; // definition
// main.c: #include "calculator.h"
// count++; // works — extern resolved at link time3. static File Scope
static at file scope makes a function/variable private to that .c file — like Java's package-private but stricter (not even same package can access).
// Java: private static int helper()// In calculator.c
static int clamp(int v, int lo, int hi) { // file-private
return v < lo ? lo : v > hi ? hi : v;
}
int add(int a, int b) { return clamp(a+b, INT_MIN, INT_MAX); }4. Basic Makefile
Makefile automates compilation. make builds only changed files. Each rule: target: deps → tab + command.
# Maven: mvn compile
# Gradle: gradle buildCC = gcc
CFLAGS = -Wall -std=c11
app: main.o calc.o
\t$(CC) -o app main.o calc.o
main.o: main.c calc.h
\t$(CC) $(CFLAGS) -c main.c
calc.o: calc.c calc.h
\t$(CC) $(CFLAGS) -c calc.c
clean:
\trm -f *.o appCommon Mistakes
When coming from Java, developers often make these mistakes:
- C splits into .h (interface) + .c (implementation) files — Java uses one .java per class
- extern declares a variable defined in another file — Java has no equivalent (use class fields)
- Each .c file compiles to a .o object file; linker combines them — Java uses classpath
Key Takeaways
- .h declares interface, .c implements — #include pulls declarations in
- extern declares global from another file; define it (without extern) in exactly one .c
- static at file scope makes names private to that .c file
- Makefile: target: deps → tab command; make only rebuilds changed files