Table of Contents

0. Setup & Environment

macOS: Xcode Command Line Tools

The fastest way to get a C compiler on macOS is Apple's Xcode Command Line Tools, which ships Apple Clang as cc.

# Install
xcode-select --install

# Verify
cc --version
# Apple clang version 15.x.x (clang-1500.x.x.x)
Note: On macOS, gcc and cc are both aliased to Apple Clang. If you need true GCC, install it via Homebrew (see below) and invoke it as gcc-14 (or whichever version Homebrew installs).

Alternative: GCC via Homebrew

Use GCC when you need specific C standard compliance, GCC-specific extensions (__attribute__, nested functions), or want to cross-check behavior against Clang.

brew install gcc

# Homebrew installs as gcc-14 (version may differ)
gcc-14 --version
# gcc-14 (Homebrew GCC 14.x.x) 14.x.x

Build Tools

make ships with Xcode CLI tools. For larger projects, install CMake via Homebrew.

brew install cmake

# Compile a single file (recommended flags for learning)
cc -Wall -Wextra -std=c17 -o hello hello.c
Tip: Always compile with -Wall -Wextra during development. These flags surface bugs that would otherwise compile silently — uninitialized variables, unused parameters, implicit type conversions, and more.

Debugger

lldb ships with Xcode CLI tools and is the macOS default debugger. Compile with -g to include debug symbols.

# Compile with debug symbols
cc -g -Wall -std=c17 -o hello hello.c

# Launch debugger
lldb ./hello

# Common lldb commands
(lldb) breakpoint set --name main   # b main
(lldb) run                          # r
(lldb) next                         # n  (step over)
(lldb) step                         # s  (step into)
(lldb) print argc                   # p argc
(lldb) quit                         # q

Editor Setup

Quick End-to-End Verify

mkdir ~/c-refresher && cd ~/c-refresher
cat > hello.c << 'EOF'
#include <stdio.h>
int main(void) {
    printf("Hello, C!\n");
    return 0;
}
EOF
cc -Wall -std=c17 -o hello hello.c && ./hello
# Hello, C!
Tip: Use int main(void) rather than int main() in C. In C, an empty parameter list means "unspecified parameters", not "no parameters" — void is explicit and correct.

1. Program Basics

Compilation Model

C compilation is a four-stage pipeline. Understanding each stage is critical for debugging linker errors and managing build systems.

StageToolInputOutputFlag to stop here
Preprocessorcpp.cExpanded .i-E
Compilercc1.iAssembly .s-S
Assembleras.sObject .o-c
Linkerld.o + libsExecutable(none)
# Compile with all warnings, debug info, C17 standard
gcc -Wall -Wextra -Wpedantic -std=c17 -g -o program main.c

# Optimized release build
gcc -O2 -DNDEBUG -std=c17 -o program main.c

# Stop at preprocessor output (inspect macro expansion)
gcc -E main.c -o main.i

# Stop at assembly (inspect what the compiler generated)
gcc -S -O2 main.c -o main.s

# Compile to object file only (no linking)
gcc -c main.c -o main.o

# Compile multiple files
gcc -Wall -std=c17 -o program main.c utils.c io.c

# Static analysis with clang
clang --analyze main.c

Hello World & main() Signatures

#include <stdio.h>
#include <stdlib.h>

/* Two valid signatures for main */
int main(void);                          /* no arguments */
int main(int argc, char *argv[]);        /* command-line args */
int main(int argc, char *argv[], char *envp[]);  /* + environment (POSIX) */

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <name>\n", argv[0]);
        return EXIT_FAILURE;  /* 1 — from stdlib.h */
    }
    printf("Hello, %s!\n", argv[1]);
    return EXIT_SUCCESS;      /* 0 */
}

/* argv layout:
 *   argv[0]      = program name (or "" if unavailable)
 *   argv[1..N-1] = arguments
 *   argv[argc]   = NULL sentinel
 */

Headers, Source Files & Translation Units

/* math_utils.h — declarations (what callers need to know) */
#ifndef MATH_UTILS_H   /* header guard */
#define MATH_UTILS_H

#include <stddef.h>    /* size_t */

/* Function declaration (prototype) */
double vector_dot(const double *a, const double *b, size_t n);

/* Inline function defined in header (C99) */
static inline int max(int a, int b) { return a > b ? a : b; }

/* Macro constant */
#define PI 3.14159265358979323846

#endif /* MATH_UTILS_H */
/* math_utils.c — definitions (implementation) */
#include "math_utils.h"

double vector_dot(const double *a, const double *b, size_t n) {
    double sum = 0.0;
    for (size_t i = 0; i < n; i++) {
        sum += a[i] * b[i];
    }
    return sum;
}
One Definition Rule
A function or variable may be declared many times but defined exactly once across all translation units. Putting definitions (not just declarations) in headers causes multiple-definition linker errors unless they are inline or static.

Key Compiler Flags

FlagEffect
-WallEnable common warnings
-WextraAdditional warnings beyond -Wall
-WpedanticStrict ISO C conformance warnings
-WerrorTreat warnings as errors
-std=c99/c11/c17/c23Language standard
-gDebug symbols (DWARF)
-O0/O1/O2/O3/OsOptimization level (0=none, s=size)
-DNAME=VALUEDefine preprocessor macro
-I/pathAdd include search path
-L/path -lnameAdd library search path / link library
-fsanitize=addressAddressSanitizer (memory errors)
-fsanitize=undefinedUBSanitizer (undefined behavior)
-fstack-protectorStack canaries against overflows

2. Type System

Fundamental Types

C's fundamental types have implementation-defined sizes. The standard only guarantees minimum ranges, not exact widths.

TypeMin sizeTypical (LP64)Format specifier
char8 bits8 bits%c, %hhd
short16 bits16 bits%hd
int16 bits32 bits%d, %i
long32 bits64 bits (Linux), 32 bits (Windows)%ld
long long64 bits64 bits%lld
float32-bit IEEE 754%f, %g
double64-bit IEEE 754%lf, %g
long double80-bit extended (x86)%Lf

Fixed-Width Types (stdint.h)

Use these whenever you need exact widths — network protocols, binary file formats, hardware registers.

#include <stdint.h>
#include <stddef.h>

/* Exact-width signed */
int8_t   s8  = -128;
int16_t  s16 = 32767;
int32_t  s32 = 2147483647;
int64_t  s64 = INT64_MAX;

/* Exact-width unsigned */
uint8_t  u8  = 255;
uint16_t u16 = 65535;
uint32_t u32 = UINT32_MAX;
uint64_t u64 = UINT64_MAX;

/* Fast/least variants (prefer for general use) */
int_fast32_t  fast32;   /* fastest type >= 32 bits */
int_least16_t least16;  /* smallest type >= 16 bits */

/* Pointer-sized integers */
size_t    sz  = sizeof(int);    /* result of sizeof, array index — UNSIGNED */
ptrdiff_t pd  = ptr2 - ptr1;   /* pointer difference — SIGNED */
intptr_t  ip  = (intptr_t)ptr; /* integer that fits a pointer */
uintptr_t up  = (uintptr_t)ptr;

/* Format macros (inttypes.h) */
#include <inttypes.h>
printf("%" PRId32 "\n", s32);   /* avoids %d vs %ld confusion */
printf("%" PRIu64 "\n", u64);
printf("%" PRIxPTR "\n", up);   /* pointer as hex */
char Signedness is Implementation-Defined
char may be signed or unsigned depending on the platform/compiler. Never compare char values to EOF (which is -1) without casting to unsigned char first, or use int for the return value of getc().

Type Qualifiers

/* const — value will not be modified through this variable */
const int MAX = 100;
const char *msg = "hello";     /* pointer to const char */
char * const ptr = buf;        /* const pointer to char */
const char * const cp = "hi";  /* const pointer to const char */

/* volatile — value may change outside program's control
 * (memory-mapped I/O, signal handlers, setjmp targets) */
volatile int flag;             /* don't cache in register */
volatile uint32_t *reg = (volatile uint32_t *)0xDEAD0000;

/* restrict (C99) — pointer is the ONLY way to access this data
 * within its scope; enables more aggressive optimization */
void memcpy_fast(void * restrict dst, const void * restrict src, size_t n);

/* _Atomic (C11) — atomic read/modify/write */
#include <stdatomic.h>
_Atomic int ref_count = 0;
atomic_fetch_add(&ref_count, 1);

Type Conversions

/* Implicit integer promotions: char/short promoted to int in expressions */
char a = 100, b = 200;
int  c = a + b;   /* both promoted to int before addition */

