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

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.

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

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 .h and .c files with interface separated from implementation
  • Write include guards and say what they prevent
  • State the one-definition rule and use extern to share a constant across files
  • Use gcc -c to 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 main signatures

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 ;)
  • #define constants
  • typedef declarations
  • struct definitions when more than one .c file needs to see the fields
  • extern declarations 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

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:

  • argc is 4
  • argv[0] is "./calculator"
  • argv[1] is "10"
  • argv[2] is "+"
  • argv[3] is "5"
  • argv[4] is NULL

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

Select every statement that is true.
AInclude guards prevent a header from being pasted into the same translation unit twice, but they do not prevent a variable definition in a header from causing a linker error when two .c files include the header.
Bgcc -c produces an executable; you run it with ./file.o.
Cargv[argc] is guaranteed by the C standard to be NULL.
DMakefile recipe lines must begin with a TAB character; "missing separator" almost always means a recipe line starts with spaces.
Eextern int x; in a header allocates storage for x; you do not need a matching definition in any .c file.
Fint main(int argc, char **argv) and int main(int argc, char *argv[]) are two legal ways to spell the same signature.
Correct: A, C, D, F.
  • B is wrong: gcc -c produces an object file, not an executable. Binaries come from the link step.
  • E is wrong: extern is a declaration, not a definition. Without a matching definition somewhere in the program, the linker reports undefined reference.

Next, Sorting & Searching puts this structure to work. Drill this page: Headers & Makefiles · CLI Args · practice gallery.