What Are Header Files?
Header files are the glue that holds multi-file C programs together. They contain declarations that tell the compiler about functions, types, and variables defined elsewhere, enabling different parts of your program to communicate without knowing implementation details.
The Purpose of Header Files
Imagine you are writing a large program with thousands of lines of code. Putting everything in one file would be a nightmare to navigate, debug, and maintain. Header files solve this by acting as contracts or interfaces between different parts of your program.
When you write #include <stdio.h>, you are including a header file that declares
functions like printf() and scanf(). The actual code for these functions
lives in a library - the header just tells the compiler what the functions look like so it can
check your calls are correct.
Header File (.h)
A header file is a file with a .h extension that contains
declarations (not definitions) meant to be shared across multiple source files. It serves
as an interface, telling the compiler about types, functions, and variables without
providing the actual implementation.
Key principle: Headers declare, source files define. A header says "this function exists and takes these parameters" while the .c file provides the actual code.
What Belongs in Header Files
Put in Headers
- Function prototypes (declarations)
- Type definitions (struct, enum, typedef)
- Macro definitions (#define)
- External variable declarations (extern)
- Include guards
- Other #include directives needed
Keep Out of Headers
- Function definitions (implementations)
- Variable definitions (allocations)
- Static functions (file-local)
- Large amounts of code
- Executable statements
- Local variables
Standard Library Headers
You have been using standard headers throughout this course. Here are some common ones and what they provide:
| Header | Purpose | Key Declarations |
|---|---|---|
<stdio.h> |
Standard I/O | printf, scanf, FILE, fopen, fclose |
<stdlib.h> |
General utilities | malloc, free, exit, atoi, rand |
<string.h> |
String operations | strlen, strcpy, strcmp, memcpy |
<math.h> |
Math functions | sqrt, pow, sin, cos, log |
<stdbool.h> |
Boolean type | bool, true, false |
<stdint.h> |
Fixed-width integers | int32_t, uint8_t, INT_MAX |
How Headers Work
When the preprocessor encounters #include, it literally copies the contents of
the header file into your source file. This is pure text substitution - there is nothing
magical about it.
// Before preprocessing
#include <stdio.h>
int main() {
printf("Hello\n");
return 0;
}
// After preprocessing (simplified)
// ... thousands of lines from stdio.h ...
int printf(const char *format, ...);
// ... more declarations ...
int main() {
printf("Hello\n");
return 0;
}
Practice Questions
Task: Which of these should go in a header file? Mark each as Header or Source.
1. int add(int a, int b);
2. int add(int a, int b) { return a + b; }
3. typedef struct { int x, y; } Point;
4. #define MAX_SIZE 100
5. int counter = 0;
6. extern int globalCount;
Show Solution
int add(int a, int b);- Header (function declaration/prototype)int add(...) { return a + b; }- Source (function definition)typedef struct...- Header (type definition)#define MAX_SIZE 100- Header (macro definition)int counter = 0;- Source (variable definition with allocation)extern int globalCount;- Header (external variable declaration)
Task: Which header file do you need to include for each function?
1. strlen()
2. malloc()
3. sqrt()
4. printf()
5. isalpha()
Show Solution
#include <string.h> // 1. strlen()
#include <stdlib.h> // 2. malloc()
#include <math.h> // 3. sqrt()
#include <stdio.h> // 4. printf()
#include <ctype.h> // 5. isalpha()
Creating Custom Header Files
Creating your own header files is essential for organizing larger programs. A well-designed header file provides a clean interface to your module, hiding implementation details while exposing only what other parts of the program need to know.
Basic Header File Structure
Every header file should follow a consistent structure: include guards at the top, necessary includes, type definitions, macro definitions, and finally function prototypes. This ordering ensures dependencies are resolved correctly.
// math_utils.h - A custom header file
#ifndef MATH_UTILS_H // Include guard start
#define MATH_UTILS_H
// Include other headers this header depends on
#include <stdbool.h>
// Macro definitions
#define PI 3.14159265359
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
// Type definitions
typedef struct {
double x;
double y;
} Point;
typedef struct {
Point center;
double radius;
} Circle;
// Function prototypes (declarations only!)
double distance(Point a, Point b);
double circle_area(Circle c);
double circle_circumference(Circle c);
bool point_in_circle(Point p, Circle c);
#endif // MATH_UTILS_H - Include guard end
The Corresponding Source File
The source file (.c) includes its own header and provides the actual implementations:
// math_utils.c - Implementation file
#include "math_utils.h" // Include own header first
#include <math.h> // For sqrt()
// Function definitions (implementations)
double distance(Point a, Point b) {
double dx = b.x - a.x;
double dy = b.y - a.y;
return sqrt(dx * dx + dy * dy);
}
double circle_area(Circle c) {
return PI * c.radius * c.radius;
}
double circle_circumference(Circle c) {
return 2 * PI * c.radius;
}
bool point_in_circle(Point p, Circle c) {
return distance(p, c.center) <= c.radius;
}
Using Your Custom Header
Now any file can use your math utilities by including the header:
// main.c - Uses the math_utils module
#include <stdio.h>
#include "math_utils.h" // Your custom header
int main() {
Point p1 = {0.0, 0.0};
Point p2 = {3.0, 4.0};
printf("Distance: %.2f\n", distance(p1, p2)); // 5.00
Circle c = {{0.0, 0.0}, 5.0};
printf("Area: %.2f\n", circle_area(c)); // 78.54
if (point_in_circle(p2, c)) {
printf("Point is inside the circle\n");
}
return 0;
}
Header File Naming Conventions
| Convention | Example | When to Use |
|---|---|---|
| Module name | database.h |
General purpose modules |
| Feature prefix | str_utils.h |
Utility libraries |
| Project prefix | myapp_config.h |
Project-specific headers |
| Internal marker | internal.h |
Implementation details |
Best Practice: Self-Contained Headers
A header should include everything it needs to compile on its own. If your header uses
bool, include <stdbool.h>. If it uses size_t,
include <stddef.h>. Users should not need to know what other headers
to include first.
Practice Questions
Task: Create a header file temperature.h with functions to convert between Celsius and Fahrenheit.
Show Solution
// temperature.h
#ifndef TEMPERATURE_H
#define TEMPERATURE_H
// Convert Celsius to Fahrenheit
double celsius_to_fahrenheit(double celsius);
// Convert Fahrenheit to Celsius
double fahrenheit_to_celsius(double fahrenheit);
// Check if temperature is freezing (Celsius)
int is_freezing(double celsius);
#endif // TEMPERATURE_H
Task: Create a header student.h with a Student struct and functions to create, print, and compare students.
Show Solution
// student.h
#ifndef STUDENT_H
#define STUDENT_H
#include <stdbool.h>
#define MAX_NAME_LENGTH 50
typedef struct {
int id;
char name[MAX_NAME_LENGTH];
float gpa;
} Student;
// Create a new student
Student create_student(int id, const char *name, float gpa);
// Print student information
void print_student(const Student *s);
// Compare two students by GPA (returns true if a has higher GPA)
bool compare_gpa(const Student *a, const Student *b);
// Check if student is on honor roll (GPA >= 3.5)
bool is_honor_roll(const Student *s);
#endif // STUDENT_H
Task: Create both header and source files for a simple stack data structure.
Show Solution
// stack.h
#ifndef STACK_H
#define STACK_H
#include <stdbool.h>
#define STACK_CAPACITY 100
typedef struct {
int items[STACK_CAPACITY];
int top;
} Stack;
// Initialize an empty stack
void stack_init(Stack *s);
// Push an item onto the stack
bool stack_push(Stack *s, int value);
// Pop an item from the stack
bool stack_pop(Stack *s, int *value);
// Peek at the top item without removing it
bool stack_peek(const Stack *s, int *value);
// Check if stack is empty
bool stack_is_empty(const Stack *s);
// Check if stack is full
bool stack_is_full(const Stack *s);
// Get current size of stack
int stack_size(const Stack *s);
#endif // STACK_H
// ========================================
// stack.c
#include "stack.h"
void stack_init(Stack *s) {
s->top = -1;
}
bool stack_push(Stack *s, int value) {
if (stack_is_full(s)) return false;
s->items[++s->top] = value;
return true;
}
bool stack_pop(Stack *s, int *value) {
if (stack_is_empty(s)) return false;
*value = s->items[s->top--];
return true;
}
bool stack_peek(const Stack *s, int *value) {
if (stack_is_empty(s)) return false;
*value = s->items[s->top];
return true;
}
bool stack_is_empty(const Stack *s) {
return s->top == -1;
}
bool stack_is_full(const Stack *s) {
return s->top == STACK_CAPACITY - 1;
}
int stack_size(const Stack *s) {
return s->top + 1;
}
Include Guards and #pragma once
Include guards are essential for preventing multiple inclusion of header files, which would cause redefinition errors. Every header file you create should have include guards - no exceptions.
The Multiple Inclusion Problem
Without include guards, including the same header twice (directly or indirectly) causes errors:
// point.h (NO include guards - BAD!)
typedef struct {
int x, y;
} Point;
// graphics.h
#include "point.h" // Includes Point definition
typedef struct {
Point start, end;
} Line;
// main.c
#include "point.h" // Point defined here
#include "graphics.h" // Includes point.h again!
// ERROR: redefinition of 'Point'
Traditional Include Guards
Include guards use preprocessor conditionals to ensure header contents are processed only once:
// point.h (WITH include guards - GOOD!)
#ifndef POINT_H // If POINT_H is NOT defined...
#define POINT_H // ...define it
typedef struct {
int x, y;
} Point;
Point create_point(int x, int y);
double distance(Point a, Point b);
#endif // POINT_H // End of conditional
// Now including point.h multiple times is safe:
// First include: POINT_H undefined, contents processed, POINT_H defined
// Second include: POINT_H already defined, contents skipped
Naming Conventions for Include Guards
| Style | Example | Notes |
|---|---|---|
| Simple uppercase | FILENAME_H |
Most common, simple |
| With underscores | _FILENAME_H_ |
Avoid - reserved for implementation |
| Project prefix | MYPROJECT_MODULE_H |
Prevents conflicts across projects |
| Path-based | SRC_UTILS_STRING_H |
Includes directory structure |
| UUID-based | H_5A8F3... |
Guaranteed unique, less readable |
#pragma once Alternative
Modern compilers support #pragma once as a simpler alternative to include guards:
// point.h using #pragma once
#pragma once
typedef struct {
int x, y;
} Point;
Point create_point(int x, int y);
double distance(Point a, Point b);
#pragma once Pros
- Simpler - just one line
- No need to invent unique names
- Cannot have typos in guard name
- Faster compilation (sometimes)
#pragma once Cons
- Not in C standard (compiler extension)
- May fail with symbolic links
- Not supported by all compilers
- Behavior can vary across platforms
Belt-and-Suspenders Approach
For maximum compatibility, you can use both methods:
// point.h - Using both methods
#ifndef POINT_H
#define POINT_H
#pragma once // Compiler uses whichever it prefers
typedef struct {
int x, y;
} Point;
#endif // POINT_H
Common Mistake: Mismatched Guards
Always ensure your #ifndef, #define, and #endif
use the exact same macro name. A typo like #ifndef POINT_H followed by
#define PONIT_H will not work!
Practice Questions
Task: Add proper include guards to this header file:
// config.h
#define VERSION "1.0.0"
#define MAX_USERS 100
#define DEBUG_MODE 1
Show Solution
// config.h
#ifndef CONFIG_H
#define CONFIG_H
#define VERSION "1.0.0"
#define MAX_USERS 100
#define DEBUG_MODE 1
#endif // CONFIG_H
Task: Find and fix the bug in these include guards:
// utils.h
#ifndef UTILS_H
#define UTIL_H
int max(int a, int b);
int min(int a, int b);
#endif
Show Solution
// utils.h - Fixed version
#ifndef UTILS_H
#define UTILS_H // Fixed: was UTIL_H, now UTILS_H
int max(int a, int b);
int min(int a, int b);
#endif // UTILS_H
// The bug: #ifndef checks UTILS_H but #define sets UTIL_H (missing 'S')
// This means the guard never works - the header can be included multiple times
Declarations vs Definitions
Understanding the difference between declarations and definitions is crucial for proper header file design. Confusing them leads to linker errors that can be difficult to debug.
The Key Difference
Declaration vs Definition
A declaration introduces a name and tells the compiler about its type, but does not allocate storage or provide implementation. A definition creates the actual entity - it allocates memory for variables or provides code for functions.
Rule of thumb: You can declare something multiple times (in multiple files), but you can only define it once across the entire program. This is called the One Definition Rule (ODR).
Function Declarations and Definitions
// DECLARATION (prototype) - goes in header
int add(int a, int b); // No body - just the signature
int multiply(int, int); // Parameter names optional in declaration
// DEFINITION (implementation) - goes in source file
int add(int a, int b) { // Has body with actual code
return a + b;
}
int multiply(int x, int y) {
return x * y;
}
Variable Declarations and Definitions
// DEFINITION - creates the variable (allocates memory)
int counter = 0; // Definition with initialization
double pi = 3.14159; // Definition
int values[100]; // Definition - allocates array
// DECLARATION - tells compiler about variable defined elsewhere
extern int counter; // Declaration only (extern keyword)
extern double pi; // Declaration only
extern int values[]; // Declaration (size optional)
// In practice:
// config.c
int globalConfig = 42; // Definition
// config.h
extern int globalConfig; // Declaration - so other files can use it
Type Declarations and Definitions
// DECLARATION - incomplete type (forward declaration)
struct Node; // Declares Node exists, no details
typedef struct Node Node; // Can create typedef from declaration
// DEFINITION - complete type
struct Node { // Defines the actual structure
int data;
struct Node *next;
};
// Type definitions can appear in headers because they don't allocate memory
// They just tell the compiler how types are structured
Summary Table
| Element | Declaration (Header) | Definition (Source) |
|---|---|---|
| Function | int func(int x); |
int func(int x) { return x*2; } |
| Variable | extern int count; |
int count = 0; |
| Struct | struct Data; |
struct Data { int x; }; |
| Typedef | typedef int MyInt; (same in both - no memory allocated) |
|
| Macro | #define MAX 100 (same in both - preprocessor) |
|
The extern Keyword
The extern keyword is how you declare a variable that is defined elsewhere:
// globals.c - The ONE definition
int errorCount = 0;
char programName[256] = "MyApp";
// globals.h - Declarations for other files to use
#ifndef GLOBALS_H
#define GLOBALS_H
extern int errorCount; // "There's an int errorCount somewhere"
extern char programName[]; // "There's a char array somewhere"
#endif
// main.c - Using the global variables
#include "globals.h"
void logError(const char *msg) {
errorCount++; // Uses the variable defined in globals.c
printf("[%s] Error #%d: %s\n", programName, errorCount, msg);
}
Practice Questions
Task: Mark each line as Declaration (D) or Definition (Def):
1. int calculate(int x);
2. int calculate(int x) { return x * 2; }
3. extern double PI;
4. double PI = 3.14159;
5. typedef int Integer;
6. struct Person { char name[50]; int age; };
Show Solution
int calculate(int x);- Declaration (no body)int calculate(int x) { return x * 2; }- Definition (has body)extern double PI;- Declaration (extern = declaration)double PI = 3.14159;- Definition (allocates memory)typedef int Integer;- Definition (type alias definition)struct Person {...};- Definition (complete struct definition)
Task: This code causes "multiple definition" linker errors. Fix it:
// counter.h
#ifndef COUNTER_H
#define COUNTER_H
int count = 0; // Problem!
void increment(void);
int get_count(void);
#endif
Show Solution
// counter.h - Fixed
#ifndef COUNTER_H
#define COUNTER_H
extern int count; // Declaration only
void increment(void);
int get_count(void);
#endif
// counter.c - Add definition here
#include "counter.h"
int count = 0; // Definition in ONE source file
void increment(void) {
count++;
}
int get_count(void) {
return count;
}
Task: These two structs need to reference each other. Use forward declarations to make it work:
// person.h - Person has a pointer to their Company
// company.h - Company has a pointer to its CEO (Person)
// Currently causes circular dependency!
Show Solution
// person.h
#ifndef PERSON_H
#define PERSON_H
struct Company; // Forward declaration
typedef struct Person {
char name[50];
struct Company *employer; // Can use pointer to incomplete type
} Person;
Person *create_person(const char *name);
#endif
// company.h
#ifndef COMPANY_H
#define COMPANY_H
struct Person; // Forward declaration
typedef struct Company {
char name[100];
struct Person *ceo; // Can use pointer to incomplete type
int employee_count;
} Company;
Company *create_company(const char *name);
#endif
// Note: Forward declarations work for pointers because all pointers
// are the same size. You cannot use forward declarations where
// the compiler needs to know the struct's size.
Multi-File Project Organization
As your C projects grow, proper organization becomes critical. A well-structured project is easier to understand, maintain, debug, and extend. Let us look at how to organize files and compile multi-file projects.
Typical Project Structure
my_project/
├── include/ # Header files
│ ├── math_utils.h
│ ├── string_utils.h
│ └── data_types.h
├── src/ # Source files
│ ├── main.c
│ ├── math_utils.c
│ └── string_utils.c
├── tests/ # Test files
│ └── test_math.c
├── build/ # Compiled objects (generated)
├── bin/ # Executables (generated)
├── Makefile # Build configuration
└── README.md # Documentation
Example: Calculator Project
Let us build a simple calculator with proper modular design:
// include/calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
// Basic operations
double add(double a, double b);
double subtract(double a, double b);
double multiply(double a, double b);
double divide(double a, double b, int *error);
// Advanced operations
double power(double base, int exponent);
double square_root(double x, int *error);
// Error codes
#define CALC_OK 0
#define CALC_DIV_BY_ZERO 1
#define CALC_NEGATIVE_SQRT 2
#endif // CALCULATOR_H
// src/calculator.c
#include "calculator.h"
#include <math.h>
double add(double a, double b) {
return a + b;
}
double subtract(double a, double b) {
return a - b;
}
double multiply(double a, double b) {
return a * b;
}
double divide(double a, double b, int *error) {
if (b == 0.0) {
*error = CALC_DIV_BY_ZERO;
return 0.0;
}
*error = CALC_OK;
return a / b;
}
double power(double base, int exponent) {
return pow(base, exponent);
}
double square_root(double x, int *error) {
if (x < 0) {
*error = CALC_NEGATIVE_SQRT;
return 0.0;
}
*error = CALC_OK;
return sqrt(x);
}
// src/main.c
#include <stdio.h>
#include "calculator.h"
int main() {
int error;
printf("5 + 3 = %.2f\n", add(5, 3)); // 8.00
printf("10 - 4 = %.2f\n", subtract(10, 4)); // 6.00
printf("6 * 7 = %.2f\n", multiply(6, 7)); // 42.00
double result = divide(10, 2, &error);
if (error == CALC_OK) {
printf("10 / 2 = %.2f\n", result); // 5.00
}
result = divide(10, 0, &error);
if (error == CALC_DIV_BY_ZERO) {
printf("Error: Division by zero!\n");
}
printf("2^10 = %.2f\n", power(2, 10)); // 1024.00
return 0;
}
Compiling Multi-File Projects
# Method 1: Compile all at once
gcc -I include src/main.c src/calculator.c -o calculator -lm
# Method 2: Compile separately (better for large projects)
gcc -I include -c src/calculator.c -o build/calculator.o
gcc -I include -c src/main.c -o build/main.o
gcc build/calculator.o build/main.o -o bin/calculator -lm
# The -I flag adds include/ to the header search path
# The -c flag compiles without linking (creates .o object file)
# The -lm links the math library (needed for sqrt, pow)
Using a Makefile
Makefiles automate the build process and only recompile changed files:
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -I include
LDFLAGS = -lm
SRCDIR = src
BUILDDIR = build
BINDIR = bin
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SOURCES))
TARGET = $(BINDIR)/calculator
all: $(TARGET)
$(TARGET): $(OBJECTS) | $(BINDIR)
$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILDDIR) $(BINDIR):
mkdir -p $@
clean:
rm -rf $(BUILDDIR) $(BINDIR)
.PHONY: all clean
Include Order Best Practices
// Recommended include order in source files:
// 1. Own header first (catches missing dependencies)
#include "mymodule.h"
// 2. System headers
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 3. Third-party library headers
#include <sqlite3.h>
#include <curl/curl.h>
// 4. Other project headers
#include "utils.h"
#include "config.h"
Why Own Header First?
Including your own header first ensures it is self-contained. If mymodule.h
needs <stdlib.h> but forgot to include it, you will get an error
immediately rather than having it accidentally work because some other file included
stdlib first.
Practice Questions
Task: Write the gcc command to compile these files:
project/
├── include/
│ └── utils.h
├── src/
│ ├── main.c
│ └── utils.c
Show Solution
# All at once:
gcc -I include src/main.c src/utils.c -o program
# Or separately:
gcc -I include -c src/utils.c -o utils.o
gcc -I include -c src/main.c -o main.o
gcc utils.o main.o -o program
Task: Design header and source file structure for a simple bank account module with: create account, deposit, withdraw, get balance.
Show Solution
// account.h
#ifndef ACCOUNT_H
#define ACCOUNT_H
#include <stdbool.h>
typedef struct {
int id;
char holder_name[100];
double balance;
} Account;
// Create a new account
Account create_account(int id, const char *name, double initial_balance);
// Deposit money (returns true if successful)
bool deposit(Account *acc, double amount);
// Withdraw money (returns true if successful)
bool withdraw(Account *acc, double amount);
// Get current balance
double get_balance(const Account *acc);
// Print account info
void print_account(const Account *acc);
#endif // ACCOUNT_H
// account.c
#include "account.h"
#include <stdio.h>
#include <string.h>
Account create_account(int id, const char *name, double initial) {
Account acc;
acc.id = id;
strncpy(acc.holder_name, name, 99);
acc.holder_name[99] = '\0';
acc.balance = initial > 0 ? initial : 0;
return acc;
}
bool deposit(Account *acc, double amount) {
if (amount <= 0) return false;
acc->balance += amount;
return true;
}
bool withdraw(Account *acc, double amount) {
if (amount <= 0 || amount > acc->balance) return false;
acc->balance -= amount;
return true;
}
double get_balance(const Account *acc) {
return acc->balance;
}
void print_account(const Account *acc) {
printf("Account #%d: %s, Balance: $%.2f\n",
acc->id, acc->holder_name, acc->balance);
}
Key Takeaways
Headers Are Interfaces
Headers declare what a module provides; source files contain the implementation
Always Use Include Guards
#ifndef/#define/#endif or #pragma once prevents multiple inclusion errors
Declarations vs Definitions
Declare in headers (can repeat), define in source files (only once)
Use extern for Variables
extern declares variables defined elsewhere; define in one .c file only
Organize Large Projects
Separate include/, src/, build/ directories for clean project structure
Self-Contained Headers
Headers should include everything they need - do not rely on include order
Knowledge Check
Quick Quiz
Test what you have learned about C header files and modular design