Header Files & Multi-file Programs
Small programs fit in one file. Real ones do not: the Linux kernel has tens of thousands of files, and even a student project quickly becomes hard to manage as one long main.c. C lets you split a program into several source files (.c) that share declarations through header files (.h).
In this lesson you will split a program into a header and two source files, protect headers with include guards, compile files separately, see the four stages of compilation, and learn to read the linker errors that appear when something is missing.
Why split a program?
- Organisation. Each file handles one topic: billing, file storage, menus.
- Reuse. A well-made
bill.ccan be used by several programs. - Faster builds. When you change one file, only that file needs compiling again.
- Hiding details. Helper functions marked
staticstay private to their file.
A header and its source file
A header contains declarations: what exists and how to use it. The matching .c file contains the definitions: the actual code. Here is a small billing module.
// bill.h: the public interface of the billing module
#ifndef BILL_H
#define BILL_H
#define GST_RATE 18 // percent
typedef struct {
char name[30];
int price; // in rupees
int quantity;
} Item;
int item_total(const Item *item);
int bill_with_gst(const Item items[], int count);
#endif// bill.c: the implementation
#include "bill.h"
static int add_gst(int amount) // private helper, invisible outside this file
{
return amount + amount * GST_RATE / 100;
}
int item_total(const Item *item)
{
return item->price * item->quantity;
}
int bill_with_gst(const Item items[], int count)
{
int sum = 0;
for (int i = 0; i < count; i++) {
sum += item_total(&items[i]);
}
return add_gst(sum);
}// main.c: uses the module through its header
#include <stdio.h>
#include "bill.h"
int main(void)
{
Item cart[] = {
{"Notebook", 45, 4},
{"Pen", 12, 10},
{"Calculator", 650, 1},
};
int count = 3;
for (int i = 0; i < count; i++) {
printf("%-12s Rs %d\n", cart[i].name, item_total(&cart[i]));
}
printf("Total with %d%% GST: Rs %d\n", GST_RATE, bill_with_gst(cart, count));
return 0;
}Compile both .c files together. Headers are never listed in the command; they are pulled in by #include.
gcc -std=c23 -Wall -Wextra main.c bill.c -o shop
./shopNotebook Rs 180 Pen Rs 120 Calculator Rs 650 Total with 18% GST: Rs 1121
The items add up to Rs 950, and 18% GST adds Rs 171. Notice that bill.c includes its own header. This lets the compiler check that the prototypes in bill.h match the real definitions; if they differ, you get an error instead of a silent bug.
Include guards
The lines #ifndef BILL_H, #define BILL_H and #endif form an include guard. The first time bill.h is included, BILL_H is not defined, so the contents are used and BILL_H gets defined. If the same header is included again, perhaps through another header, the contents are skipped. Without a guard, the typedef would appear twice and cause an error.
Many compilers also accept #pragma once at the top of a header, which does the same job in one line. It is widely supported by GCC, Clang and Microsoft's compiler, but it is not part of the C standard, so classic guards are the fully portable choice.
What belongs in a header
| Put in a header | Keep in a .c file |
|---|---|
| Function prototypes | Function definitions |
typedef, struct, enum and union types | static helper functions |
| Macros and constants that callers need | Global variable definitions (int count = 0;) |
extern declarations of shared variables | Anything callers should not depend on |
The key rule: a header must not define variables or ordinary functions. If bill.h contained int total_bills = 0; and two .c files included it, each would define its own total_bills, and the linker would report a multiple definition. Instead, write extern int total_bills; in the header and int total_bills = 0; in exactly one .c file.
Separate compilation and linking
Behind the single gcc command are four stages:
| Stage | Option to stop here | Result |
|---|---|---|
| Preprocess | gcc -E main.c | Expanded C text |
| Compile | gcc -S main.c | main.s, assembly language |
| Assemble | gcc -c main.c | main.o, an object file of machine code |
| Link | gcc main.o bill.o -o shop | The executable program |
Each .c file is compiled on its own into an object file. The object file contains machine code, plus a list of names it defines and names it still needs. The linker joins the object files and the C library, connecting each call to its definition. You can run the stages yourself:
gcc -std=c23 -Wall -Wextra -c bill.c
gcc -std=c23 -Wall -Wextra -c main.c
gcc main.o bill.o -o shopNow, if you edit only main.c, you recompile main.c and link again; bill.o is reused unchanged. In a project with hundreds of files, this saves a lot of time. Typing these commands by hand gets tedious, which is exactly the problem that Makefiles solve.
Reading linker errors
Compiler errors mention a line in your code. Linker errors happen after compilation, so they mention names instead. If you forget to compile bill.c, every file compiles fine, but the linker cannot find the definitions. On Linux, the output looks like this (the details in brackets vary):
gcc -std=c23 -Wall -Wextra main.c -o shop/usr/bin/ld: /tmp/ccQ2xL9a.o: in function `main': main.c:(.text+0x9e): undefined reference to `item_total' /usr/bin/ld: main.c:(.text+0xc1): undefined reference to `bill_with_gst' collect2: error: ld returned 1 exit status
Undefined reference means "this was declared and used, but no file defined it". Check that every .c file is in the command and that names are spelt the same everywhere. The opposite error, multiple definition, means two files defined the same name. Look for a variable or function defined in a header, or the same helper copied into two files without static.
Common mistakes
- Including a
.cfile.#include "bill.c"pastes the definitions into another file and leads to multiple definitions. Include headers only. - Defining variables in a header. Declare with
externin the header and define in one.cfile. - Forgetting a file in the build command. The result is an undefined reference.
- No include guard. Including the header twice causes redefinition errors.
- A source file that does not include its own header. Mismatches between prototype and definition go unnoticed.
Key points
- Headers (
.h) hold declarations; source files (.c) hold definitions. - Include your own headers with quotes and protect each with an include guard (or
#pragma once). - Never define variables or ordinary functions in a header; use
externdeclarations instead. gcc -ccompiles one file into an object file; the linker joins object files into a program.- "Undefined reference" means a definition is missing; "multiple definition" means there are two.
- Mark helpers
staticso they stay private to their file.
Exercise
Split a temperature converter into three files. convert.h declares double to_fahrenheit(double c); and double to_celsius(double f); with an include guard. convert.c defines both functions. main.c includes the header and prints 37 °C in Fahrenheit and 212 °F in Celsius, each with one decimal place. Write the compile command too.
Show solution
// convert.h
#ifndef CONVERT_H
#define CONVERT_H
double to_fahrenheit(double c);
double to_celsius(double f);
#endif// convert.c
#include "convert.h"
double to_fahrenheit(double c)
{
return c * 9 / 5 + 32;
}
double to_celsius(double f)
{
return (f - 32) * 5 / 9;
}// main.c
#include <stdio.h>
#include "convert.h"
int main(void)
{
printf("37.0 C = %.1f F\n", to_fahrenheit(37.0));
printf("212.0 F = %.1f C\n", to_celsius(212.0));
return 0;
}gcc -std=c23 -Wall -Wextra main.c convert.c -o convert
./convert37.0 C = 98.6 F 212.0 F = 100.0 C
main.c knows only the prototypes from convert.h; the linker connects the calls to the definitions in convert.c.
Quick Quiz
1. What should a header file usually contain?
Headers declare what exists; definitions belong in exactly one .c file.
2. What does gcc -c bill.c produce?
-c stops after compiling and assembling, producing an object file to link later.
3. What does "undefined reference to `item_total'" usually mean?
The function was declared and called, but no object file being linked defined it.
4. What is the purpose of #ifndef BILL_H / #define BILL_H / #endif?
The include guard skips the contents when the header is included a second time.
Learn C with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
