Header Files & Multi-file Programs

C · Lesson 25 of 32 7 min read Updated

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.c can be used by several programs.
  • Faster builds. When you change one file, only that file needs compiling again.
  • Hiding details. Helper functions marked static stay 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.

C
// 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
C
// 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);
}
C
// 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.

Terminal
gcc -std=c23 -Wall -Wextra main.c bill.c -o shop
./shop
Output
Notebook     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 headerKeep in a .c file
Function prototypesFunction definitions
typedef, struct, enum and union typesstatic helper functions
Macros and constants that callers needGlobal variable definitions (int count = 0;)
extern declarations of shared variablesAnything 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:

StageOption to stop hereResult
Preprocessgcc -E main.cExpanded C text
Compilegcc -S main.cmain.s, assembly language
Assemblegcc -c main.cmain.o, an object file of machine code
Linkgcc main.o bill.o -o shopThe 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:

Terminal
gcc -std=c23 -Wall -Wextra -c bill.c
gcc -std=c23 -Wall -Wextra -c main.c
gcc main.o bill.o -o shop

Now, 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):

Terminal
gcc -std=c23 -Wall -Wextra main.c -o shop
Output
/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 .c file. #include "bill.c" pastes the definitions into another file and leads to multiple definitions. Include headers only.
  • Defining variables in a header. Declare with extern in the header and define in one .c file.
  • 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 extern declarations instead.
  • gcc -c compiles 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 static so 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
C
// convert.h
#ifndef CONVERT_H
#define CONVERT_H

double to_fahrenheit(double c);
double to_celsius(double f);

#endif
C
// 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;
}
C
// 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;
}
Terminal
gcc -std=c23 -Wall -Wextra main.c convert.c -o convert
./convert
Output
37.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?

2. What does gcc -c bill.c produce?

3. What does "undefined reference to `item_total'" usually mean?

4. What is the purpose of #ifndef BILL_H / #define BILL_H / #endif?

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