C

C Fundamentals

18 lessons

Progress0%
1. Introduction to C
1What is C?
2. Variables and Data Types
1Data Types in C
3. Control Flow
ConditionalsLoops
4. Functions
Defining FunctionsRecursion
5. Arrays and Pointers
Arrays and StringsPointers
6. Memory Management
Dynamic MemoryStructs and Files
7. Preprocessor & Macros
Preprocessor DirectivesMacros & Inline Functions
8. Bitwise Operations
Bitwise OperatorsBit Flags & Masking
9. Enums, Unions & typedef
Enums & typedefUnions & Complex Types
10. Multi-file Programs
Header Files & Compilation UnitsLinkage, Storage Classes & Make
All Tutorials
CMulti-file Programs
Lesson 18 of 18 min
Chapter 10 · Lesson 2

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:

KeywordLifetimeScopeLinkage
autoBlock (default for locals)BlockNone
registerBlockBlockNone — hint to store in register (ignored by modern compilers)
static (local)Program lifetimeBlockNone — persists between calls
static (file scope)Program lifetimeFileInternal
externProgram lifetimeFile/GlobalExternal

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:

c
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:

makefile
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 program

make only rebuilds targets whose prerequisites are newer — incremental builds.

Key GCC Flags

FlagEffect
-WallEnable most common warnings
-WextraEnable additional warnings
-WerrorTreat warnings as errors
-std=c11Use C11 standard
-gInclude debug symbols (for gdb)
-O0No optimisation (default, best for debugging)
-O2Moderate optimisation (production builds)
-O3Aggressive optimisation
-cCompile to object file only (no linking)
-o nameSet output file name
-I dirAdd directory to include search path
-L dirAdd directory to library search path
-l nameLink against library (e.g., -lm for math)
-fsanitize=addressEnable 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:

bash
ar rcs libmylib.a math.o utils.o
gcc main.c -L. -lmylib -o program

Code Examples

Static Local Variable Counterc
#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.

Simple Makefile Annotatedc
# ===== 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.

GCC Compilation Sequencec
#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?

PreviousFinish