/* Usual arithmetic conversions (both operands converted to common type):
 * long double > double > float > unsigned long long > long long > ... */
int    i = 5;
double d = 2.0;
double r = i + d;   /* i promoted to double */

/* Explicit casts — narrow conversions lose data */
double pi    = 3.14159;
int    trunc = (int)pi;          /* 3 — truncates toward zero */
uint8_t byte = (uint8_t)0x1FF;  /* 0xFF — truncates to 8 bits */

/* Signed/unsigned mixing is dangerous */
int          neg    = -1;
unsigned int pos    = 1;
int          result = neg < pos;  /* 0 (false)! -1 converted to UINT_MAX */

/* Safe cast pattern: check range before narrowing */
long val = some_function();
assert(val >= INT_MIN && val <= INT_MAX);
int safe = (int)val;
Signed/Unsigned Comparison is a Trap
Comparing int to unsigned int causes the int to be silently converted to unsigned. -1 < 1u is false. Enable -Wsign-compare (included in -Wextra) to catch this at compile time.

_Bool and stdbool.h

#include <stdbool.h>   /* C99 */

bool flag = true;
bool other = false;

/* _Bool is the underlying type; any nonzero value becomes 1 */
_Bool b1 = 42;    /* stores 1 */
_Bool b2 = 0;     /* stores 0 */

/* stdbool.h defines: bool, true (1), false (0) */
bool is_valid(int x) {
    return x > 0 && x < 100;
}

3. Variables & Storage Classes

Storage Class Specifiers

/* auto — default for local variables; stack-allocated, undefined on entry */
{
    auto int x;     /* "auto" is implicit, almost never written explicitly */
    int y;          /* same as auto int y */
}

/* static — persists for program lifetime */
void counter(void) {
    static int count = 0;  /* initialized once at program start */
    count++;
    printf("called %d times\n", count);
}

/* File-scope static: internal linkage (not visible outside this .c file) */
static int module_state = 0;   /* cannot be accessed from other .c files */
static void helper(void) { }   /* private function */

/* extern — declaration that the definition lives elsewhere */
extern int global_counter;     /* defined in another .c file */
extern void other_func(void);

/* register — hint to compiler to keep in CPU register (advisory, rarely useful) */
register int i;    /* compiler may ignore; cannot take address of register var */

/* _Thread_local (C11) — one instance per thread */
#include <threads.h>
_Thread_local int errno_copy;   /* each thread gets its own copy */

Scope Rules

int x = 1;                      /* file scope: visible from here to end of file */

void func(int x) {              /* parameter scope: x shadows file-scope x */
    printf("%d\n", x);          /* prints parameter */
    {
        int x = 3;              /* block scope: inner x shadows parameter x */
        printf("%d\n", x);      /* prints 3 */
    }
    printf("%d\n", x);          /* prints parameter again */
}

/* Prototype scope: parameter names in declarations have no effect */
void foo(int n, int arr[n]);    /* n is in prototype scope only */

Initialization Rules

/* Static storage duration: zero-initialized automatically */
static int a;          /* 0 */
static int *p;         /* NULL */
static double d;       /* 0.0 */

/* Automatic storage duration: INDETERMINATE (undefined behavior to read!) */
int b;                  /* garbage — reading is UB */
int *q;                 /* garbage pointer */

/* Partial aggregate initialization: rest is zero-initialized */
int arr[10] = {1, 2};  /* arr = {1, 2, 0, 0, 0, 0, 0, 0, 0, 0} */
int all_zero[100] = {0}; /* zero-initialize all 100 */

/* Designated initializers (C99) */
int days[7] = { [0]=0, [6]=6 };   /* others are 0 */

struct Point { int x, y, z; };
struct Point p = { .y = 5 };      /* x=0, y=5, z=0 */

/* Compound literals (C99) — anonymous object with given type */
struct Point *pp = &(struct Point){ .x=1, .y=2 };
int *tmp = (int[]){ 10, 20, 30 };  /* lifetime: enclosing block */

4. Operators & Expressions

Operator Precedence (high to low)

Prec.OperatorsAssociativity
1 (highest)() [] . -> ++ -- (postfix)Left
2++ -- + - ! ~ (type) * & sizeof _Alignof (prefix/unary)Right
3* / %Left
4+ -Left
5<< >>Left
6< <= > >=Left
7== !=Left
8& (bitwise AND)Left
9^ (bitwise XOR)Left
10| (bitwise OR)Left
11&&Left
12||Left
13?:Right
14= += -= *= /= %= &= ^= |= <<= >>=Right
15 (lowest),Left

Bitwise Operations

uint32_t flags = 0;

/* Set bit N */
flags |= (1u << N);

/* Clear bit N */
flags &= ~(1u << N);

/* Toggle bit N */
flags ^= (1u << N);

/* Test bit N */
if (flags & (1u << N)) { /* bit is set */ }

/* Extract bits [high:low] (inclusive) */
#define BITS(x, high, low)  (((x) >> (low)) & ((1u << ((high)-(low)+1)) - 1))
uint32_t field = BITS(reg, 7, 4);   /* bits 7..4 */

/* Common masks */
#define MASK_LOWER_BYTE  0x000000FFu
#define MASK_UPPER_WORD  0xFFFF0000u

/* Bit tricks */
int is_power_of_two = (n > 0) && ((n & (n-1)) == 0);
int lowest_set_bit  = n & (-n);           /* isolate LSB */
int clear_lowest    = n & (n-1);          /* clear LSB */
int popcount        = __builtin_popcount(n); /* GCC/Clang built-in */

/* Signed right shift is arithmetic on most platforms (fills with sign bit)
 * but is technically implementation-defined — avoid for portable code */
int arithmetic_shift = x >> 1;  /* x / 2 for non-negative */
uint32_t logical_shift = (uint32_t)x >> 1;  /* portable unsigned shift */

Sequence Points & Undefined Behavior

/* Undefined: modifying a variable twice between sequence points */
i = i++;          /* UB */
a[i] = i++;       /* UB */
func(i++, i++);   /* UB: order of argument evaluation unspecified */

/* Defined: comma operator is a sequence point */
int j = (i++, i + 1);   /* i incremented, then i+1 evaluated */

/* Defined: && and || short-circuit and are sequence points */
if (ptr != NULL && ptr->val > 0) { }   /* safe */

/* Signed integer overflow is UNDEFINED BEHAVIOR */
int x = INT_MAX;
int y = x + 1;    /* UB — compiler may assume this never happens */

/* Unsigned integer overflow is defined: wraps modulo 2^N */
uint32_t u = UINT32_MAX;
uint32_t v = u + 1;   /* 0 — well-defined */

/* Overflow-safe signed arithmetic */
#include <limits.h>
bool safe_add(int a, int b, int *result) {
    if (b > 0 && a > INT_MAX - b) return false;
    if (b < 0 && a < INT_MIN - b) return false;
    *result = a + b;
    return true;
}
Signed Overflow is UB — Compilers Exploit This
With -O2, GCC will eliminate if (x + 1 < x) overflow checks because signed overflow "cannot happen." Use -fwrapv to make signed overflow wrap (non-standard), or use unsigned arithmetic for overflow-sensitive code.

5. Control Flow

switch Statement

/* Fall-through is the default — always use break or comment intentional fall */
switch (state) {
    case STATE_IDLE:
        init();
        break;
    case STATE_RUN:
        /* FALLTHROUGH */
    case STATE_RESUME:   /* both run and resume share this code */
        execute();
        break;
    default:
        handle_error();
        break;
}

/* Duff's Device — classic loop unrolling via fall-through (historical curiosity) */
void duff_copy(char *to, const char *from, int count) {
    int n = (count + 7) / 8;
    switch (count % 8) {
        case 0: do { *to++ = *from++;
        case 7:      *to++ = *from++;
        case 6:      *to++ = *from++;
        case 5:      *to++ = *from++;
        case 4:      *to++ = *from++;
        case 3:      *to++ = *from++;
        case 2:      *to++ = *from++;
        case 1:      *to++ = *from++;
                } while (--n > 0);
    }
}

goto — Valid Uses

/* Pattern 1: error cleanup (common in Linux kernel) */
int open_files(void) {
    FILE *f1 = fopen("a.txt", "r");
    if (!f1) return -1;

    FILE *f2 = fopen("b.txt", "r");
    if (!f2) goto cleanup_f1;

    FILE *f3 = fopen("c.txt", "r");
    if (!f3) goto cleanup_f2;

    /* do work */
    fclose(f3);
cleanup_f2:
    fclose(f2);
cleanup_f1:
    fclose(f1);
    return 0;
}

