Headers, Makefiles & CLI Args
Multi-file projects, include guards, extern, automated builds, and argc / argv
Based on content from Dr. Stu Steiner, Eastern Washington University.
In a nutshell
In Java, when Main.java needed Car, the compiler found Car.class on the classpath and worked out the rest. In C you do that work by hand. A .h file declares what a module provides; a .c file provides it; every variable or function must have exactly one definition in the whole program. extern is how you declare a shared constant without defining it. make decides which .c files changed and therefore which .o files need to be rebuilt. argc / argv let your program read its command-line arguments.
Practice this topic: Headers & Makefiles or CLI Args drills, or browse the practice gallery.
After this lesson, you will be able to:
- Split a C program into
.hand.cfiles with interface separated from implementation - Write include guards and say what they prevent
- State the one-definition rule and use
externto share a constant across files - Use
gcc -cto compile individual source files and link the objects - Write a Makefile with targets, prerequisites, and recipes
- Accept command-line arguments and know the three legal
mainsignatures
Quick reference
| Purpose | .h? |
.c? |
|---|---|---|
Function prototype: int readCount(void); |
Yes | No (unless local) |
Function body: int readCount(void) { ... } |
No | Yes |
typedef struct Node Node; |
Yes | No |
#define CONSTANT 42 |
Yes | Yes |
Variable definition: const double FLAT_R = 5.00; |
No | Yes (exactly one file) |
Variable declaration: extern const double FLAT_R; |
Yes | (covered by #include) |
Coming from CSCD 210
In Java, an interface declared the methods a class provided, and a class implemented them. Headers and source files play the same two roles in C: .h is the contract, .c is the implementation. The mechanical difference: Java’s import is a name lookup the compiler does at link time, while C’s #include literally pastes the header’s text into the source file before compiling. That simpler, cruder mechanism is why include guards exist.
Headers and the one-definition rule
Interface vs. implementation
A header file 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 /* MATH_UTILS_H */
Headers contain:
- Function prototypes (declarations, ending in
;) #defineconstantstypedefdeclarationsstructdefinitions when more than one.cfile needs to see the fieldsexterndeclarations for shared variables
Headers do not contain function bodies or variable definitions that allocate storage.
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;
}
#include "math_utils.h" uses double quotes for your own headers; angle brackets (<stdio.h>) are for system headers. The preprocessor searches the current directory first for quoted includes and jumps straight to the system directories for angle-bracket includes.
Include guards
The #ifndef / #define / #endif wrapper is an include guard:
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* ... declarations ... */
#endif
The first time the preprocessor encounters the header, MATH_UTILS_H is not defined. It defines the macro and processes the rest. The second time the same header is pasted in (from a chain like A.h includes B.h and C.h also includes B.h, then main.c includes both), the macro is already defined and everything between #ifndef and #endif is skipped. Without the guard, the second inclusion triggers duplicate-declaration errors.
Convention: macro name is the filename uppercased with underscores. math_utils.h becomes MATH_UTILS_H.
Declarations vs. definitions
A declaration says “something with this name exists, here is its type.” A definition says “here it is; allocate storage.” For any variable or function, the program must have exactly one definition and as many declarations as it wants. Headers hold declarations; exactly one .c file holds the definition.
/* WRONG: this header, included in two .c files, produces
two definitions of FLAT_R. The linker refuses. */
/* constants.h */
const double FLAT_R = 5.00;
The correct pattern: put the definition in exactly one .c file and announce it in the header with extern.
/* main.c (or constants.c; just pick one) */
const double FLAT_R = 5.00;
/* constants.h */
extern const double FLAT_R; /* declaration only; no storage */
extern says “this name refers to storage defined elsewhere; the linker will find it at link time.” Many declarations, one definition.
Check your understanding (what is wrong?)
/* util.h */
#ifndef UTIL_H
#define UTIL_H
const double TAX_RATE = 0.085;
#endif
main.c and report.c both #include "util.h". The compile succeeds, but the linker reports multiple definition of 'TAX_RATE'. Why, and what is the minimal fix?
Reveal answer
Include guards prevent the header from being pasted twice into the same .c file. They cannot prevent it from being pasted once into each of two different .c files, which produces two independent copies of const double TAX_RATE = 0.085; (one per translation unit). The linker sees both and refuses.
Fix:
/* util.h */
#ifndef UTIL_H
#define UTIL_H
extern const double TAX_RATE;
#endif
/* util.c (exactly one file) */
#include "util.h"
const double TAX_RATE = 0.085;
Build automation with make
Compile each .c to an object file (.o), then link them:
gcc -Wall -Wextra -std=c99 -c math_utils.c # produces math_utils.o
gcc -Wall -Wextra -std=c99 -c main.c # produces main.o
gcc -o calculator math_utils.o main.o # link into the executable
-c means “compile only, do not link.” If only main.c changed, you only recompile main.c and relink; math_utils.o stays put. make automates exactly that decision.
A Makefile
Each rule has three parts: target: prerequisites on one line, then the recipe indented with a TAB (not spaces).
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -pedantic -g
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 artifacts
make compares file timestamps and only rebuilds what is out of date.
Course policy on -std=:
- Week 4 labs:
-std=c90 -pedantic. - Week 5 and later:
-std=c99 -pedantic. - Newer standards (
-std=c11,-std=c17) add features this course does not require. Do not opt into them without a reason.
Recipe lines must start with a TAB, not spaces. If make reports missing separator, the line in question has spaces. This is by far the most common Makefile error.
Command-line arguments
The three legal main signatures
| Signature | When you write it |
|---|---|
int main(void) |
Program takes no arguments (Lab 1). |
int main(int argc, char **argv) |
Program takes arguments. |
int main(int argc, char *argv[]) |
Equivalent syntax for the array-of-pointers form. |
What argc and argv contain
argc is the argument count, including the program name. argv is an array of C strings, one per argument. The C standard guarantees that argv[argc] is NULL: a sentinel.
If you run ./calculator 10 + 5:
argcis4argv[0]is"./calculator"argv[1]is"10"argv[2]is"+"argv[3]is"5"argv[4]isNULL
argc includes the program name, so ./program file1 file2 has argc == 3, not 2. The user’s arguments start at argv[1].
Validating arguments
Always check you got what you need:
if (argc != 3) {
fprintf(stderr, "Usage: %s <filename> <count>\n", argv[0]);
return 1;
}
Using argv[0] in the usage message shows the program name as the user typed it. Writing to stderr keeps usage messages out of pipelines that consume stdout.
Converting arguments to numbers
Command-line arguments are always strings. To use one as a number:
#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 */
atoi has no error checking: atoi("hello") returns 0 with no way to tell “zero” from “parse failed.” For coursework that is acceptable. For production, use strtol / strtod, which set errno and an end-pointer you can inspect.
What comes next
- B is wrong:
gcc -cproduces an object file, not an executable. Binaries come from the link step. - E is wrong:
externis a declaration, not a definition. Without a matching definition somewhere in the program, the linker reportsundefined reference.
Next, Sorting & Searching puts this structure to work. Drill this page: Headers & Makefiles · CLI Args · practice gallery.