Header files are the part of C that make multi-file programs manageable. If you are writing anything bigger than a single throwaway source file, you need a clean way to share function declarations, constants, types, and compile-time settings across translation units. That is what header files do.
The basic rule is simple: put declarations in header files, put definitions in source files, and include the header wherever that interface is needed. In practice, that means your .h file describes the contract and your .c file provides the implementation. Once you get that separation right, the compiler can check your code consistently and you can split work across files without turning the project into a maze of duplicated prototypes.
What header files are for
A header file is a reusable interface file. It usually contains things other files need to know before they can call your functions or use your types.
Typical contents include:
- Function declarations
struct,enum, andtypedefdeclarations#defineconstants and macro helpersexterndeclarations for shared global variables- Include guards or
#pragma once
A header should usually not contain full function bodies unless the function is intentionally static inline or a macro. If you place ordinary non-inline function definitions in a header and include that header from multiple .c files, the linker will complain about duplicate symbols.
The split between declaration and definition
This distinction matters more than beginners expect.
| Item | Where it belongs | Example |
|---|---|---|
| Function declaration | Header | int add(int a, int b); |
| Function definition | Source file | int add(int a, int b) { return a + b; } |
| Shared type declaration | Header | typedef struct { ... } Thing; |
| Implementation details | Source file | helper functions, private state |
A declaration tells the compiler that something exists and what its shape is. A definition provides the actual storage or executable code. The header exposes the contract. The source file fulfills it.
That split is the foundation of modular C.
A minimal example
Suppose you want a small math library with two functions.
math_utils.h
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
#endif
math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
main.c
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("%d\n", add(3, 4));
printf("%d\n", subtract(10, 7));
return 0;
}
Here the header gives main.c access to the function signatures. The implementation lives in math_utils.c. When you compile the project, both source files get compiled and linked together.
A typical command looks like this:
gcc main.c math_utils.c -o app
Why include guards matter
Headers are often included by multiple source files, and sometimes one header includes another header. Without protection, the same declarations could be seen more than once in the same translation unit.
That is why most headers use include guards:
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* declarations */
#endif
The pattern prevents duplicate inclusion. If the header has already been processed, the guard macro blocks the body from being included again.
Some projects use #pragma once instead. It is shorter and common, but include guards are still the most portable, explicit option.
What to put in a header, and what to leave out
The main habit to build is keeping headers lean.
Put these in headers:
- Public function declarations
- Public type declarations
- Public constants and macros
- Shared
externdeclarations - Small inline helpers when appropriate
Leave these in source files:
- Function bodies for ordinary functions
- Private helper functions
- Internal data structures not meant for external use
- File-local state
- Implementation-specific includes that are not part of the public interface
This separation keeps compile times lower and dependencies cleaner. It also makes it easier to change implementation details without forcing unrelated files to rebuild.
How extern works with globals
If a global variable must be shared across files, declare it in a header with extern, and define it once in a source file.
config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int debug_mode;
#endif
config.c
#include "config.h"
int debug_mode = 0;
Any file that includes config.h can read or modify debug_mode, but the actual storage exists only once in config.c.
If you skip extern and define the variable directly in the header, every source file that includes it gets its own definition, which usually breaks linking.
How to organize headers in real projects
A practical layout often looks like this:
include/for public headerssrc/for implementation.cfilestests/for test codebuild/for generated artifacts
Public headers go in include/ when other parts of the project, or other projects, need to consume them. Internal headers may stay under src/ if they are only for internal use.
This split helps you decide what is part of the API and what is private.
Common mistakes
Most header-file bugs come from a few repeating patterns.
1. Putting function definitions in headers
If the function is not static inline, this creates duplicate definitions when several source files include the header.
2. Forgetting the header guard
This can lead to repeated declarations or confusing compiler errors when a header is included indirectly more than once.
3. Including too much in a header
If a header includes many unrelated headers, every file that includes it inherits that dependency chain. That slows compilation and makes the code harder to reason about.
4. Exposing private details
If you expose internal struct layouts or helper macros that are not really part of the public API, you make future refactoring harder.
5. Mismatched declarations and definitions
If the declaration in the header says one signature and the source file defines another, the compiler or linker will catch it, but often only after you have already scattered calls through the codebase.
A simple workflow for using headers well
When you add a new feature, follow a predictable sequence:
- Decide what other files need to know.
- Put those declarations in a header.
- Put the implementations in a
.cfile. - Include the header wherever the declarations are used.
- Compile the whole set of source files together.
- Fix any missing declaration, include, or linkage issue before adding more code.
This workflow keeps the interface stable and prevents accidental duplication.
Header files and compilation units
A C compiler processes one translation unit at a time. A translation unit is the source file after the preprocessor has expanded all includes. That means the header contents are literally copied into each .c file that includes them.
That is why headers are not magical modules. They are preprocessing tools. Their value comes from consistency and reuse, not from separate compilation by themselves.
Once you understand that, a lot of C build behavior becomes clearer:
- A header does not create code on its own.
- A source file produces object code.
- The linker combines the object files.
- The header makes sure each source file sees the same interface.
When inline functions belong in headers
Sometimes a small function is performance-sensitive or trivial enough that you want the compiler to see the body directly.
In those cases, a static inline function in a header can be appropriate:
#ifndef MATH_INLINE_H
#define MATH_INLINE_H
static inline int square(int x) {
return x * x;
}
#endif
Use this carefully. Inline functions are best for tiny helpers with obvious behavior. If the function grows, move it back into a .c file.
Practical checklist
Before you decide a header is finished, check the following:
- Does it declare only what other files need?
- Are all non-inline function bodies kept out of the header?
- Is there an include guard or
#pragma once? - Are all declarations consistent with the definitions in the source file?
- Are unnecessary includes avoided?
- Are private implementation details hidden?
If you can answer yes to those questions, the header is probably in good shape.
Final takeaway
Header files are how C programs share interfaces across source files. They let you separate declaration from implementation, reduce duplication, and keep larger projects readable. Use headers for the public contract, source files for the code itself, and include guards to keep the build predictable.
Once you start thinking of headers as the visible surface of a module, using them becomes much easier. The goal is not to put more code into headers. The goal is to make the codebase easier to compile, understand, and change.