/* Pattern 2: break out of nested loops (goto is cleaner than flag variables) */
bool found = false;
for (int i = 0; i < ROWS; i++) {
    for (int j = 0; j < COLS; j++) {
        if (matrix[i][j] == target) {
            row = i; col = j;
            goto done;
        }
    }
}
done:
    ;   /* empty statement required after label */

_Static_assert (C11)

#include <assert.h>

/* Compile-time assertion — fails to compile if condition is false */
_Static_assert(sizeof(int) == 4, "int must be 32 bits on this platform");
_Static_assert(sizeof(void *) == 8, "64-bit pointers required");

/* C23 allows omitting the message */
static_assert(sizeof(long) >= 8);   /* C23 */

/* Runtime assertion — aborts if condition false in debug builds */
assert(ptr != NULL);           /* disabled when NDEBUG is defined */
assert(index < array_size);

/* Custom assert with context */
#define ASSERT(cond, msg) do {                          \
    if (!(cond)) {                                      \
        fprintf(stderr, "ASSERT failed: %s\n"          \
                "  At: %s:%d in %s()\n",               \
                (msg), __FILE__, __LINE__, __func__);   \
        abort();                                        \
    }                                                   \
} while (0)

6. Functions

Declarations, Definitions & Prototypes

/* Declaration (prototype) — tells compiler the signature */
int add(int a, int b);             /* parameter names optional in declaration */
int add(int, int);                 /* same declaration */

/* Definition — provides the body */
int add(int a, int b) {
    return a + b;
}

/* K&R style (ancient, avoid): */
/* int add(a, b) int a; int b; { return a + b; } */

/* Functions without prototypes: dangerous in C (assumed to return int,
 * accept any args). Always declare before use. */

Pass by Value & Pointer Parameters

/* C always passes by value. To modify caller's variable, pass a pointer */
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

/* Usage: pass address of variables */
int x = 1, y = 2;
swap(&x, &y);   /* x=2, y=1 */

/* Array parameters decay to pointer — size info is lost */
void process(int arr[], size_t len);   /* arr is int* */
void process(int *arr, size_t len);    /* identical */

/* Pass array by pointer to preserve dimensions */
void fill(int (*matrix)[4], int rows); /* pointer to array of 4 ints */

Variadic Functions

#include <stdarg.h>

/* At least one fixed parameter required */
int sum(int count, ...) {
    va_list ap;
    va_start(ap, count);   /* initialize; count = last named param */

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(ap, int);   /* extract next argument as type int */
    }

    va_end(ap);            /* cleanup (required before return) */
    return total;
}

/* Usage */
int s = sum(3, 10, 20, 30);   /* 60 */

/* Forwarding va_list to another variadic function */
void log_msg(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    vprintf(fmt, ap);    /* vprintf accepts va_list directly */
    va_end(ap);
}

/* Safe printf-like with format checking (GCC attribute) */
__attribute__((format(printf, 1, 2)))
void my_log(const char *fmt, ...);

Function Pointers

/* Declaration: return_type (*name)(param_types) */
int (*fp)(int, int);               /* pointer to function taking 2 ints, returning int */

/* Typedef makes it readable */
typedef int (*compare_fn)(const void *, const void *);

/* Assignment and call */
fp = add;               /* no & required — function decays to pointer */
int r = fp(3, 4);       /* call via pointer */
int r2 = (*fp)(3, 4);   /* equivalent, more explicit */

/* Callback pattern */
void array_sort(int *arr, size_t n, int (*cmp)(int, int)) {
    /* ... uses cmp(a, b) to compare elements */
}

/* Dispatch table (virtual-function-like) */
typedef void (*handler_fn)(int event);

static void on_connect(int e) { printf("connect: %d\n", e); }
static void on_data(int e)    { printf("data: %d\n", e); }
static void on_close(int e)   { printf("close: %d\n", e); }

static const handler_fn handlers[] = {
    [EVENT_CONNECT] = on_connect,
    [EVENT_DATA]    = on_data,
    [EVENT_CLOSE]   = on_close,
};

/* Call: handlers[event](event_data); */

/* Array of function pointers */
typedef double (*math_fn)(double);
math_fn ops[] = { sin, cos, tan, sqrt };

inline Functions (C99)

/* Hint to compiler to substitute the call site with the body.
 * Unlike macros: type-safe, respects scope, debuggable. */

/* In a header file: must be static or have an external definition elsewhere */
static inline int clamp(int val, int lo, int hi) {
    return val < lo ? lo : (val > hi ? hi : val);
}

/* In a .c file: inline + external definition in exactly one TU */
/* math.h */   inline int square(int x);          /* inline declaration */
/* math.c */   extern inline int square(int x) { return x * x; } /* external def */

7. Pointers

Pointer Arithmetic

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;           /* p points to arr[0] */

/* Arithmetic scales by sizeof(pointed-to type) */
p++;                    /* p now points to arr[1] (advances by sizeof(int) bytes) */
p += 2;                 /* p now points to arr[3] */
*(p - 1);               /* arr[2] = 30 */

/* Pointer difference: result is ptrdiff_t */
int *end = arr + 5;     /* one past last element */
ptrdiff_t len = end - arr;   /* 5 */

/* Array indexing is defined in terms of pointer arithmetic */
arr[i] == *(arr + i)    /* exactly equivalent */
i[arr] == *(i + arr)    /* legal but never do this */

/* Valid pointer arithmetic:
 *   - within an array (including one-past-the-end)
 *   - same object
 * Invalid (UB): arithmetic on pointers into different objects */

Pointer to Pointer

/* char **argv: array of strings (pointer to pointer to char) */
void print_args(int argc, char **argv) {
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
}

/* Modifying a pointer through double indirection */
void allocate(int **pp, size_t n) {
    *pp = malloc(n * sizeof(int));  /* sets caller's pointer */
}

int *buf = NULL;
allocate(&buf, 100);  /* buf now points to allocated memory */

/* 2D array of strings */
char *names[] = {"Alice", "Bob", "Carol", NULL};  /* NULL-terminated */
for (char **name = names; *name != NULL; name++) {
    printf("%s\n", *name);
}

void Pointers (Generic Programming)

/* void* can hold any pointer, but cannot be dereferenced directly */
void *generic_ptr;

int x = 42;
generic_ptr = &x;         /* no cast needed in C (unlike C++) */

int *ip = generic_ptr;    /* no cast needed in C */
printf("%d\n", *ip);      /* 42 */

/* malloc returns void* */
int *arr = malloc(10 * sizeof(int));

/* Generic swap using memcpy */
void generic_swap(void *a, void *b, size_t size) {
    /* Use a VLA or alloca for small sizes; heap for large */
    unsigned char tmp[size];   /* C99 VLA */
    memcpy(tmp, a, size);
    memcpy(a, b, size);
    memcpy(b, tmp, size);
}

/* void* in callbacks — carry user context */
typedef void (*callback_t)(void *ctx, int event);
void register_handler(callback_t cb, void *user_data);

const Correctness with Pointers

/* Read right-to-left: "pointer to const int" */
const int *p1;         /* can change p1 itself, cannot modify *p1 */
int const *p2;         /* same as above */

/* "const pointer to int" */
int * const p3 = &x;  /* cannot change p3, can modify *p3 */

/* "const pointer to const int" */
const int * const p4 = &x;  /* neither p4 nor *p4 can be changed */

/* Functions should take const pointers when not modifying data */
size_t strlen(const char *s);     /* won't modify the string */
void *memcpy(void * restrict dst, const void * restrict src, size_t n);

/* const propagation in practice */
void process(const int *data, size_t n) {
    /* data[i] = 0;  -- compile error: assignment to const */
    for (size_t i = 0; i < n; i++) {
        printf("%d\n", data[i]);   /* read-only: fine */
    }
}

/* Discarding const is UB if the object was actually const */
const int CI = 5;
int *bad = (int *)&CI;
*bad = 10;   /* UB — behavior undefined even if it "works" */

NULL, Dangling & Wild Pointers

#include <stddef.h>

/* NULL: a null pointer constant, guaranteed to compare != any valid pointer */
int *p = NULL;
if (p == NULL) { /* never dereference NULL */ }

/* C23 introduces nullptr keyword (like C++) */
/* int *p = nullptr;  -- C23 only */

/* Dangling pointer: points to freed or out-of-scope memory */
int *dangling;
{
    int local = 5;
    dangling = &local;
}   /* local is gone, dangling is now invalid */
/* *dangling = 1;  -- UB */

