Menu

Header Files in C: .h vs .c, Include Guards, and Multi-File Programs

How to split a C program across files: what belongs in a .h, what belongs in a .c, include guards that stop double inclusion, compiling several files together, and sharing globals with extern.

This page includes runnable editors - edit, run, and see output instantly.

A single .c file is fine until it is 2,000 lines long. Splitting a program across files lets each part be compiled separately, reused in other programs, and read on its own - but C has no import system. What it has is the preprocessor pasting text, plus a linker that joins the pieces at the end.

A header file (.h) is the shared contract between those pieces: it tells every source file what exists elsewhere, without containing the implementation.

Declarations vs Definitions

The whole design rests on one distinction.

A declaration says this exists somewhere and here is its shape. It generates no code and can appear any number of times:

int add(int a, int b);        /* function declaration (prototype) */
extern int error_count;       /* variable declaration */
struct Point { int x, y; };   /* type definition - safe to repeat per file */

A definition creates the thing. It must appear exactly once in the whole program:

int add(int a, int b) { return a + b; }   /* function definition */
int error_count = 0;                      /* variable definition */

Headers hold declarations. Source files hold definitions. Get that backwards and the linker complains about "multiple definition of ..." - the one error message that reliably means a definition wandered into a header.

A Two-File Program

Here is the smallest useful split. A header declaring two functions:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

The source file that implements them - note it includes its own header:

/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

And the program that uses them:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

Compile both source files together:

gcc main.c math_utils.c -o app
./app

Two details worth noticing. First, math_utils.c includes its own header - that is not redundant. It makes the compiler check that each definition matches its declaration, so if you change the header's prototype and forget the .c file, you get an error immediately rather than a mismatch at link time.

Second, math_utils.h is not on the gcc command line. Headers are never compiled; they are pasted into .c files by #include. Passing a .h to the compiler produces a stray precompiled-header file and no linkable code.

The same program squeezed into one file, so you can run it here:

The declarations before main are what the header supplies in the real version - which is exactly why function prototypes and headers are the same idea at two scales.

Include Guards

#include pastes text, and text pasted twice is text duplicated. That is harmless for a function prototype and fatal for a struct:

/* shapes.h WITHOUT a guard */
struct Point { int x, y; };

If main.c includes both shapes.h and canvas.h, and canvas.h also includes shapes.h, the compiler sees struct Point defined twice in one translation unit and stops with "redefinition of 'struct Point'". In a real project these chains get deep enough that you cannot track them by hand.

The fix is an include guard: a macro that records "this header has already been pasted".

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

The first inclusion finds SHAPES_H undefined, so the body is kept - and defines SHAPES_H on the way through. Every later inclusion in the same file finds it defined and skips straight to #endif. The macro name must be unique across the project; FILENAME_H derived from the path is the usual convention.

The one-line alternative is supported by every mainstream compiler:

/* shapes.h */
#pragma once

struct Point { int x, y; };

#pragma once cannot suffer a name collision and cannot be broken by a typo in the #endif. Its only drawback is that it is not in the C standard, so a project that must build on unusual compilers should prefer the #ifndef form. Either way, every header gets one - no exceptions, including headers you think nothing else will include.

What Belongs in a Header

Put in a .h:

  • Function prototypes
  • struct, union, and enum definitions
  • typedef declarations
  • Macros meant to be shared
  • extern declarations of shared global variables
  • The #includes that header itself needs to be self-contained

Keep out of a .h:

  • Function bodies (unless deliberately static inline)
  • Variable definitions - int counter; in a header defines a separate variable in every file that includes it, or a link error, depending on the compiler
  • #include of headers the header does not itself need - it pushes that dependency onto everyone downstream

"Self-contained" is worth making a rule: a header should compile when included first, before anything else. If shapes.h uses size_t, it includes <stddef.h> itself rather than hoping the including file did.

A complete, well-formed header:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* for size_t, used below */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* shared across the program, defined once in inventory.c */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

Sharing a Global with extern

A global variable must be defined in exactly one .c file and declared everywhere else. extern is what makes the declaration:

/* inventory.h  - declaration, no storage */
extern int item_count;
/* inventory.c  - the one definition */
#include "inventory.h"
int item_count = 0;
/* main.c - uses it, via the header */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d items\n", item_count);
    return 0;
}

Drop the extern from the header and every including file defines its own item_count, which is a "multiple definition" link error at best and two independent counters at worst.

The opposite need is just as common: a variable or helper function that should stay private to one .c file. static at file scope does that - it gives the name internal linkage, invisible to the linker and therefore to every other file:

/* inventory.c */
static Item storage[256];          /* private to this file */
static int  find_slot(const char *name);   /* private helper */

Two files can each have a static int counter; with no collision. This is C's version of a private member, and it is the default you should reach for - only what other files genuinely need goes in the header.

Compiling Bigger Programs

Listing every file works and is slow, because every file is recompiled every time:

gcc main.c inventory.c report.c -o app

The scalable form compiles each source to an object file and links them:

gcc -c main.c        # produces main.o
gcc -c inventory.c   # produces inventory.o
gcc -c report.c      # produces report.o
gcc main.o inventory.o report.o -o app

Now changing report.c only needs gcc -c report.c and a relink. That is precisely the bookkeeping make automates:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

If your headers live in a subdirectory, -Iinclude adds it to the angle-bracket search path.

Errors and What They Mean

The two failure modes are easy to tell apart once you know which stage produced them.

"undefined reference to 'add'" - a linker error. The declaration was found, the definition was not. Either you forgot to list the .c file on the command line, or the function is static, or the name is misspelled in one of the two places.

"multiple definition of 'item_count'" - also a linker error, the mirror image: a definition ended up in a header, or in two source files. Move it to one .c and leave an extern declaration in the header.

"redefinition of 'struct Item'" - a compiler error, meaning a header was pasted twice into one file. Add the include guard.

"implicit declaration of function 'add'" - a compiler warning (an error in C99 and later modes) meaning the prototype was never seen. You forgot the #include, or the header does not declare it.

Once a program spans files, the next thing you usually want is to vary what gets compiled per platform or per build type - which is conditional compilation.

Frequently Asked Questions

What goes in a .h file and what goes in a .c file?

The header holds declarations - function prototypes, struct and typedef definitions, enums, macros, and extern declarations of shared globals. The .c file holds definitions - the function bodies and the actual variables. The rule of thumb: a header says what exists, a source file says what it does.

What is an include guard and why do I need one?

#include pastes text, so including a header twice pastes its contents twice - which redefines every struct and typedef in it and fails to compile. An include guard wraps the header in #ifndef MYHEADER_H / #define MYHEADER_H / #endif, so the second inclusion sees the macro already defined and skips the body.

How do I compile a C program with multiple files?

List every .c file on the command line: gcc main.c math_utils.c -o app. Never put a .h file there - headers are pasted in by #include, not compiled on their own. For larger projects, compile to object files (gcc -c main.c) and link them, which is what a Makefile automates.

Should I use #pragma once or #ifndef include guards?

Both work. #pragma once is one line and cannot have a name collision, and every mainstream compiler supports it - but it is not in the C standard. The #ifndef/#define/#endif form is standard and works everywhere. Pick one and use it consistently in a project; for maximum portability choose the #ifndef form.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED