Headers, Makefiles & CLI Args
Multi-file projects, automated builds, and command-line argument parsing
Quick check before you start: Can you explain the difference between a function declaration and a function definition? If so, skip to Include Guards. If not, read on.
Practice this topic: Headers & Makefiles and CLI Args skill drills
After this lesson, you will be able to:
- Split a C program into
.hand.cfiles with proper separation of interface and implementation - Write include guards (
#ifndef/#define/#endif) in every header file - Use
gcc -cto compile individual source files and link object files together - Write a Makefile with targets, prerequisites, and recipes
- Accept command-line arguments using
argcandargv - Convert string arguments to numbers with
atoiandatol
Header Files: Interface vs. Implementation
Your programs are growing. Putting everything in one file becomes unmanageable. C’s solution is separate compilation: split your code into multiple .c files, each with a corresponding .h header file.
A header file is an interface — it declares what a module provides without showing how:
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
double calculate_area(double radius);
double calculate_circumference(double radius);
#define PI 3.14159265358979
#endif
Headers contain function prototypes (declarations), #define constants, and typedef declarations. Headers do NOT contain function bodies or variable definitions.
The implementation goes in the .c file:
/* math_utils.c */
#include "math_utils.h"
double calculate_area(double radius)
{
return PI * radius * radius;
}
double calculate_circumference(double radius)
{
return 2.0 * PI * radius;
}
Note: #include "math_utils.h" uses quotes for your own headers. Angle brackets (<stdio.h>) are for system headers.
From Java: A
.hfile is like a Java interface — it declares what is available. A.cfile is like the implementing class.#include "math_utils.h"is likeimport. The difference: C’s#includeliterally pastes the header’s text into your file, while Java’simportis a name lookup.
Include Guards
Those #ifndef/#define/#endif lines are include guards:
#ifndef MATH_UTILS_H // If MATH_UTILS_H is NOT defined...
#define MATH_UTILS_H // ...define it (so this block runs only once)
// ... declarations ...
#endif // End the conditional
Include guards prevent a header from being processed twice. Without them, if A.h includes B.h and C.h also includes B.h, then a file that includes both A.h and C.h gets B.h’s contents twice — causing “duplicate definition” errors. The convention is to use the filename in uppercase with underscores: MATH_UTILS_H for math_utils.h.
Separate Compilation and Makefiles
Compiling Step by Step
Compile each .c file to an object file (.o), then link them together:
gcc -Wall -c math_utils.c # Produces math_utils.o
gcc -Wall -c main.c # Produces main.o
gcc -o calculator math_utils.o main.o # Link into executable
The -c flag means “compile only, do not link.” Change main.c? Only recompile main.c, not math_utils.c.
Writing a Makefile
A Makefile automates the build process. Each rule has three parts:
target: prerequisites
recipe (MUST use TAB, not spaces!)
A complete example:
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
calculator: main.o math_utils.o
$(CC) $(CFLAGS) -o calculator main.o math_utils.o
main.o: main.c math_utils.h
$(CC) $(CFLAGS) -c main.c
math_utils.o: math_utils.c math_utils.h
$(CC) $(CFLAGS) -c math_utils.c
clean:
rm -f calculator *.o
Run it:
make # Builds the first target (calculator)
make clean # Deletes build files
make checks file timestamps and only recompiles files that changed. Edit math_utils.c but not main.c? Only math_utils.c gets recompiled, then the final link step reruns.
Common Pitfall: Makefiles require a TAB character before each recipe line, not spaces. If you see “missing separator” errors, check your tabs. This is the single most common Makefile error.
Command-Line Arguments: argc and argv
Every Unix command you have used takes arguments: ls -la /home, grep -rn "printf" *.c, gcc -Wall -o hello hello.c. Your programs can do the same.
The Full main Signature
Until now, we have written int main(void). The full version accepts command-line arguments:
int main(int argc, char *argv[])
argc— argument count (number of arguments, including the program name)argv— argument vector (array of strings)
If you run ./calculator 10 + 5:
argc= 4argv[0]="./calculator"(program name)argv[1]="10"argv[2]="+"argv[3]="5"
Common Pitfall:
argcincludes the program name, so./program file1 file2hasargc = 3, not 2. The actual arguments start atargv[1].
Validating Arguments
Always check that the user provided the right number of arguments:
if (argc != 3)
{
printf("Usage: %s <filename> <count>\n", argv[0]);
return 1;
}
Using argv[0] in the usage message shows the actual program name the user typed.
Converting Arguments to Numbers
Command-line arguments are always strings. To use them as numbers:
#include <stdlib.h>
int count = atoi(argv[1]); // String to int
long big = atol(argv[2]); // String to long
double rate = atof(argv[3]); // String to double
Common Pitfall:
atoihas no error checking —atoi("hello")returns 0 without any indication that the conversion failed. For labs,atoi/atolare fine. For production code, usestrtolorstrtodwhich report errors.
From Java: In Java,
public static void main(String[] args)serves the same purpose.args[0]is the first argument (not the class name). In C,argv[0]is the program name andargv[1]is the first user argument. The shift by one is important.
#ifndef MATH_UTILS_H do at the top of a header file?#ifndef means "if not defined." The first time the preprocessor encounters this header, MATH_UTILS_H is not defined, so it processes the contents and defines the macro via #define MATH_UTILS_H. If the same header is included again (through a chain of includes), the macro is already defined, so the entire block is skipped. This prevents duplicate declaration errors when multiple files include the same header.
What Comes Next
You can now organize multi-file C projects with headers and Makefiles, and accept input from the command line. In the next lesson, you will implement sorting and searching algorithms on arrays — building by hand what Java’s Arrays.sort() hides from you.