student@ubuntu:~$
c-foundations Lesson 11 10 min read

Headers, Makefiles & CLI Args

Multi-file projects, automated builds, and command-line argument parsing

Reading: Hanly & Koffman: §12.1–12.3 (pp. 666–677), §12.7 (pp. 688–690)

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 .h and .c files with proper separation of interface and implementation
  • Write include guards (#ifndef/#define/#endif) in every header file
  • Use gcc -c to compile individual source files and link object files together
  • Write a Makefile with targets, prerequisites, and recipes
  • Accept command-line arguments using argc and argv
  • Convert string arguments to numbers with atoi and atol

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 .h file is like a Java interface — it declares what is available. A .c file is like the implementing class. #include "math_utils.h" is like import. The difference: C’s #include literally pastes the header’s text into your file, while Java’s import is 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[])
  • argcargument count (number of arguments, including the program name)
  • argvargument vector (array of strings)

If you run ./calculator 10 + 5:

  • argc = 4
  • argv[0] = "./calculator" (program name)
  • argv[1] = "10"
  • argv[2] = "+"
  • argv[3] = "5"

Common Pitfall: argc includes the program name, so ./program file1 file2 has argc = 3, not 2. The actual arguments start at argv[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: atoi has no error checking — atoi("hello") returns 0 without any indication that the conversion failed. For labs, atoi/atol are fine. For production code, use strtol or strtod which 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 and argv[1] is the first user argument. The shift by one is important.


Check Your Understanding
What does #ifndef MATH_UTILS_H do at the top of a header file?
AIt imports the math_utils module into the current file
BIt checks whether the file math_utils.h exists on disk
CIt checks whether the macro MATH_UTILS_H has already been defined, and skips the header contents if so
DIt prevents other files from modifying the functions declared in the header
Answer: C. #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.