/* Wild (uninitialized) pointer */
int *wild;   /* contains garbage address */
/* *wild = 1;  -- UB (almost certainly segfault or silent corruption) */

/* Best practice: zero out after free */
free(ptr);
ptr = NULL;   /* prevents double-free and use-after-free accidents */

/* Macro to free and nullify */
#define SAFE_FREE(p)  do { free(p); (p) = NULL; } while (0)

/* Double free is UB */
free(ptr);
free(ptr);   /* UB — heap corruption, potential security exploit */

8. Arrays & Strings

Arrays

/* Stack arrays: size must be a constant expression in C89 */
int fixed[10];
int fixed2[10] = {1, 2, 3};   /* rest zero-initialized */

/* VLAs (C99) — size known only at runtime; stack-allocated */
void process(int n) {
    int vla[n];     /* n bytes on stack — danger: stack overflow for large n */
    /* No initializer syntax for VLAs */
    for (int i = 0; i < n; i++) vla[i] = i;
}

/* Array decay: array name in most expressions decays to pointer to first element
 * Exceptions: sizeof, _Alignof, &(address-of), string literal initializer */
int arr[5] = {1,2,3,4,5};
int *p = arr;             /* decays to &arr[0] */
sizeof(arr);              /* 20 bytes — no decay */
sizeof(p);                /* 8 bytes (pointer size) — already decayed */

/* Multidimensional arrays: row-major storage */
int matrix[3][4];
matrix[1][2] = 42;
/* &matrix[1][2] == (int*)matrix + 1*4 + 2 == (int*)matrix + 6 */

/* Array of pointers vs 2D array */
int rows[3][4];          /* contiguous 3*4 block */
int *ptrs[3];            /* 3 pointers to independently allocated rows */
ptrs[0] = malloc(4 * sizeof(int));   /* rows can be different sizes (jagged) */

/* Pass 2D array to function */
void fill_matrix(int m[][4], int rows);  /* column count must be known */
void fill_matrix2(int (*m)[4], int rows); /* equivalent */
VLA Caution
VLAs are optional in C11/C17 (implementations may not support them). They can cause stack overflows for large inputs and have no way to detect allocation failure. Prefer heap allocation (malloc) for anything beyond a few hundred bytes.

C Strings

#include <string.h>

/* C strings: null-terminated char arrays */
char s1[] = "hello";           /* {'h','e','l','l','o','\0'} — 6 bytes, modifiable */
const char *s2 = "hello";      /* string literal — do NOT modify */
char s3[20] = "hello";         /* 20-byte buffer, "hello\0" + padding */

/* string.h functions */
size_t len = strlen(s1);                  /* 5 — does not count '\0' */
int cmp = strcmp(s1, s2);                 /* 0 if equal, <0, or >0 */
int cmp2 = strncmp(s1, s2, 3);           /* compare at most 3 chars */
char *pos = strchr(s1, 'l');              /* pointer to first 'l' */
char *last = strrchr(s1, 'l');            /* pointer to last 'l' */
char *found = strstr(s1, "ll");           /* substring search */

/* UNSAFE: strcpy, strcat don't check bounds */
/* strcpy(dst, src);   -- buffer overflow if src >= dst size */

/* SAFE alternatives */
char dst[20];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';             /* strncpy does NOT guarantee NUL */

/* Better: snprintf always NUL-terminates */
snprintf(dst, sizeof(dst), "%s", src);

/* Even better: strlcpy (BSD, not C standard but common) */
strlcpy(dst, src, sizeof(dst));           /* always NUL-terminates, returns needed size */

/* String to number */
#include <stdlib.h>
int n     = atoi("42");                   /* unsafe: no error detection */
long l    = strtol("42", NULL, 10);       /* base 10 */
double d  = strtod("3.14", NULL);

/* Error-checking strtol */
char *endptr;
errno = 0;
long val = strtol(str, &endptr, 10);
if (errno != 0 || endptr == str || *endptr != '\0') {
    /* conversion failed */
}

9. Structs, Unions & Enums

Structs

/* Struct declaration */
struct Point {
    double x;
    double y;
};

/* Typedef for convenience */
typedef struct {
    double x;
    double y;
} Point;

/* Initialization */
Point p1 = {1.0, 2.0};                    /* positional */
Point p2 = { .x = 1.0, .y = 2.0 };       /* designated (C99) — preferred */
Point p3 = { .y = 5.0 };                  /* x defaults to 0.0 */

/* Member access */
Point p;
p.x = 3.0;
Point *pp = &p;
pp->y = 4.0;    /* equivalent to (*pp).y = 4.0 */

/* Struct padding: compiler adds padding for alignment */
struct Padded {
    char  c;    /* 1 byte */
                /* 3 bytes padding */
    int   i;    /* 4 bytes — must be 4-byte aligned */
    char  d;    /* 1 byte */
                /* 7 bytes padding (for 8-byte struct alignment in array) */
    double f;   /* 8 bytes */
};
/* sizeof(struct Padded) == 24, not 14 */

/* Pack to eliminate padding (non-standard but widely supported) */
#pragma pack(1)
struct Packed { char c; int i; };
#pragma pack()   /* restore default */
/* GCC/Clang: */ struct Packed2 { char c; int i; } __attribute__((packed));

Bit Fields

/* Useful for hardware register maps and protocol headers */
struct IPv4Flags {
    unsigned int reserved : 1;
    unsigned int dont_fragment : 1;
    unsigned int more_fragments : 1;
    unsigned int fragment_offset : 13;
};

struct Status {
    uint32_t ready   : 1;
    uint32_t error   : 1;
    uint32_t mode    : 3;   /* 3-bit field: values 0-7 */
    uint32_t padding : 27;  /* pad to 32 bits */
};

/* Layout is implementation-defined: bit order, padding, int type */
/* Do not use bit fields for portable binary protocols */

Unions & Type Punning

/* Union: all members share the same storage */
union Value {
    int    i;
    float  f;
    double d;
    char   bytes[8];
};

union Value v;
v.i = 42;
printf("%d\n", v.i);    /* 42 */
v.f = 3.14f;
printf("%f\n", v.f);    /* 3.14 — i is now garbage */

/* Type punning via union (C99 permits this; pointer aliasing does not) */
union Float32 {
    float    f;
    uint32_t bits;
};

union Float32 fp;
fp.f = -0.5f;
printf("bits: %08X\n", fp.bits);   /* inspect IEEE 754 bits */

/* Tagged union (discriminated union) — idiomatic C */
typedef enum { TAG_INT, TAG_FLOAT, TAG_STR } ValueTag;

typedef struct {
    ValueTag tag;
    union {
        int         i;
        double      f;
        const char *s;
    };   /* anonymous union (C11) */
} Variant;

Variant v2 = { .tag = TAG_INT, .i = 99 };
if (v2.tag == TAG_INT) printf("%d\n", v2.i);

Flexible Array Members (C99)

/* Struct with variable-length tail — must be last member */
typedef struct {
    size_t  len;
    uint8_t data[];   /* flexible array member — zero size, no padding */
} Buffer;

/* Allocate with space for actual data */
Buffer *buf = malloc(sizeof(Buffer) + 256);
buf->len = 256;
memset(buf->data, 0, 256);

/* Size of struct does not include the FAM */
sizeof(Buffer);   /* == sizeof(size_t) -- just the len field */

Enums

/* Enum values are int by default */
typedef enum {
    COLOR_RED   = 0,
    COLOR_GREEN = 1,
    COLOR_BLUE  = 2,
} Color;

/* Values auto-increment from previous */
typedef enum {
    MON = 1,
    TUE,    /* 2 */
    WED,    /* 3 */
    THU,    /* 4 */
    FRI,    /* 5 */
    SAT,    /* 6 */
    SUN,    /* 7 */
} Weekday;

/* Enum underlying type is implementation-defined (usually int).
 * C does NOT enforce that only valid values are stored — it's just an int.
 * Use -Wswitch to get warned when switch cases don't cover all enum values. */

Color c = 99;          /* legal, no error */
switch (c) {
    case COLOR_RED:   break;
    case COLOR_GREEN: break;
    /* -Wswitch warns: COLOR_BLUE not handled */
}

10. Dynamic Memory

malloc / calloc / realloc / free

#include <stdlib.h>

/* malloc: allocate N bytes, uninitialized content */
int *arr = malloc(100 * sizeof(int));
if (arr == NULL) {
    perror("malloc");
    return -1;
}

