What you'll learn
Real C programs divide declarations from definitions and compile source files independently. Headers describe contracts; source files own implementations; the linker combines them into one program.
By the end, you'll be able to:
- Explain preprocessing, translation units, object files, and linking
- Create guarded headers with public declarations
- Write safer macros and conditional compilation
- Control whether definitions are visible within or across translation units
Translation units
Preprocessing a source file together with everything it includes produces onetranslation unit. Each translation unit compiles independently to an object file; the linker resolves references between them.
main.cpreprocess + compile
main.o
calculator.cpreprocess + compile
calculator.o
format.cpreprocess + compile
format.o
calculatorexecutable
| Diagnostic | Typical cause |
|---|---|
| Compiler error | Invalid syntax, incompatible types, or missing declaration in one translation unit |
| Undefined reference | A declaration was used, but no matching definition reached the linker |
| Multiple definition | More than one external definition was linked |
Headers & source files
A header exposes the interface clients need: function declarations, public types, constants, and carefully chosen macros. A source file includes its own header and provides definitions.
#ifndef CALCULATOR_H
#define CALCULATOR_H
double add(double left, double right);
double subtract(double left, double right);
#endif#include "calculator.h"
double add(double left, double right) {
return left + right;
}
double subtract(double left, double right) {
return left - right;
}#include <stdio.h>
#include "calculator.h"
int main(void) {
printf("%.1f\n", add(7.0, 5.0));
printf("%.1f\n", subtract(7.0, 5.0));
return 0;
}Key idea
Include guards
Headers can be reached through several include paths. An include guard ensures the header's contents are processed only once per translation unit.
#ifndef CALCULATOR_Hasks whether the guard is absent.#define CALCULATOR_Hmarks the header as included.#endifcloses the guarded region.
Note
#pragma once is widely supported but not standardized by ISO C. Traditional guards remain maximally portable.Macros
The preprocessor replaces tokens before the compiler sees C expressions. Macros have no type system and can evaluate an argument more than once.
#include <stdio.h>
#define ARRAY_COUNT(array) (sizeof(array) / sizeof((array)[0]))
#define MAXIMUM(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int values[] = {2, 4, 6, 8};
printf("%zu\n", ARRAY_COUNT(values));
printf("%d\n", MAXIMUM(7, 12));
// Avoid MAXIMUM(i++, j++): a macro argument may be evaluated twice.
return 0;
}Parentheses are mandatory discipline
Prefer typed functions for behavior. Macros remain appropriate for conditional compilation, generic compile-time patterns, and operations such as array counts that require the operand to remain an array expression.
Conditional compilation
#if, #ifdef, and related directives include source selectively. Build flags can enable diagnostics, platform adapters, optional features, and version strings.
#include <stdio.h>
#ifndef APP_VERSION
#define APP_VERSION "development"
#endif
void log_debug(const char *message) {
#ifdef DEBUG
fprintf(stderr, "[debug] %s\n", message);
#else
(void) message;
#endif
}
int main(void) {
log_debug("starting");
printf("Version: %s\n", APP_VERSION);
return 0;
}gcc -DDEBUG -DAPP_VERSION="1.0" conditional.c -o appLinkage
Linkage determines whether declarations in different scopes or translation units refer to the same entity. File-scope static gives internal linkage and keeps helpers private.
// counter.c
static int count = 0; // internal linkage: private to this translation unit
static void increment(void) {
count++;
}
int next_count(void) { // external linkage: public definition
increment();
return count;
}| Form | Role |
|---|---|
static at file scope | Private definition in this translation unit |
extern declaration | Refer to an external definition elsewhere |
| ordinary function definition | External linkage by default |
Build a multi-file project
The -c option compiles without linking. Only changed translation units need recompilation, which becomes important as projects grow.
gcc -std=c17 -Wall -Wextra -Wpedantic -c calculator.c
gcc -std=c17 -Wall -Wextra -Wpedantic -c main.c
gcc calculator.o main.o -o calculator
./calculatorTip
Recap & quick check
Key takeaways
- A preprocessed source file forms one translation unit, compiled independently to object code.
- Headers publish declarations; source files provide definitions.
- Include guards prevent repeated header contents within one translation unit.
- Macros are token substitution, so parenthesization and side effects require care.
- File-scope static gives internal linkage and keeps implementation details private.
Quick check
1. What does the linker combine?
2. What belongs in a public header?
3. Why can MAX(i++, j++) be dangerous?
4. What does static mean on a file-scope function?
Your programs can now scale across files. Next up: Module 13 — File Handling & Persistent Data, where information survives after the process exits.