Linkage, Storage Classes & Make
Linkage, Storage Classes & Make
Storage Classes
Every variable in C has a storage class that determines its lifetime, scope, and linkage:
| Keyword | Lifetime | Scope | Linkage |
|---|---|---|---|
auto | Block (default for locals) | Block | None |
register | Block | Block | None — hint to store in register (ignored by modern compilers) |
static (local) | Program lifetime | Block | None — persists between calls |
static (file scope) | Program lifetime | File | Internal |
extern | Program lifetime | File/Global | External |
Static Local Variables
A local variable declared static is initialised only once and retains its value between function calls. This is useful for counters, caches, and one-time initialisation:
int next_id(void) {
static int counter = 0; /* initialised to 0 once at program start */
return ++counter;
}Weak vs Strong Symbols
In ELF (Linux/GCC), a regular function or global variable definition is a strong symbol — the linker errors if two strong symbols with the same name exist. A definition marked __attribute__((weak)) is a weak symbol — if a strong definition exists anywhere, the linker silently picks the strong one. Weak symbols are used in library code to provide default implementations that application code can override.
The Build System: Make
For projects with more than a few files, manually typing gcc commands becomes impractical. make is the traditional C build tool. A Makefile defines targets, prerequisites, and recipes:
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -g
program: main.o math.o utils.o
gcc $(CFLAGS) -o program main.o math.o utils.o
main.o: main.c math.h utils.h
gcc $(CFLAGS) -c main.c
clean:
rm -f *.o programmake only rebuilds targets whose prerequisites are newer — incremental builds.
Key GCC Flags
| Flag | Effect |
|---|---|
-Wall | Enable most common warnings |
-Wextra | Enable additional warnings |
-Werror | Treat warnings as errors |
-std=c11 | Use C11 standard |
-g | Include debug symbols (for gdb) |
-O0 | No optimisation (default, best for debugging) |
-O2 | Moderate optimisation (production builds) |
-O3 | Aggressive optimisation |
-c | Compile to object file only (no linking) |
-o name | Set output file name |
-I dir | Add directory to include search path |
-L dir | Add directory to library search path |
-l name | Link against library (e.g., -lm for math) |
-fsanitize=address | Enable AddressSanitizer (memory error detection) |
Static Libraries
The ar tool bundles multiple .o files into a static library (.a file). The linker extracts only the needed objects:
ar rcs libmylib.a math.o utils.o
gcc main.c -L. -lmylib -o programCode Examples
#include <stdio.h>
/* Static local: counter survives between calls, initialised once */
int next_id(void) {
static int counter = 0; /* zeroed once at program start — NOT on every call */
return ++counter;
}
/* Static local for one-time initialisation */
const char *get_greeting(void) {
static char greeting[64] = {0};
static int initialised = 0;
if (!initialised) {
/* Imagine this reads from a config file */
snprintf(greeting, sizeof(greeting), "Hello from C (built %s)", __DATE__);
initialised = 1;
printf(" [greeting initialised]\n");
}
return greeting;
}
/* Function call counter using static */
void log_call(const char *fn_name) {
static int total_calls = 0;
total_calls++;
printf(" call #%d to %s()\n", total_calls, fn_name);
}
int main(void) {
printf("ID generation:\n");
for (int i = 0; i < 5; i++) {
printf(" next_id() = %d\n", next_id());
}
printf("\nOne-time init:\n");
printf(" %s\n", get_greeting());
printf(" %s\n", get_greeting()); /* no re-init message */
printf(" %s\n", get_greeting()); /* no re-init message */
printf("\nCall logging:\n");
log_call("process");
log_call("validate");
log_call("process");
log_call("render");
return 0;
}static int counter = 0 inside a function means: allocate this variable in the data segment (not the stack), initialise it once to 0 before main starts, and keep its value between calls. The one-time initialisation pattern is useful for expensive setup you only want to do once. Note: static locals are not thread-safe without additional synchronisation.
# ===== Makefile =====
# This is NOT C code — it is a Makefile for the 'make' build tool.
# Paste this into a file named exactly: Makefile (no extension)
# Run 'make' to build, 'make clean' to remove build artefacts.
# --- Variables ---
CC = gcc # C compiler to use
CFLAGS = -Wall -Wextra -std=c11 -g # compile flags: warnings + C11 + debug symbols
LDFLAGS = # linker flags (add -lm for math library etc.)
TARGET = myprogram # final executable name
SRCS = main.c math_utils.c utils.c # list of source files
OBJS = $(SRCS:.c=.o) # auto-generate .o names from .c names
# --- Default target (first target = default) ---
all: $(TARGET)
# --- Link step: combine object files into the final executable ---
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
# $@ = target name (myprogram)
# $^ = all prerequisites (all .o files)
# --- Compile step: each .c -> .o (make's implicit rule could handle this too) ---
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# $< = first prerequisite (the .c file)
# --- Header dependencies (regenerated with: gcc -MM *.c) ---
main.o: main.c math_utils.h utils.h
math_utils.o: math_utils.c math_utils.h
utils.o: utils.c utils.h
# --- Phony targets: not real files, always run when requested ---
.PHONY: all clean rebuild
clean:
rm -f $(OBJS) $(TARGET)
rebuild: clean all
# ===== What 'make' does when you run it =====
# 1. Reads Makefile, default target is 'all'
# 2. 'all' depends on $(TARGET) = myprogram
# 3. myprogram depends on main.o math_utils.o utils.o
# 4. For each .o: if the .o doesn't exist OR its .c is newer, recompile
# 5. If any .o was rebuilt, relink to produce myprogram
# 6. 'make clean' removes all generated files
# Compile command sequence (what make runs internally):
# gcc -Wall -Wextra -std=c11 -g -c -o main.o main.c
# gcc -Wall -Wextra -std=c11 -g -c -o math_utils.o math_utils.c
# gcc -Wall -Wextra -std=c11 -g -c -o utils.o utils.c
# gcc -Wall -Wextra -std=c11 -g -o myprogram main.o math_utils.o utils.o
/* Actual C code demonstrating the build output */
#include <stdio.h>
int main(void) {
printf("Build system demo\n");
printf("Compiled with: gcc -Wall -Wextra -std=c11 -g\n");
printf("The Makefile above would build this program incrementally.\n");
return 0;
}The Makefile's automatic variables ($@, $^, $<) keep rules generic. The %.o: %.c pattern rule handles any source file. Make's incremental builds are the key benefit: when only main.c changes, only main.o is recompiled and then the link step re-runs — math_utils.o and utils.o are reused as-is.
#include <stdio.h>
#include <stdlib.h>
/* ===== Demonstrating the full GCC pipeline =====
Step 1: Preprocessing only
gcc -E main.c -o main.i
Expands #includes and #defines, outputs preprocessed C source.
main.i is pure C with no # directives — can be very large.
Step 2: Compile to assembly
gcc -S main.i -o main.s (or: gcc -S main.c -o main.s)
Outputs human-readable assembly code for the target architecture.
Step 3: Assemble to object file
gcc -c main.s -o main.o (or: gcc -c main.c -o main.o [skips -E and -S])
Produces an ELF/COFF binary object file. Not yet executable.
Step 4: Link to executable
gcc main.o -o main (links against standard C library automatically)
gcc main.o -lm -o main (also link the math library)
gcc main.o lib/mylib.a -o main (link a static library)
Common one-step compilation (skips intermediate files):
gcc -Wall -Wextra -std=c11 -O2 main.c utils.c -lm -o program
Debug build:
gcc -Wall -Wextra -std=c11 -g -O0 main.c -o program_debug
Production build:
gcc -Wall -Wextra -std=c11 -O2 -DNDEBUG main.c -o program_release
Memory error checking (AddressSanitizer):
gcc -Wall -std=c11 -g -fsanitize=address,undefined main.c -o program_asan
Static analysis:
gcc -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes main.c
*/
/* A small program to demonstrate different build outputs */
double factorial(int n) {
if (n <= 1) return 1.0;
return n * factorial(n - 1);
}
int main(void) {
printf("Factorial table:\n");
for (int i = 0; i <= 10; i++) {
printf(" %2d! = %.0f\n", i, factorial(i));
}
printf("\nCompiler flags used in different scenarios:\n");
printf(" Debug : -g -O0 -Wall -Wextra -fsanitize=address\n");
printf(" Release : -O2 -DNDEBUG -Wall\n");
printf(" Profile : -O2 -pg (enables gprof profiling)\n");
return 0;
}The four GCC pipeline stages (preprocess, compile, assemble, link) can be run separately with -E, -S, -c, and no flag respectively. Understanding each stage helps when debugging compiler errors and build issues. -DNDEBUG disables assert() macros in release builds. -fsanitize=address,undefined is invaluable during development — it instruments the binary to detect buffer overflows, use-after-free, and undefined behaviour at runtime.
Quick Quiz
1. What is the behaviour of a `static` local variable declared inside a function?
2. Which gcc flag enables most common compiler warnings?
3. In Make, what is the purpose of the `.PHONY` declaration?
4. What does the gcc flag `-c` do?
Was this lesson helpful?