/* calloc: allocate N elements of size M, zero-initialized */
int *zero_arr = calloc(100, sizeof(int));

/* realloc: resize an existing allocation */
arr = realloc(arr, 200 * sizeof(int));
if (arr == NULL) {
    /* original ptr is NOT freed on realloc failure — avoid losing it */
    free(original);  /* free original before returning */
    return -1;
}

/* Always use a temp for realloc to avoid losing the original on failure */
void *tmp = realloc(arr, new_size);
if (tmp == NULL) {
    /* arr still valid and usable */
    return -ENOMEM;
}
arr = tmp;

/* free: return memory to heap */
free(arr);
arr = NULL;   /* prevent dangling pointer bugs */

/* aligned_alloc (C11): alignment must be power of 2, size multiple of alignment */
void *aligned = aligned_alloc(64, 1024);   /* 1024 bytes, 64-byte aligned */
free(aligned);

/* alloca: stack allocation (not standard, but widely available) */
/* void *p = alloca(n);  -- no free needed, freed on function return */
realloc Pitfall
p = realloc(p, new_size) is a bug: if realloc returns NULL, the original pointer is lost and you have a memory leak. Always use a temporary: void *tmp = realloc(p, new_size); if (tmp) p = tmp;

Common Memory Bugs

/* 1. Buffer overrun: writing past allocated end */
char *buf = malloc(10);
strcpy(buf, "this string is too long");   /* overflows — heap corruption */

/* 2. Use after free */
free(ptr);
printf("%d\n", *ptr);   /* UB — may crash, corrupt, or silently misbehave */

/* 3. Double free */
free(ptr);
free(ptr);   /* UB — heap corruption */

/* 4. Memory leak: forgetting to free */
void leaky(void) {
    int *arr = malloc(1000 * sizeof(int));
    if (arr[0] == 0) return;   /* leak: didn't free arr */
    free(arr);
}

/* 5. Wrong size to malloc */
int **matrix = malloc(rows * sizeof(int));     /* WRONG: should be sizeof(int*) */
int **matrix2 = malloc(rows * sizeof(*matrix2)); /* CORRECT: sizeof deref type */

/* 6. Freeing stack memory */
int local = 5;
free(&local);   /* UB — you don't own this memory */

/* 7. Freeing interior of allocation */
int *arr2 = malloc(10 * sizeof(int));
arr2 += 5;
free(arr2);   /* UB — not the original pointer from malloc */

Ownership Patterns

/* Convention: the allocator is the owner; document transfer explicitly */

/* Callee allocates, caller must free (transfer of ownership) */
char *str_dup(const char *src) {
    size_t n = strlen(src) + 1;
    char *copy = malloc(n);
    if (copy) memcpy(copy, src, n);
    return copy;   /* caller owns this */
}

/* Caller allocates, passes buffer to callee (no ownership transfer) */
int read_data(uint8_t *buf, size_t buf_size, size_t *out_len);

/* Struct with owned members: cleanup function */
typedef struct {
    char   *name;    /* owned */
    int    *data;    /* owned */
    size_t  len;
} Record;

void record_free(Record *r) {
    free(r->name);
    free(r->data);
    /* r itself is caller's responsibility */
}

void record_destroy(Record **rp) {
    if (!rp || !*rp) return;
    record_free(*rp);
    free(*rp);
    *rp = NULL;
}

Arena / Bump Allocator Pattern

/* Arena: bulk-allocate from a large buffer; free everything at once.
 * Eliminates individual frees, no fragmentation, excellent cache locality. */

typedef struct {
    uint8_t *base;
    size_t   pos;
    size_t   cap;
} Arena;

Arena arena_new(size_t capacity) {
    return (Arena){
        .base = malloc(capacity),
        .pos  = 0,
        .cap  = capacity,
    };
}

void *arena_alloc(Arena *a, size_t size, size_t align) {
    /* Align pos up to required alignment */
    size_t aligned = (a->pos + align - 1) & ~(align - 1);
    if (aligned + size > a->cap) return NULL;  /* out of space */
    a->pos = aligned + size;
    return a->base + aligned;
}

void arena_reset(Arena *a) { a->pos = 0; }  /* reuse without re-alloc */
void arena_free(Arena *a)  { free(a->base); a->base = NULL; }

/* Usage: request lifetime — create arena, process request, reset */
Arena scratch = arena_new(1024 * 1024);  /* 1 MB */
char *name = arena_alloc(&scratch, 256, 1);
int  *data = arena_alloc(&scratch, 100 * sizeof(int), alignof(int));
/* ... use name and data ... */
arena_reset(&scratch);   /* free all at once */

Valgrind & AddressSanitizer

# Valgrind memcheck: detects leaks, use-after-free, uninit reads
valgrind --leak-check=full --track-origins=yes ./program

# AddressSanitizer: faster, built into clang/gcc
gcc -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer -g ./program
./program   # crashes immediately on first violation with stack trace

# MemorySanitizer: uninitialized memory reads (clang only)
clang -fsanitize=memory -g ./program

# Combine sanitizers
clang -fsanitize=address,undefined -g ./program

11. Preprocessor

Macros

/* Object-like macros */
#define MAX_SIZE    1024
#define PI          3.14159265358979323846
#define TRUE        1
#define FALSE       0

/* Function-like macros: all args in parens, whole expression in parens */
#define MAX(a, b)       ((a) > (b) ? (a) : (b))
#define SQUARE(x)       ((x) * (x))
#define ARRAY_LEN(arr)  (sizeof(arr) / sizeof((arr)[0]))

/* Stringification: # converts argument to string literal */
#define STRINGIFY(x)  #x
#define TOSTRING(x)   STRINGIFY(x)   /* double expansion for macros */
STRINGIFY(hello)        /* "hello" */
STRINGIFY(MAX_SIZE)     /* "MAX_SIZE" — not "1024" */
TOSTRING(MAX_SIZE)      /* "1024" — expands macro first */

/* Token concatenation: ## joins tokens */
#define MAKE_VAR(prefix, num)  prefix##num
int MAKE_VAR(count, 1) = 0;   /* expands to: int count1 = 0; */

/* Variadic macros (C99) */
#define LOG(fmt, ...)  fprintf(stderr, "[LOG] " fmt "\n", ##__VA_ARGS__)
LOG("value = %d", x);
LOG("hello");   /* ## before __VA_ARGS__ removes comma when empty (GCC extension) */

/* do-while(0) wrapper: makes multi-statement macro safe in if/else */
#define SWAP(a, b, T)  do {   \
    T _tmp = (a);             \
    (a) = (b);                \
    (b) = _tmp;               \
} while (0)

/* if (cond) SWAP(x, y, int);  -- works correctly */
Macro Double Evaluation
MAX(x++, y) expands to ((x++) > (y) ? (x++) : (y))x may be incremented twice. Never pass expressions with side effects to function-like macros. Prefer static inline functions.

Conditional Compilation

/* Include guard */
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* header content */
#endif

/* #pragma once — non-standard but universally supported */
#pragma once

/* Platform detection */
#ifdef _WIN32
    #include <windows.h>
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
#elif defined(__linux__)
    #include <unistd.h>
#endif

/* Feature flags */
#ifdef DEBUG
    #define LOG(msg)  fprintf(stderr, "DEBUG: %s\n", (msg))
#else
    #define LOG(msg)  ((void)0)   /* no-op; casts to void to suppress warnings */
#endif

/* #if with expressions */
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
    /* C11 or later */
    #define THREAD_LOCAL _Thread_local
#else
    #define THREAD_LOCAL /* fallback: not thread-local */
#endif

/* Architecture detection */
#if defined(__x86_64__) || defined(_M_X64)
    #define ARCH_X86_64
#elif defined(__aarch64__) || defined(_M_ARM64)
    #define ARCH_ARM64
#endif

Predefined Macros

/* Standard predefined macros */
__FILE__            /* current source file name as string literal */
__LINE__            /* current line number as integer */
__func__            /* current function name as string (C99) */
__DATE__            /* compilation date: "Feb 23 2026" */
__TIME__            /* compilation time: "14:30:00" */
__STDC__            /* 1 if conforming C implementation */
__STDC_VERSION__    /* 199901L (C99), 201112L (C11), 201710L (C17) */

/* Useful for debugging */
#define HERE  fprintf(stderr, "%s:%d in %s()\n", __FILE__, __LINE__, __func__)

