Skip to content
Laviyx Learn

Try

C course menu

Header Files and Multi-file Programs in C: .h and .c Files, Linking and Makefiles

Split a C program across several files: what goes in a header (.h) and what goes in a source (.c) file, include guards, compiling and linking multiple files with gcc, static for private functions, extern for shared variables, and a simple Makefile.

Advanced8 min readUpdated
On this page

Real C programs are split across many files. Each .c file is compiled separately into an object file, and the linker joins the object files into one program. Header files (.h) let the files agree on what each one provides.

Header and source files#

  • A header (.h) holds declarations: function prototypes, type definitions (struct, typedef, enum), macros and extern variable declarations. It is the module's public interface.
  • A source file (.c) holds definitions: function bodies and variables. It #includes its own header so the compiler checks that the two match.

Here is a small "bank account" module and a program that uses it.

account.h (C)
#ifndef ACCOUNT_H
#define ACCOUNT_H

typedef struct {
    char holder[32];
    double balance;
} Account;

extern int accounts_opened;          /* defined in account.c */

Account account_open(const char *holder, double initial);
int account_withdraw(Account *acc, double amount);
void account_print(const Account *acc);

#endif /* ACCOUNT_H */
account.c (C)
#include <stdio.h>
#include "account.h"

int accounts_opened = 0;              /* the one definition */

static int valid_amount(double amount) {   /* private to this file */
    return amount > 0;
}

Account account_open(const char *holder, double initial) {
    Account a;
    snprintf(a.holder, sizeof a.holder, "%s", holder);
    a.balance = initial;
    accounts_opened++;
    return a;
}

int account_withdraw(Account *acc, double amount) {
    if (!valid_amount(amount) || amount > acc->balance) return 0;
    acc->balance -= amount;
    return 1;
}

void account_print(const Account *acc) {
    printf("%s: %.2f\n", acc->holder, acc->balance);
}
main.c (C)
#include <stdio.h>
#include "account.h"

int main(void) {
    Account a = account_open("Asha", 5000);
    Account b = account_open("Ravi", 1200);
    account_withdraw(&a, 750);
    if (!account_withdraw(&b, 2000)) printf("Ravi: insufficient funds\n");
    account_print(&a);
    account_print(&b);
    printf("%d accounts opened\n", accounts_opened);
    return 0;
}

Compiling and linking#

Compile all the source files together (headers are never compiled on their own; they are included):

Terminal
gcc -std=c17 -Wall -Wextra main.c account.c -o bank
./bank
Output
Ravi: insufficient funds
Asha: 4250.00
Ravi: 1200.00
2 accounts opened

Or compile each file to an object file, then link. Only the changed files need recompiling:

Terminal
gcc -std=c17 -Wall -Wextra -c account.c
gcc -std=c17 -Wall -Wextra -c main.c
gcc main.o account.o -o bank

Compiler errors versus linker errors#

  • Compiler errors come from one file: a syntax mistake, or a call that does not match a prototype.
  • Linker errors come from joining files. Forgetting to compile account.c gives an undefined reference to `account_open' error: the prototype promised the function, but no file defined it. Defining the same global function or variable in two files gives a multiple definition error.

static and extern across files#

  • A function or global variable declared static in a .c file is private to that file. valid_amount above cannot be called from main.c, and another file may use the same name without a clash. Make helper functions static by default.
  • A global variable shared between files is defined once in a .c file (int accounts_opened = 0;) and declared with extern in the header. Shared globals should be rare.

What not to put in a header#

  • Function bodies and variable definitions: every file that includes the header would define them again, causing multiple definition errors. (static inline functions are an exception.)
  • Unnecessary #includes: include what the header itself needs, and put the rest in the .c file.

A simple Makefile#

make rebuilds only what changed. A minimal Makefile for this project (the command lines must start with a tab):

Makefile (MAKEFILE)
CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g

bank: main.o account.o
	$(CC) main.o account.o -o bank

main.o: main.c account.h
	$(CC) $(CFLAGS) -c main.c

account.o: account.c account.h
	$(CC) $(CFLAGS) -c account.c

clean:
	rm -f *.o bank

Run make to build and make clean to delete the build files. Larger projects often use CMake, which generates build files for many platforms.

Practice#

Practice exercise

Split a small "temperature" module into temp.h and temp.c, providing double c_to_f(double c) and double f_to_c(double f), plus a private helper. Write a main.c that converts 37 °C and 98.6 °F, and give the command to build it.

Show solution
temp.h (C)
#ifndef TEMP_H
#define TEMP_H

double c_to_f(double c);
double f_to_c(double f);

#endif
temp.c (C)
#include "temp.h"

static double scale(double x, double factor, double offset) {
    return x * factor + offset;
}

double c_to_f(double c) { return scale(c, 9.0 / 5.0, 32.0); }
double f_to_c(double f) { return scale(f - 32.0, 5.0 / 9.0, 0.0); }
main.c (C)
#include <stdio.h>
#include "temp.h"

int main(void) {
    printf("%.1f F\n", c_to_f(37));
    printf("%.1f C\n", f_to_c(98.6));
    return 0;
}
Terminal
gcc -std=c17 -Wall -Wextra main.c temp.c -o temp
Output
98.6 F
37.0 C

Key takeaway#

Finished this lesson?