Chapter 15 of 24
15. Multi-file programs and libraries
Split a program into cooperating files
A small program can live in one source file. When it grows, separate pieces of work into modules so each part has a clear job. A module's header describes what other files may use; its source file contains the implementation.
- Write a function declaration in a header such as
math_utils.h. - Define that function in
math_utils.c, including its own header. - Include the header where needed, compile each source file, and link the resulting object files together.
The header is the module's public contract, not a place for unrelated code. The later example follows this path and also introduces command-line arguments.
As a program grows, split it into modules. A module typically has:
- A
.hheader describing the public interface. - A
.csource file implementing it.
Example math_utils.h:
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int left, int right);
#endif
Example math_utils.c:
#include "math_utils.h"
int add(int left, int right)
{
return left + right;
}
Compile and link both source files, for example:
gcc -std=c17 -Wall -Wextra -Wpedantic main.c math_utils.c -o app
Command-line arguments
A hosted C program may receive command-line arguments through this form of
main:
#include <stdio.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s name\n", argv[0]);
return 1;
}
printf("Hello, %s\n", argv[1]);
return 0;
}
argc is the number of argument strings, and argv is an array of pointers
to null-terminated strings. argv[0] conventionally names the program;
arguments supplied by the user begin at argv[1]. Check argc before
accessing an argument. Parse numeric arguments using the same validation
principles as interactive input; do not assume atoi can report malformed
or out-of-range values.
Command-line programs should print a brief usage message when arguments are
missing or malformed, return a nonzero status on failure, and write
diagnostics to stderr. The precise command-line and process behavior is
defined by the hosted implementation and operating system.
Keep implementation details private when possible. A file-scope static
function is visible only in its source file. Do not put a non-static function
body in a header included by multiple source files; that can cause duplicate
definitions at link time.
A module's public contract
The header should tell callers what operations are available, what arguments
mean, what is returned, and how failure is reported. The .c file implements
those operations. Keep the two synchronized: including your own header in its
implementation lets the compiler detect mismatches between a declaration and
its definition.
An example project might have main.c, math_utils.c, and
math_utils.h. main.c includes the header and calls a function. Both .c
files are compiled and then linked together. If the function is declared but
its .c file is omitted from the build command, compilation may succeed but
linking will fail with an unresolved or undefined reference.
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int left, int right);
#endif
// math_utils.c
#include "math_utils.h"
int add(int left, int right)
{
return left + right;
}
The header is the caller-visible contract; the source file supplies one
definition for the linker. The build must compile and link math_utils.c.
Do not define the same external object or function in multiple source files.
Use extern declarations for shared declarations and exactly one definition
for an external object. Prefer passing state through function parameters over
shared global variables when practical; explicit dependencies are easier to
test and reason about.
Checkpoint: Draw which source file includes which header and identify
which compilation command must include every implementation file.