/* Assertion with location */
#define ASSERT_MSG(cond, msg)                                          \
    do {                                                               \
        if (!(cond)) {                                                 \
            fprintf(stderr, "Assertion failed: %s\n"                  \
                    "  Message: %s\n"                                  \
                    "  At: %s:%d (%s)\n",                             \
                    #cond, (msg), __FILE__, __LINE__, __func__);       \
            abort();                                                   \
        }                                                              \
    } while (0)

X-Macros Pattern

/* X-macros: define a list once, expand it multiple ways */

/* Define the table in one place */
#define ERROR_TABLE \
    X(ERR_NONE,    0, "no error")         \
    X(ERR_IO,      1, "I/O error")        \
    X(ERR_NOMEM,   2, "out of memory")    \
    X(ERR_INVALID, 3, "invalid argument") \
    X(ERR_TIMEOUT, 4, "operation timed out")

/* Expansion 1: generate enum */
typedef enum {
#define X(name, val, msg)  name = val,
    ERROR_TABLE
#undef X
} ErrorCode;

/* Expansion 2: generate string table */
static const char *error_messages[] = {
#define X(name, val, msg)  [val] = msg,
    ERROR_TABLE
#undef X
};

const char *error_string(ErrorCode e) {
    if ((unsigned)e < sizeof(error_messages)/sizeof(*error_messages))
        return error_messages[e];
    return "unknown error";
}

12. File I/O

stdio.h Basics

#include <stdio.h>
#include <errno.h>

/* Open / close */
FILE *f = fopen("data.bin", "rb");    /* "r","w","a","r+","w+","rb","wb" */
if (f == NULL) {
    perror("fopen");    /* prints: "fopen: No such file or directory" */
    return -1;
}
fclose(f);

/* Formatted I/O */
fprintf(f, "count=%d name=%s\n", count, name);
fscanf(f, "%d %63s", &count, name);   /* 63 = buffer_size - 1 */

/* Binary I/O */
size_t nread = fread(buf, sizeof(uint32_t), 100, f);   /* read 100 uint32_t */
if (nread < 100 && ferror(f)) { /* handle error */ }

size_t nwritten = fwrite(buf, sizeof(uint32_t), 100, f);

/* Text line I/O */
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
    /* fgets includes the '\n'; remove it: */
    line[strcspn(line, "\n")] = '\0';
    process(line);
}

/* Seeking */
fseek(f, 0, SEEK_END);            /* go to end */
long file_size = ftell(f);        /* get position = file size */
fseek(f, 0, SEEK_SET);            /* back to start */
rewind(f);                        /* equivalent to fseek(f, 0, SEEK_SET) + clears error */

/* fseek constants */
/* SEEK_SET = 0 (from beginning), SEEK_CUR = 1 (from current), SEEK_END = 2 (from end) */

/* For large files (> 2GB), use fseeko/ftello with off_t */
fseeko(f, (off_t)offset, SEEK_SET);

I/O Error Handling

#include <errno.h>
#include <string.h>

FILE *f = fopen(path, "r");
if (f == NULL) {
    /* errno is set by fopen on failure */
    fprintf(stderr, "Cannot open '%s': %s\n", path, strerror(errno));
    return -errno;
}

/* After reading, check for actual error vs EOF */
if (fgets(line, sizeof(line), f) == NULL) {
    if (feof(f)) {
        /* normal end of file */
    } else if (ferror(f)) {
        fprintf(stderr, "Read error: %s\n", strerror(errno));
    }
}

/* Clear error and EOF flags */
clearerr(f);

/* Buffering control */
setvbuf(f, NULL, _IONBF, 0);     /* unbuffered */
setvbuf(f, NULL, _IOLBF, 0);     /* line-buffered */
setvbuf(f, buf, _IOFBF, 4096);   /* fully buffered with user buffer */
fflush(f);                         /* force write buffered data */

POSIX File I/O (Brief)

#include <fcntl.h>
#include <unistd.h>

/* Lower-level, no buffering, works with file descriptors (int) */
int fd = open("data.bin", O_RDONLY);
if (fd == -1) { perror("open"); return -1; }

uint8_t buf[4096];
ssize_t nread;
while ((nread = read(fd, buf, sizeof(buf))) > 0) {
    /* process buf[0..nread-1] */
}
if (nread == -1) { perror("read"); }

write(fd, data, len);
close(fd);

/* Flags for open() */
/* O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_NONBLOCK */
int wfd = open("out.bin", O_WRONLY | O_CREAT | O_TRUNC, 0644);

/* Memory-mapped I/O (POSIX) */
#include <sys/mman.h>
void *map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); }
/* Access like normal memory: map[0], map[100], etc. */
munmap(map, file_size);

13. Error Handling Patterns

Return Codes & errno

#include <errno.h>
#include <string.h>

/* Convention: return 0 (or non-negative) on success, negative on error */
int parse_int(const char *str, int *out) {
    if (!str || !out) return -EINVAL;

    errno = 0;
    char *end;
    long val = strtol(str, &end, 10);

    if (errno != 0)     return -errno;        /* overflow, etc. */
    if (end == str)     return -EINVAL;        /* no digits */
    if (*end != '\0')   return -EINVAL;        /* trailing garbage */
    if (val > INT_MAX || val < INT_MIN) return -ERANGE;

    *out = (int)val;
    return 0;
}

/* Caller checks and handles */
int value;
int rc = parse_int(input, &value);
if (rc < 0) {
    fprintf(stderr, "parse error: %s\n", strerror(-rc));
    return rc;
}

/* perror: print errno message with context */
if (fopen("x", "r") == NULL) perror("fopen");  /* "fopen: No such file or directory" */

/* strerror: get error string without printing */
const char *msg = strerror(ENOENT);   /* "No such file or directory" */

goto Cleanup Pattern

/* Standard pattern in Linux kernel and systems code:
 * acquire resources top-to-bottom, release bottom-to-top on error */

int process_file(const char *path) {
    int rc = -1;
    FILE *f = NULL;
    char *buf = NULL;
    sqlite3 *db = NULL;

    f = fopen(path, "r");
    if (!f) { rc = -errno; goto out; }

    buf = malloc(MAX_BUF);
    if (!buf) { rc = -ENOMEM; goto out; }

    if (sqlite3_open(":memory:", &db) != SQLITE_OK) {
        rc = -EIO;
        goto out;
    }

    /* actual work */
    rc = do_work(f, buf, db);

out:
    if (db)  sqlite3_close(db);
    free(buf);
    if (f)   fclose(f);
    return rc;
}

setjmp / longjmp (Non-local Jumps)

#include <setjmp.h>

static jmp_buf error_ctx;

void risky_operation(void) {
    /* Throw: jump back to the setjmp call site */
    longjmp(error_ctx, 1);   /* second arg becomes return value of setjmp */
}

int main(void) {
    /* setjmp returns 0 initially, non-zero when jumped to */
    int code = setjmp(error_ctx);
    if (code == 0) {
        /* normal path */
        risky_operation();
    } else {
        /* error path: code is the value passed to longjmp */
        fprintf(stderr, "Error: %d\n", code);
    }
    return 0;
}

/* Caveats:
 * - Local variables modified between setjmp and longjmp may be clobbered
 *   unless they are volatile.
 * - Destructors / cleanup functions do NOT run (unlike C++ exceptions).
 * - Not async-signal-safe.
 * Use sparingly — goto cleanup is almost always cleaner for single-file code. */

14. Concurrency (C11)

C11 Threads (threads.h)

#include <threads.h>
#include <stdio.h>

typedef struct { int id; int *result; } Args;

int worker(void *arg) {
    Args *a = arg;
    *a->result = a->id * a->id;
    return thrd_success;
}

int main(void) {
    thrd_t t;
    int result;
    Args args = { .id = 7, .result = &result };

    /* Create thread */
    if (thrd_create(&t, worker, &args) != thrd_success) {
        return 1;
    }

    /* Wait for completion */
    int ret;
    thrd_join(t, &ret);
    printf("result=%d, thread returned %d\n", result, ret);

    return 0;
}

/* Mutex */
mtx_t lock;
mtx_init(&lock, mtx_plain);     /* mtx_plain, mtx_recursive, mtx_timed */
mtx_lock(&lock);
/* critical section */
mtx_unlock(&lock);
mtx_destroy(&lock);

/* Condition variable */
cnd_t cond;
cnd_init(&cond);
cnd_wait(&cond, &lock);         /* atomically unlock and wait */
cnd_signal(&cond);              /* wake one waiter */
cnd_broadcast(&cond);           /* wake all waiters */
cnd_destroy(&cond);

/* Thread-local storage */
_Thread_local int tls_value;    /* each thread has its own copy */

