Once you get comfortable grouping data with structs and enums, the next milestone is organizing code across multiple files. In C, the default pattern is to split function or struct definitions into several source files and collect their declarations in headers so other files can share them. This keeps you from stuffing every line into a single file and makes each file’s role explicit. In this post we will build the smallest possible two-file project to see exactly how .h and .c files divide responsibilities.
New Terms in This Post
- Header file: A
.hfile that carries declarations for functions, structs, and constants that other files need - Declaration: A statement that tells the compiler a name and type exist
- Definition: The statement that provides the actual function body or storage for a variable
- Include guard: A preprocessor pattern that blocks duplicate declarations when the same header is included multiple times
Key Ideas
Study Notes
- Time required: about 70 minutes
- Prerequisites: structs, basic functions, experience running
clang- Goal: separate headers and sources, distinguish declarations from definitions, understand multi-file build commands
Keep three things in mind as you read:
- Headers hold declarations, constants, typedefs—everything that describes the API—while
.cfiles keep the actual implementation. - Use the
#ifndef/#defineinclude guard pattern to prevent duplicate inclusions. - Learn how to compile multiple
.cfiles at once or as separate object files.
Code Walkthrough
Establishing the Project Layout
project/
├── main.c
├── stats.c
└── stats.h
stats.h lists the function declarations and any required structs. stats.c implements them, and main.c includes the header to call the functions. A declaration tells the compiler what exists; a definition provides the actual code or data.
stats.h: introduces the names and typesstats.c: performs the calculationsmain.c: calls the functions and prints the results
Writing the Header
// stats.h
#ifndef STATS_H
#define STATS_H
#include <stddef.h>
typedef struct {
double average;
int max;
int min;
} Stats;
Stats calculate_stats(const int *numbers, size_t length);
#endif
The file starts with #ifndef and #define, then ends with #endif. That pattern guarantees the header is processed only once even if multiple source files include it. The header contains declarations, not implementations.
Implementing the Functions
// stats.c
#include "stats.h"
Stats calculate_stats(const int *numbers, size_t length) {
size_t i;
Stats stats = {0.0, 0, 0};
int total = 0;
if (length == 0) {
return stats;
}
stats.max = numbers[0];
stats.min = numbers[0];
for (i = 0; i < length; i++) {
total += numbers[i];
if (numbers[i] > stats.max) {
stats.max = numbers[i];
}
if (numbers[i] < stats.min) {
stats.min = numbers[i];
}
}
stats.average = (double)total / (double)length;
return stats;
}
Implementation files include the header to keep declarations and definitions aligned. If you change the function signature in one place but not the other, the compiler immediately throws an error.
Calling the Function from main.c
// main.c
#include <stdio.h>
#include "stats.h"
int main(void) {
int numbers[5] = {72, 85, 90, 68, 77};
Stats stats = calculate_stats(numbers, 5);
printf("avg = %.1f\n", stats.average);
printf("max = %d\n", stats.max);
printf("min = %d\n", stats.min);
return 0;
}
Use double quotes for project headers so the compiler searches the current project first. Use angle brackets (<...>) for installed standard headers like <stdio.h>.
Compiling Multiple Files
clang main.c stats.c -o stats-app
Once the project grows, you can compile each .c file separately into object files and then link them together. That is faster than rebuilding everything every time.
Or build object files separately for more control:
clang -c main.c -o main.o
clang -c stats.c -o stats.o
clang main.o stats.o -o stats-app
The second approach scales better as the project grows. When you edit one .c file you only need to recompile that translation unit; the other object files stay untouched. The combined command (clang main.c stats.c ...) works because stats.c contains the definition of calculate_stats, which main.c calls.
Declaring External Variables
// config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int MAX_USERS;
#endif
// config.c
int MAX_USERS = 100;
When a global variable must be shared, put the extern declaration in the header and define the variable in exactly one .c file. extern does not allocate memory; it simply tells the compiler “this variable lives elsewhere.” This pattern prevents duplicate definition errors.
Why It Matters
Large programs quickly outgrow a single file. Splitting headers and sources lets teammates work on different areas simultaneously and lets the compiler build each unit independently, saving time. Because declarations and definitions are separate, developers can use a function by reading its header without diving into the implementation, making testing and documentation easier.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.
💬 댓글
이 글에 대한 의견을 남겨주세요