/* Once initialization */
static once_flag init_flag = ONCE_FLAG_INIT;
call_once(&init_flag, initialize);   /* runs initialize exactly once */

Atomics (stdatomic.h)

#include <stdatomic.h>

/* Atomic types */
_Atomic int counter = 0;
atomic_int  ref_count = ATOMIC_VAR_INIT(0);
atomic_bool shutdown = false;

/* Operations */
atomic_fetch_add(&counter, 1);          /* counter++ atomically */
atomic_fetch_sub(&counter, 1);          /* counter-- */
atomic_store(&shutdown, true);
bool s = atomic_load(&shutdown);

/* Compare-and-swap (CAS) — foundation of lock-free algorithms */
int expected = 0;
int desired  = 1;
bool swapped = atomic_compare_exchange_strong(&counter, &expected, desired);
/* if counter == expected, set to desired and return true
 * otherwise, load current value into expected and return false */

/* Memory ordering */
atomic_store_explicit(&flag, 1, memory_order_release);
int v = atomic_load_explicit(&flag, memory_order_acquire);
/* acquire/release: synchronizes with paired store/load */

/* Memory order summary */
/* memory_order_relaxed:    no sync, just atomicity (cheapest) */
/* memory_order_acquire:    this load synchronizes with a release store */
/* memory_order_release:    this store synchronizes with an acquire load */
/* memory_order_acq_rel:    both acquire and release (for RMW operations) */
/* memory_order_seq_cst:    total order across all seq_cst ops (most expensive) */

/* Reference counting example */
typedef struct {
    atomic_int refcount;
    char       data[256];
} Shared;

void shared_retain(Shared *s) {
    atomic_fetch_add_explicit(&s->refcount, 1, memory_order_relaxed);
}

void shared_release(Shared *s) {
    if (atomic_fetch_sub_explicit(&s->refcount, 1, memory_order_acq_rel) == 1) {
        /* Last reference: safe to free */
        free(s);
    }
}

POSIX Threads (pthreads) — Brief Comparison

#include <pthread.h>

/* pthreads predates C11 threads; more widely available on existing systems */
pthread_t thread;
pthread_create(&thread, NULL, worker_func, arg);
pthread_join(thread, &retval);

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_rdlock(&rwlock);   /* multiple readers OK */
pthread_rwlock_wrlock(&rwlock);   /* exclusive write */
pthread_rwlock_unlock(&rwlock);

/* Link with -lpthread */
C11 Threads vs pthreads
C11 threads (threads.h) are the standard but are not implemented on all platforms (notably missing on older glibc). For portable systems code, pthreads is still the safer choice. On Windows, use Win32 threads or a portability layer.

15. Build Systems & Tooling

Make

# Makefile structure:
# target: prerequisites
#         recipe (must be TAB-indented, not spaces)
/* Makefile */
CC      = gcc
CFLAGS  = -Wall -Wextra -std=c17 -g
LDFLAGS =
LDLIBS  = -lm

# Source and object files
SRCS = main.c utils.c io.c
OBJS = $(SRCS:.c=.o)
TARGET = myapp

# Default target
all: $(TARGET)

# Link
$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)

# Compile (pattern rule)
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# Automatic dependency tracking
DEPS = $(OBJS:.o=.d)
-include $(DEPS)
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c -o $@ $<

clean:
	rm -f $(OBJS) $(DEPS) $(TARGET)

.PHONY: all clean

CMake Essentials

# CMakeLists.txt
/* CMakeLists.txt */
cmake_minimum_required(VERSION 3.20)
project(MyApp C)

set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Compiler warnings
add_compile_options(-Wall -Wextra -Wpedantic)

# Executable
add_executable(myapp main.c utils.c io.c)

# Library
add_library(mylib STATIC lib.c)           # static library
add_library(myshared SHARED lib.c)        # shared library

# Link
target_link_libraries(myapp PRIVATE mylib m)

# Include paths
target_include_directories(myapp PRIVATE include/)

# Find external library
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE OpenSSL::SSL)

# Build
# cmake -B build -DCMAKE_BUILD_TYPE=Release
# cmake --build build
# cmake --build build -- -j$(nproc)

Static vs Shared Libraries

# Build a static library (.a)
gcc -c -Wall -fPIC utils.c -o utils.o
ar rcs libutils.a utils.o          # ar: create static archive
# Link against it
gcc main.o -L. -lutils -o program

# Build a shared library (.so on Linux, .dylib on macOS)
gcc -shared -fPIC -o libutils.so utils.o   # Linux
gcc -dynamiclib -o libutils.dylib utils.o  # macOS
# Link against it
gcc main.o -L. -lutils -Wl,-rpath,. -o program

# Inspect a library
nm -D libutils.so          # list exported symbols
objdump -d libutils.a      # disassemble
ldd program                # list shared lib dependencies (Linux)
otool -L program           # same on macOS
readelf -d program         # ELF dynamic section

gdb Basics

gcc -g -O0 -o program main.c    # compile with debug info, no optimization
gdb ./program

# Inside gdb:
run                    # start the program
run arg1 arg2          # with arguments
break main             # breakpoint at function
break file.c:42        # breakpoint at line
watch expr             # watchpoint: break when expr changes
info breakpoints       # list breakpoints
delete 1               # delete breakpoint #1

next                   # step over (next line)
step                   # step into
finish                 # run until current function returns
continue               # resume until next breakpoint

print x                # print variable
print *ptr             # print what pointer points to
print arr[0]@5         # print arr[0..4]
x/10xw 0xADDR          # examine 10 words at address (hex)
info locals            # all local variables
backtrace              # call stack
frame 2                # switch to stack frame 2
list                   # show source context

# Post-mortem debugging
ulimit -c unlimited    # enable core dumps
gdb ./program core     # load core dump

16. Common Patterns & Idioms

Opaque Pointers (Information Hiding)

/* In the public header: queue.h */
typedef struct Queue Queue;   /* forward declaration — clients see only a pointer */

Queue *queue_new(size_t capacity);
void   queue_free(Queue *q);
bool   queue_push(Queue *q, int value);
bool   queue_pop(Queue *q, int *out);
size_t queue_size(const Queue *q);

/* In the implementation: queue.c */
struct Queue {             /* full definition hidden from clients */
    int    *data;
    size_t  head, tail, size, cap;
};

Queue *queue_new(size_t capacity) {
    Queue *q = malloc(sizeof(*q));
    if (!q) return NULL;
    q->data = malloc(capacity * sizeof(int));
    if (!q->data) { free(q); return NULL; }
    q->head = q->tail = q->size = 0;
    q->cap = capacity;
    return q;
}

/* This enforces an ABI: clients can use Queue without knowing its internals.
 * Changing struct fields doesn't break client code — just recompile queue.c. */

Callback + Context Pattern

/* void* user_data carries caller context through a generic interface */
typedef void (*event_cb)(int event, void *user_data);

typedef struct {
    event_cb callback;
    void    *user_data;
} EventSource;

void event_source_emit(EventSource *src, int event) {
    if (src->callback) {
        src->callback(event, src->user_data);
    }
}

/* Caller provides context */
typedef struct { int count; FILE *log; } MyCtx;

static void my_handler(int event, void *user_data) {
    MyCtx *ctx = user_data;   /* safe: we know what we registered */
    ctx->count++;
    fprintf(ctx->log, "event %d (total: %d)\n", event, ctx->count);
}

MyCtx ctx = { .count = 0, .log = stderr };
EventSource src = { .callback = my_handler, .user_data = &ctx };

container_of Macro (Linux Kernel Style)

/* Given a pointer to a struct member, get pointer to the containing struct */
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

/* Usage: intrusive linked list */
typedef struct ListNode {
    struct ListNode *next;
} ListNode;

typedef struct {
    int      value;
    ListNode node;    /* embedded list node */
} MyData;

/* Traverse list and recover containing struct */
for (ListNode *n = list_head; n != NULL; n = n->next) {
    MyData *item = container_of(n, MyData, node);
    printf("%d\n", item->value);
}

Object-Oriented C (vtable pattern)

/* Simulate polymorphism with function pointer tables */

/* "Interface" / vtable */
typedef struct ShapeVtable {
    double (*area)(const void *self);
    double (*perimeter)(const void *self);
    void   (*draw)(const void *self);
    void   (*destroy)(void *self);
} ShapeVtable;

/* Base "class" */
typedef struct {
    const ShapeVtable *vtable;
} Shape;

/* "Subclass": Circle */
typedef struct {
    Shape   base;      /* MUST be first member */
    double  radius;
} Circle;

static double circle_area(const void *self) {
    const Circle *c = self;
    return 3.14159 * c->radius * c->radius;
}
static double circle_perimeter(const void *self) {
    const Circle *c = self;
    return 2 * 3.14159 * c->radius;
}
static void circle_draw(const void *self) { printf("O"); }
static void circle_destroy(void *self) { free(self); }

static const ShapeVtable circle_vtable = {
    .area      = circle_area,
    .perimeter = circle_perimeter,
    .draw      = circle_draw,
    .destroy   = circle_destroy,
};

Circle *circle_new(double r) {
    Circle *c = malloc(sizeof(*c));
    c->base.vtable = &circle_vtable;
    c->radius = r;
    return c;
}

/* Polymorphic call via Shape* (works for any shape) */
void shape_print_info(const Shape *s) {
    printf("area=%.2f perimeter=%.2f\n",
           s->vtable->area(s),
           s->vtable->perimeter(s));
}

Generic Programming with _Generic (C11)

/* _Generic selects an expression based on the type of the controlling expression */

#define abs_val(x) _Generic((x),   \
    int:          abs(x),          \
    long:         labs(x),         \
    long long:    llabs(x),        \
    float:        fabsf(x),        \
    double:       fabs(x),         \
    long double:  fabsl(x)         \
)

abs_val(3);     /* calls abs(3) */
abs_val(3.0);   /* calls fabs(3.0) */

/* Type-safe print */
#define print(x) _Generic((x),  \
    int:    printf("%d\n", x),  \
    double: printf("%f\n", x),  \
    char *: printf("%s\n", x),  \
    default: printf("?\n")      \
)

/* Generic min/max using statement expressions (GCC extension) */
#define min(a, b)                  \
    __extension__ ({               \
        __typeof__(a) _a = (a);    \
        __typeof__(b) _b = (b);    \
        _a < _b ? _a : _b;        \
    })

17. Common Pitfalls & Gotchas

Undefined Behavior Catalog

Undefined behavior (UB) means the standard imposes no requirements. The compiler may assume UB never occurs, which can produce startling optimizations that eliminate your safety checks.

UB CategoryExampleTypical Symptom
Signed integer overflowINT_MAX + 1Wrap, or compiler removes overflow checks
Null pointer dereference*((int*)NULL)SIGSEGV, or compiler assumes pointer non-null
Out-of-bounds accessarr[size]Heap/stack corruption, SIGSEGV
Use after freeRead freed memorySilent corruption, crash
Double freefree(p); free(p);Heap corruption, security exploit
Uninitialized readint x; use(x);Garbage values, non-determinism
Strict aliasing violationCast int* to float*Incorrect optimization, wrong values
Data raceConcurrent unprotected writeNon-determinism, corruption
Shift past width1 << 32 on 32-bit intCompiler-dependent result
Modifying string literal"hi"[0] = 'H'SIGSEGV (read-only segment)
Misaligned pointer accessAccess int at odd addressSIGBUS on strict-alignment archs
/* Strict aliasing: compiler assumes pointers to different types don't alias */
/* This is UB and may be optimized incorrectly: */
float f = 3.14f;
int *ip = (int *)&f;   /* violates strict aliasing */
*ip = 0;

/* Correct type punning: use union or memcpy */
uint32_t bits;
memcpy(&bits, &f, sizeof bits);   /* always correct, compiler optimizes to no-op */

/* Shift past type width: UB even for unsigned */
uint32_t u = 1u;
u << 32;    /* UB: shift amount must be < width of the promoted type */
u << 31;    /* OK: 0x80000000 */

/* Pointer comparison across objects: UB */
int a, b;
if (&a < &b) { }   /* UB: only < within same array/object is defined */

Implementation-Defined Behavior

/* Implementation-defined: documented but not portable */

/* char signedness: may be signed or unsigned */
char c = 200;    /* may be -56 (signed) or 200 (unsigned) */
/* Fix: use signed char or unsigned char explicitly when it matters */

/* struct padding and alignment: varies by ABI */
struct S { char c; int i; };
sizeof(struct S);   /* 8 on most 32/64-bit systems, but could be 5 */
/* Fix: use offsetof() to find actual offsets */

/* Right shift of signed negative: arithmetic vs logical */
int x = -8;
x >> 1;   /* implementation-defined: -4 (arithmetic) or large positive (logical) */
/* Fix: use unsigned types for bit manipulation */

/* Byte order (endianness) */
uint32_t n = 0x01020304;
uint8_t *bytes = (uint8_t *)&n;
/* bytes[0] == 0x01 on big-endian, 0x04 on little-endian */
/* Fix: use htonl/ntohl for network byte order, or explicit shifting */

/* Atomic portable byte order conversion */
uint32_t host_to_be32(uint32_t x) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    return __builtin_bswap32(x);
#else
    return x;
#endif
}

Common Security Vulnerabilities

/* 1. Format string injection: NEVER use untrusted input as format string */
char *user_input = get_input();
printf(user_input);            /* VULNERABLE: user can embed %n, %x etc. */
printf("%s", user_input);      /* SAFE: user_input is always a string arg */

/* 2. Buffer overflow */
char buf[64];
gets(buf);                     /* NEVER USE gets(): no bounds checking, removed in C11 */
fgets(buf, sizeof(buf), stdin); /* SAFE */
scanf("%63s", buf);            /* SAFE: limit to 63 chars + NUL */

/* 3. Integer overflow leading to buffer overflow */
size_t count = user_provided_count;
char *p = malloc(count * sizeof(struct Record));  /* overflow if count * size > SIZE_MAX */
/* Safe multiplication: */
if (count > SIZE_MAX / sizeof(struct Record)) { /* overflow would occur */ }
p = malloc(count * sizeof(struct Record));

/* 4. Off-by-one */
char buf2[10];
for (int i = 0; i <= 10; i++) buf2[i] = 0;  /* writes 11 bytes to 10-byte buffer */
for (int i = 0; i < 10; i++) buf2[i] = 0;   /* correct */

/* 5. Use of unvalidated array index */
int table[256];
int idx = user_value;    /* may be negative, or >= 256 */
table[idx] = 1;          /* out-of-bounds if not validated */
/* Safe: */
if ((unsigned)idx < 256) table[idx] = 1;  /* cast to unsigned catches negatives */

/* 6. TOCTOU (time-of-check to time-of-use) */
if (access(path, W_OK) == 0)   /* check */
    f = fopen(path, "w");       /* another process may have changed file between check and use */
/* Fix: open with O_CREAT|O_EXCL and check for EEXIST */

Portability Gotchas

/* sizeof(long) varies: 4 bytes on Windows/LLP64, 8 bytes on Linux/LP64 */
/* Use int32_t/int64_t from <stdint.h> for exact sizes */

/* Alignment requirements */
struct S { char c; double d; };
/* Accessing d through an unaligned pointer is UB on strict-alignment architectures */
/* Always use offsetof(S, d) or let the compiler handle it */

/* va_arg with promoted types: char/short are promoted to int */
va_arg(ap, char);   /* WRONG: char is promoted to int */
va_arg(ap, int);    /* CORRECT */

/* main() return value: only 0 and EXIT_SUCCESS/EXIT_FAILURE are portable.
 * Other values may be truncated to 8 bits on some systems. */

/* Stack allocation limits: typically 1-8 MB on most systems */
int huge[1000000];   /* 4MB on stack — likely stack overflow */

/* Comparison of function pointer to NULL: technically implementation-defined
 * but works on all common ABIs */
void (*fp)(void) = NULL;
if (fp != NULL) fp();   /* safe on all common platforms */

/* printf format specifiers and types must match exactly */
size_t n = 100;
printf("%d\n", n);     /* WRONG on 64-bit: size_t is unsigned long */
printf("%zu\n", n);    /* CORRECT: %zu for size_t */
printf("%td\n", pd);   /* CORRECT: %td for ptrdiff_t */
Checklist for Production C Code
  • Compile with -Wall -Wextra -Wpedantic -Werror
  • Run with AddressSanitizer and UBSanitizer during development
  • Valgrind before shipping to catch leaks and uninit reads
  • Use const everywhere you don't need to modify data
  • Validate all inputs before using them as array indices or allocation sizes
  • Use snprintf instead of sprintf, fgets instead of gets
  • Never pass untrusted input as a format string to printf/fprintf
  • Zero pointers after free() to catch use-after-free
  • Use static_assert to document and enforce platform assumptions
  • Keep functions small; use the goto-cleanup pattern for resource management