C Handbook
C is a general-purpose, procedural language created by Dennis Ritchie at Bell Labs in 1972. It compiles directly to machine code, gives you explicit control over memory and hardware, and introduces almost no runtime overhead. The Unix kernel, the Linux kernel, CPython, the V8 JS engine, SQLite, and nearly every programming language runtime are written in C.
Pick C when
- You are writing an OS kernel, bootloader, or firmware — C is the de-facto language for bare-metal targets. You control every byte of memory.
- You need a tiny runtime — C has almost no runtime. The binary is small and starts in microseconds, which matters on microcontrollers (Arduino, STM32, ESP32).
- You need maximum portability across architectures — C compilers exist for every CPU ever made, including 8-bit AVR, RISC-V, and exotic DSPs.
- You are writing a library others will call from any language — the C ABI is the universal FFI glue. Python, Ruby, Java, Rust, Go all call C libraries natively.
- You are interfacing directly with hardware registers — volatile pointers, inline assembly, and bit fields map directly to hardware without abstraction cost.
- Performance is the only constraint — hand-tuned C is still the baseline everything else is measured against in benchmarks.
Think twice before choosing C when
- You want memory safety — C has no bounds checking, no ownership system, and no safe defaults. Buffer overflows and use-after-free are easy to introduce and hard to detect. Prefer Rust for new systems code that must be safe.
- Your project is large and team-facing — C has no namespaces, no generics, and no modules in the modern sense. Large C codebases require strong discipline. C++ or Go scale better.
- You need rich data structures out of the box — C has no standard hash map, no dynamic array in the stdlib (you use raw malloc). You write everything yourself or pull in a library.
- Productivity matters more than performance — writing and debugging C takes significantly longer than Python, Go, or Rust for the same feature. Reserve it for where it counts.
C vs. its closest alternatives
- C vs Rust — same performance, but Rust's borrow checker eliminates entire bug classes at compile time. New systems code that does not need C ABI compatibility should prefer Rust.
- C vs C++ — C++ is a strict superset. Use C when you want to stay in a small, auditable language; use C++ when you need OOP, templates, or the STL.
- C vs Go — Go has GC, is much easier to write correctly, and still compiles to a single binary. Choose Go for network services; C for kernels and embedded.
Resources
- cppreference.com — C — comprehensive C language and stdlib reference
- iso-9899.info — C standard information and links
- Modern C — free book by Jens Gustedt covering C17
- C FAQ — classic answers to frequently asked C questions
- Compiler Explorer — see generated assembly for any C snippet
Topics
Variables & Types
C is statically typed — every variable must be declared with its type before use.
| Type | Size (typical) | Description |
|---|---|---|
char | 1 byte | Single character / small integer |
int | 4 bytes | Signed integer |
long | 4–8 bytes | Larger signed integer |
float | 4 bytes | Single-precision float |
double | 8 bytes | Double-precision float |
_Bool | 1 byte | Boolean (C99, or use stdbool.h) |
#include <stdio.h>
#include <stdbool.h>
int main(void) {
int age = 30;
float pi = 3.14159f;
double precise = 3.14159265358979;
char letter = 'A';
bool active = true;
// stdint.h for fixed-width: int8_t, int16_t, int32_t, int64_t
// unsigned variants: uint8_t, uint16_t, uint32_t, uint64_t
const int MAX = 100; // immutable
printf("%d %f %c\n", age, pi, letter);
return 0;
}Operators
// Arithmetic
int a = 10 + 3; // 13
int b = 10 - 3; // 7
int c = 10 * 3; // 30
int d = 10 / 3; // 3 (integer division)
int e = 10 % 3; // 1 (remainder)
// Comparison → result is int (0 or 1)
int r1 = (a == b); // 0
int r2 = (a != b); // 1
// Logical
int l1 = (a > 0 && b > 0); // AND
int l2 = (a > 0 || b < 0); // OR
int l3 = !(a == b); // NOT
// Bitwise
int x = 0b1010 & 0b1100; // AND → 8
int y = 0b1010 | 0b1100; // OR → 14
int z = 0b1010 ^ 0b1100; // XOR → 6
int s = 1 << 3; // left shift → 8
int t = 16 >> 2; // right shift → 4
// Shorthand
a += 5; a -= 5; a *= 2; a /= 2; a %= 3;
a++; a--;Control Flow
int x = 42;
// if / else if / else
if (x > 100) {
printf("big\n");
} else if (x > 10) {
printf("medium\n");
} else {
printf("small\n");
}
// Ternary
const char *label = (x % 2 == 0) ? "even" : "odd";
// switch
switch (x % 3) {
case 0: printf("divisible by 3\n"); break;
case 1: printf("remainder 1\n"); break;
default: printf("remainder 2\n");
}
// while
int i = 0;
while (i < 5) { printf("%d ", i); i++; }
// do-while
do { printf("%d ", i); i--; } while (i > 0);
// for
for (int j = 0; j < 5; j++) { printf("%d ", j); }
// break / continue
for (int k = 0; k < 10; k++) {
if (k == 3) continue;
if (k == 7) break;
printf("%d ", k);
}Functions
Functions must be declared before they are called (either defined first or forward-declared).
#include <stdio.h>
#include <stdarg.h>
// Forward declaration (prototype)
int add(int a, int b);
// Function definition
int add(int a, int b) {
return a + b;
}
// void function
void greet(const char *name) {
printf("Hello, %s!\n", name);
}
// Variadic function
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; i++) total += va_arg(args, int);
va_end(args);
return total;
}
// Function pointer
int (*op)(int, int) = add;
printf("%d\n", op(3, 4)); // 7Pointers
Pointers store memory addresses and are fundamental to C programming.
int x = 10;
int *p = &x; // p holds address of x
printf("%d\n", *p); // dereference → 10
*p = 20; // modify x through p
// Pointer arithmetic
int arr[] = {1, 2, 3, 4, 5};
int *q = arr; // points to arr[0]
printf("%d\n", *(q + 2)); // arr[2] = 3
// Pointer to pointer
int **pp = &p;
printf("%d\n", **pp);
// NULL pointer
int *null_p = NULL;
if (null_p == NULL) printf("null\n");
// const pointers
const int *cp = &x; // can't change *cp
int * const pc = &x; // can't change pc itself
const int * const cpc = &x; // neither
// void pointer (generic)
void *vp = &x;
int val = *(int *)vp; // cast to useArrays & Strings
#include <string.h>
// Arrays
int nums[5] = {10, 20, 30, 40, 50};
int auto_size[] = {1, 2, 3}; // size inferred = 3
int matrix[2][3] = {{1,2,3},{4,5,6}};
// Strings are char arrays terminated by '\0'
char greeting[6] = "Hello";
char name[] = "World";
// String functions (string.h)
size_t len = strlen(name); // 5
strcpy(greeting, name); // copy
strncpy(greeting, name, 5); // safe copy
strcat(greeting, "!"); // concatenate
int cmp = strcmp(name, greeting); // 0 if equal
char *found = strstr(name, "orl"); // find substringStructs & Unions
typedef struct {
char name[50];
int age;
float score;
} Student;
Student s = {"Alice", 20, 95.5f};
printf("%s is %d years old\n", s.name, s.age);
// Pointer to struct → use ->
Student *sp = &s;
sp->age = 21;
// Union — all members share the same memory
typedef union {
int i;
float f;
char bytes[4];
} Data;
Data d;
d.i = 42; // setting one invalidates others
// Enum
typedef enum { RED, GREEN, BLUE } Color;
Color c = GREEN; // value is 1Memory Management
C requires manual memory management using standard library functions.
#include <stdlib.h>
// malloc — uninitialized memory
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) { /* handle failure */ }
// calloc — zero-initialized
int *zeros = (int *)calloc(5, sizeof(int));
// realloc — resize
arr = (int *)realloc(arr, 10 * sizeof(int));
// free — release memory
free(arr);
free(zeros);
arr = NULL; // avoid dangling pointerInput / Output
// printf format specifiers
printf("%d", 42); // integer
printf("%u", 42u); // unsigned
printf("%f", 3.14); // float
printf("%.2f", 3.14); // 2 decimal places
printf("%s", "hello"); // string
printf("%c", 'A'); // char
printf("%p", &x); // pointer address
printf("%x", 255); // hex
printf("%05d", 42); // zero-padded width 5
// scanf — read from stdin
int n; float f; char s[50];
scanf("%d", &n);
scanf("%49s", s); // reads up to 49 chars
// File I/O
FILE *fp = fopen("file.txt", "r"); // "r","w","a","rb","wb"
if (fp == NULL) { perror("fopen"); }
char line[256];
while (fgets(line, sizeof(line), fp)) {
printf("%s", line);
}
fclose(fp);
// Write to file
FILE *out = fopen("out.txt", "w");
fprintf(out, "Value: %d\n", 42);
fclose(out);Preprocessor
// Include headers
#include <stdio.h> // system header
#include "myheader.h" // local header
// Macros
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
// Conditional compilation
#define DEBUG
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg)
#endif
// Include guard (in header files)
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... header content ...
#endif
// Predefined macros
printf("%s\n", __FILE__); // current filename
printf("%d\n", __LINE__); // current line number
printf("%s\n", __DATE__); // compilation date
printf("%s\n", __func__); // current function name (C99)Function Pointers & Callbacks
Function pointers let you store and pass functions as values. typedef gives the signature a readable name. Dispatch tables and callbacks like qsort are the canonical use cases.
#include <stdio.h>
#include <stdlib.h>
// typedef makes function pointer types readable
typedef int (*BinOp)(int, int);
typedef void (*Action)(int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
void print_val(int x) { printf('%d\n', x); }
// Accepting a callback as a parameter
void apply(int *arr, int n, Action fn) {
for (int i = 0; i < n; i++) fn(arr[i]);
}
// Dispatch table — array of function pointers
BinOp ops[] = { add, sub, mul };
const char *names[] = { "add", "sub", "mul" };
// qsort comparator — standard callback pattern
int cmp_int(const void *a, const void *b) {
return (*(const int *)a) - (*(const int *)b);
}
int main(void) {
int nums[] = {5, 2, 8, 1, 9};
qsort(nums, 5, sizeof(int), cmp_int); // sort ascending
apply(nums, 5, print_val); // print each
// Use the dispatch table
for (int i = 0; i < 3; i++)
printf('%s(10, 3) = %d\n', names[i], ops[i](10, 3));
return 0;
}Multidimensional Arrays
C stores 2D arrays in row-major order. The inner dimension must be known at compile time when passing to functions; C99 VLAs or flat dynamic arrays remove that restriction.
#include <stdio.h>
#include <stdlib.h>
// 2D array — elements stored in row-major order
int grid[3][4] = {
{ 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9, 10, 11, 12}
};
// Passing to a function: inner dimension must be explicit
void print_grid(int rows, int mat[][4]) {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < 4; c++)
printf('%3d', mat[r][c]);
putchar('\n');
}
}
// C99 VLA parameter — size can be runtime values
void vla_sum(int rows, int cols, int mat[rows][cols]) {
int total = 0;
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
total += mat[r][c];
printf('sum = %d\n', total);
}
// Flat dynamic 2D array (contiguous, cache-friendly)
int rows = 3, cols = 4;
int *flat = (int *)malloc(rows * cols * sizeof(int));
flat[1 * cols + 2] = 99; // equivalent to mat[1][2]
free(flat);
// Pointer-to-pointer (non-contiguous, allows ragged rows)
int **pp = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++)
pp[i] = (int *)malloc(cols * sizeof(int));
pp[1][2] = 42;
for (int i = 0; i < rows; i++) free(pp[i]);
free(pp);Bit Fields
Bit fields let you pack multiple small values into a single struct word — commonly used for hardware registers and network protocol headers.
#include <stdio.h>
#include <stdint.h>
// Bit fields — each member gets exactly N bits
typedef struct {
unsigned int ready : 1; // 1-bit flag (0 or 1)
unsigned int mode : 3; // 3-bit field (0–7)
unsigned int error : 4; // 4-bit field (0–15)
unsigned int address : 24; // 24-bit address
} HardwareReg;
// Limitations: ordering is implementation-defined,
// cannot take the address of a bit field,
// cannot have arrays of bit fields.
// Union trick: read bit fields or raw bytes
typedef union {
struct {
uint8_t low : 4; // lower nibble
uint8_t high : 4; // upper nibble
} nibbles;
uint8_t byte;
} Byte;
int main(void) {
HardwareReg reg = {0};
reg.ready = 1;
reg.mode = 5;
reg.error = 0;
reg.address = 0x1A2B3C;
printf('ready=%u mode=%u error=%u\n',
reg.ready, reg.mode, reg.error);
Byte b;
b.byte = 0xAB;
printf('high=0x%X low=0x%X\n', b.nibbles.high, b.nibbles.low);
return 0;
}typedef & enum Patterns
typedef aliases types so call sites are cleaner. Enums with explicit values and bitflag patterns replace magic numbers and #define constants.
#include <stdio.h>
// typedef for structs — drop the 'struct' keyword at usage sites
typedef struct Point { int x; int y; } Point;
// Anonymous struct with typedef (no tag needed)
typedef struct { float real; float imag; } Complex;
// Self-referential struct must use the tag name internally
typedef struct Node {
int value;
struct Node *next;
} Node;
// typedef for function pointer — name the signature once
typedef int (*Predicate)(int);
int is_even(int n) { return n % 2 == 0; }
// Enum with explicit values
typedef enum {
HTTP_OK = 200,
HTTP_NOT_FOUND = 404,
HTTP_ERROR = 500
} HttpStatus;
// Bitflag enum — powers of two so flags can be OR-combined
typedef enum {
PERM_NONE = 0,
PERM_READ = 1 << 0, // 1
PERM_WRITE = 1 << 1, // 2
PERM_EXECUTE = 1 << 2 // 4
} Permission;
int main(void) {
Point p = {3, 4};
HttpStatus status = HTTP_OK;
int perms = PERM_READ | PERM_WRITE; // combine flags
Predicate fn = is_even;
printf('is_even(%d) = %d\n', p.x, fn(p.x));
printf('status=%d perms=%d\n', status, perms);
return 0;
}volatile, restrict & _Atomic
volatile prevents the compiler from optimising away memory accesses. restrict promises no pointer aliasing, enabling vectorisation. _Atomic (C11) provides lock-free atomic operations.
#include <stdio.h>
#include <signal.h>
#include <stdatomic.h> // C11
// volatile: every read/write goes to memory — no caching by compiler.
// Use for memory-mapped hardware registers and signal-handler flags.
volatile uint32_t *STATUS_REG = (volatile uint32_t *)0x40001000;
volatile sig_atomic_t got_signal = 0;
void sig_handler(int sig) { (void)sig; got_signal = 1; }
// restrict: promise that pointers do not alias each other.
// Enables auto-vectorisation and other loop optimisations.
void vec_add(int n,
float * restrict dst,
const float * restrict a,
const float * restrict b) {
for (int i = 0; i < n; i++) dst[i] = a[i] + b[i];
}
// _Atomic (C11) — lock-free atomic operations via <stdatomic.h>
atomic_int counter = ATOMIC_VAR_INIT(0);
void worker(void) {
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
}
int snapshot(void) {
return atomic_load_explicit(&counter, memory_order_acquire);
}
// Compare-and-exchange: set to 42 only if current value is 0
int expected = 0;
bool swapped = atomic_compare_exchange_strong(&counter, &expected, 42);
// If false, expected is updated to the actual current valueCompilation Model
C compiles each source file as an independent translation unit. The preprocessor, compiler, assembler, and linker are separate stages — understanding them explains header guards, extern, static linkage, and inline.
// Compilation pipeline:
// source.c → preprocessor → compiler → assembler → linker → binary
//
// Each .c file is a separate *translation unit* compiled independently.
// The linker resolves cross-unit references at the final step.
// ── math.h (header — declarations only) ────────────────────────────
#pragma once // simpler alternative to include guards
int add(int a, int b); // function declaration (no body)
extern int call_count; // extern: tells compiler it lives elsewhere
// ── math.c (translation unit — definitions) ────────────────────────
#include "math.h"
int call_count = 0; // actual definition (storage allocated here)
int add(int a, int b) { call_count++; return a + b; }
// static at file scope → internal linkage (invisible to other units)
static int helper(int x) { return x * 2; }
// inline in a header → definition in every unit; compiler may inline
static inline int clamp(int v, int lo, int hi) {
return v < lo ? lo : v > hi ? hi : v;
}
// ── Makefile (typical pattern) ──────────────────────────────────────
// CC = gcc
// CFLAGS = -Wall -Wextra -std=c11 -O2
// SRCS = main.c math.c
// OBJS = $(SRCS:.c=.o)
//
// app: $(OBJS)
// $(CC) $(OBJS) -o app
//
// %.o: %.c
// $(CC) $(CFLAGS) -c lt; -o $@
//
// clean:
// rm -f $(OBJS) appLinked Lists & Trees
Linked lists and trees are the building blocks of dynamic data structures in C. Because C lacks a built-in standard collection library, these patterns are implemented by hand and passed to calling code as opaque pointers.
// Singly-linked list
typedef struct Node {
int value;
struct Node *next;
} Node;
Node *push(Node *head, int val) {
Node *n = malloc(sizeof(Node));
n->value = val;
n->next = head;
return n; // new head
}
void print_list(const Node *head) {
for (const Node *cur = head; cur; cur = cur->next)
printf('%d ', cur->value);
putchar('\n');
}
void free_list(Node *head) {
while (head) {
Node *tmp = head;
head = head->next;
free(tmp);
}
}
// Binary search tree
typedef struct Tree { int val; struct Tree *left, *right; } Tree;
Tree *insert(Tree *root, int v) {
if (!root) {
Tree *n = calloc(1, sizeof(Tree));
n->val = v;
return n;
}
if (v < root->val) root->left = insert(root->left, v);
else root->right = insert(root->right, v);
return root;
}
void inorder(const Tree *t) {
if (!t) return;
inorder(t->left);
printf('%d ', t->val);
inorder(t->right);
}Error Handling
C has no exceptions. Errors propagate through return codes, the global errno, or — for truly exceptional non-local control flow — setjmp/longjmp.
#include <errno.h>
#include <string.h>
#include <setjmp.h>
// errno + strerror / perror
FILE *f = fopen('missing.txt', 'r');
if (!f) {
fprintf(stderr, 'fopen: %s\n', strerror(errno));
perror('fopen'); // same + automatic prefix
}
// Return-code pattern (dominant in C)
int parse_int(const char *s, int *out) {
char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (errno != 0 || end == s || *end != '\0') return -1;
*out = (int)v;
return 0;
}
// setjmp / longjmp — non-local exit (use sparingly)
jmp_buf jb;
void risky(int x) {
if (x < 0) longjmp(jb, 1); // jump to setjmp call site
printf('value: %d\n', x);
}
int main(void) {
if (setjmp(jb) == 0) {
risky(-1); // may longjmp
} else {
puts('caught error via longjmp');
}
// Custom error type
typedef enum { ERR_OK = 0, ERR_IO, ERR_PARSE, ERR_OOM } Err;
const char *err_str[] = { 'ok', 'io error', 'parse error', 'out of memory' };
Err e = ERR_PARSE;
fprintf(stderr, 'error: %s\n', err_str[e]);
return 0;
}C11 / C17 Features
_Generic enables type-dispatching macros without overloading. _Static_assert validates assumptions at compile time. Anonymous structs and alignas/alignof round out the C11 additions.
#include <assert.h>
#include <stdalign.h>
#include <stdnoreturn.h>
// _Static_assert — compile-time assertion (C11)
_Static_assert(sizeof(int) >= 4, 'int must be at least 4 bytes');
_Static_assert(sizeof(void *) == 8, 'expected 64-bit pointers');
// _Generic — type-generic dispatch (C11)
#define type_name(x) _Generic((x), \
int: 'int', \
float: 'float', \
double: 'double', \
char *: 'char *', \
default: 'unknown')
#define ABS(x) _Generic((x), \
int: abs(x), \
float: fabsf(x), \
double: fabs(x))
// Anonymous structs and unions (C11) — members accessible without a member name
typedef struct {
int kind;
union {
int i_val;
float f_val;
struct { int x; int y; }; // x and y directly accessible
};
} Variant;
Variant v = { .kind = 1, .f_val = 3.14f };
// alignas / alignof (C11)
alignas(16) float simd_buf[4]; // 16-byte aligned
size_t align = alignof(double); // 8
// noreturn (C11 via <stdnoreturn.h>)
noreturn void die(const char *msg) {
fprintf(stderr, '%s\n', msg);
exit(EXIT_FAILURE);
}
// Compound literals (C99) — temporary object with explicit type
int *arr = (int []){ 10, 20, 30 }; // array compound literalThreads & Concurrency
POSIX threads (pthreads) are the standard threading API on Linux/macOS. C11 added <stdatomic.h> for lock-free atomic operations. Compile with -pthread.
#include <pthread.h>
#include <stdatomic.h> // C11
#include <stdio.h>
// ── Mutex ─────────────────────────────────────────────────────────────────
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
int shared = 0;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mtx);
shared++;
pthread_mutex_unlock(&mtx);
}
return NULL;
}
// ── Condition variable ────────────────────────────────────────────────────
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
void *producer(void *arg) {
pthread_mutex_lock(&mtx);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
return NULL;
}
void *consumer(void *arg) {
pthread_mutex_lock(&mtx);
while (!ready) pthread_cond_wait(&cond, &mtx);
printf('got item\n');
pthread_mutex_unlock(&mtx);
return NULL;
}
// ── C11 atomics (lock-free) ───────────────────────────────────────────────
atomic_int counter = ATOMIC_VAR_INIT(0);
void *worker(void *arg) {
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
return NULL;
}
// ── Launching threads ─────────────────────────────────────────────────────
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf('shared = %d\n', shared); // 200000
pthread_mutex_destroy(&mtx);
return 0;
}
// Compile: gcc -pthread file.cABI & Interoperability
Every language with FFI capabilities can call C functions. A stable public header with extern "C" guards, packed structs for wire formats, and a shared library are the three pillars of C interoperability.
// C has a stable ABI — the lingua franca of native interop.
// Any language with FFI can call C functions directly.
// ── Public header (stable API contract) ──────────────────────────────────
#pragma once
#ifdef __cplusplus
extern 'C' { // tell C++ to use C linkage (no name mangling)
#endif
typedef struct { double re; double im; } Complex;
double c_add(double a, double b);
Complex cx_mul(Complex a, Complex b);
void *arena_alloc(size_t bytes);
void arena_free(void *p);
#ifdef __cplusplus
}
#endif
// ── Packed struct — exact memory layout, no padding ───────────────────────
#pragma pack(push, 1)
typedef struct {
uint8_t type;
uint16_t length;
uint8_t data[64];
} Packet; // sizeof == 67
#pragma pack(pop)
// ── Calling from Python (ctypes) ──────────────────────────────────────────
// import ctypes
// lib = ctypes.CDLL('./libmath.so')
// lib.c_add.restype = ctypes.c_double
// lib.c_add.argtypes = [ctypes.c_double, ctypes.c_double]
// result = lib.c_add(1.5, 2.5) # 4.0
// ── Calling from Rust (FFI) ───────────────────────────────────────────────
// extern 'C' { fn c_add(a: f64, b: f64) -> f64; }
// let result = unsafe { c_add(1.5, 2.5) };
// Build shared library: gcc -shared -fPIC -o libmath.so math.cString Processing
C strings are null-terminated byte arrays. The standard library provides string.h and ctype.h utilities, but safe usage demands explicit size tracking. Key patterns include bounded copies with snprintf, delimiter-based splitting via strtok, and full error checking when converting strings to numbers with strtol.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// Safe string copy that always null-terminates
void safe_strcpy(char *dst, const char *src, size_t size) {
if (size == 0) return;
strncpy(dst, src, size - 1);
dst[size - 1] = '\0';
}
// Trim leading and trailing whitespace in-place
char *trim(char *s) {
while (isspace((unsigned char)*s)) s++;
if (*s == '\0') return s;
char *end = s + strlen(s) - 1;
while (end > s && isspace((unsigned char)*end)) end--;
*(end + 1) = '\0';
return s;
}
// Split string by delimiter — returns heap-allocated array of tokens
char **split(const char *str, char delim, int *count) {
char *copy = strdup(str);
int n = 1;
for (char *p = copy; *p; p++) if (*p == delim) n++;
char **tokens = malloc(n * sizeof(char *));
int i = 0;
char *tok = strtok(copy, (char[]){delim, '\0'});
while (tok) { tokens[i++] = strdup(tok); tok = strtok(NULL, (char[]){delim, '\0'}); }
free(copy);
*count = i;
return tokens;
}
// String to integer with full error checking
int str_to_int(const char *s, int *out) {
char *end; errno = 0;
long v = strtol(s, &end, 10);
if (errno || end == s || *end) return -1;
*out = (int)v; return 0;
}
// Convert string to uppercase in-place
void str_upper(char *s) {
for (; *s; s++) *s = (char)toupper((unsigned char)*s);
}
// Find all occurrences of needle in haystack
int count_occurrences(const char *hay, const char *needle) {
int count = 0; size_t nlen = strlen(needle);
for (const char *p = hay; (p = strstr(p, needle)); p += nlen) count++;
return count;
}
int main(void) {
char buf[64];
safe_strcpy(buf, ' hello world ', sizeof(buf));
printf('trimmed: [%s]\n', trim(buf)); // [hello world]
int n; char **parts = split('a,b,c,d', ',', &n);
for (int i = 0; i < n; i++) { printf('%s ', parts[i]); free(parts[i]); }
free(parts);
}Sorting & Searching
The standard library's qsort and bsearch are generic — they work on any element type via a comparator function pointer. For small or nearly-sorted data, insertion sort outperforms quicksort. Binary search requires a sorted array and finds elements in O(log n).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// qsort comparators
int cmp_int_asc(const void *a, const void *b) {
return (*(const int *)a) - (*(const int *)b);
}
int cmp_str(const void *a, const void *b) {
return strcmp(*(const char **)a, *(const char **)b);
}
typedef struct { int key; char name[32]; } Record;
int cmp_record(const void *a, const void *b) {
return ((const Record *)a)->key - ((const Record *)b)->key;
}
// Binary search (manual — bsearch from stdlib works for sorted arrays)
int binary_search(const int *arr, int n, int target) {
int lo = 0, hi = n - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1; // not found
}
// Insertion sort — efficient for small or nearly-sorted arrays
void insertion_sort(int *arr, int n) {
for (int i = 1; i < n; i++) {
int key = arr[i], j = i - 1;
while (j >= 0 && arr[j] > key) { arr[j+1] = arr[j]; j--; }
arr[j+1] = key;
}
}
int main(void) {
int nums[] = {5, 3, 8, 1, 9, 2, 7};
int n = 7;
qsort(nums, n, sizeof(int), cmp_int_asc);
for (int i = 0; i < n; i++) printf('%d ', nums[i]); // 1 2 3 5 7 8 9
printf('\n');
int idx = binary_search(nums, n, 7);
printf('found 7 at index %d\n', idx); // 4
// bsearch — stdlib version
int target = 3;
int *found = bsearch(&target, nums, n, sizeof(int), cmp_int_asc);
printf('bsearch: %s\n', found ? 'found' : 'not found');
}Hash Tables
C has no built-in hash map, so they are implemented by hand. The standard approach uses an array of buckets with separate chaining (linked lists) for collision resolution, a polynomial rolling hash, and explicit memory management. Open addressing is an alternative that avoids pointer chasing.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HASH_SIZE 64
typedef struct Entry {
char key[64];
int value;
struct Entry *next; // chaining for collisions
} Entry;
typedef struct { Entry *buckets[HASH_SIZE]; } HashMap;
static unsigned int hash(const char *s) {
unsigned int h = 5381;
while (*s) h = h * 33 ^ (unsigned char)*s++;
return h % HASH_SIZE;
}
void map_put(HashMap *m, const char *key, int value) {
unsigned int h = hash(key);
for (Entry *e = m->buckets[h]; e; e = e->next)
if (strcmp(e->key, key) == 0) { e->value = value; return; }
Entry *e = calloc(1, sizeof(Entry));
strncpy(e->key, key, sizeof(e->key) - 1);
e->value = value;
e->next = m->buckets[h];
m->buckets[h] = e;
}
int map_get(const HashMap *m, const char *key, int *out) {
unsigned int h = hash(key);
for (const Entry *e = m->buckets[h]; e; e = e->next)
if (strcmp(e->key, key) == 0) { *out = e->value; return 1; }
return 0;
}
void map_free(HashMap *m) {
for (int i = 0; i < HASH_SIZE; i++) {
Entry *e = m->buckets[i];
while (e) { Entry *next = e->next; free(e); e = next; }
}
}
int main(void) {
HashMap m = {0};
map_put(&m, 'alice', 42);
map_put(&m, 'bob', 17);
int v;
if (map_get(&m, 'alice', &v)) printf('alice = %d\n', v);
map_free(&m);
}Generic Patterns (void*)
void* is C's mechanism for type-erased, generic containers. A generic dynamic array (vector) stores elements as raw bytes, using memcpy for copies and a size parameter at every operation. Callers cast the returned pointer back to the concrete type. The same pattern underlies qsort, bsearch, and any callback-driven API.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Generic dynamic array using void*
typedef struct {
void *data;
size_t elem_size;
size_t len;
size_t cap;
} Vec;
Vec vec_new(size_t elem_size) {
return (Vec){ .data = NULL, .elem_size = elem_size, .len = 0, .cap = 0 };
}
void vec_push(Vec *v, const void *elem) {
if (v->len == v->cap) {
v->cap = v->cap ? v->cap * 2 : 8;
v->data = realloc(v->data, v->cap * v->elem_size);
}
memcpy((char *)v->data + v->len * v->elem_size, elem, v->elem_size);
v->len++;
}
void *vec_get(const Vec *v, size_t i) {
return (char *)v->data + i * v->elem_size;
}
void vec_free(Vec *v) { free(v->data); *v = vec_new(v->elem_size); }
// Generic swap
void swap(void *a, void *b, size_t size) {
char tmp[256];
memcpy(tmp, a, size);
memcpy(a, b, size);
memcpy(b, tmp, size);
}
// Generic min — caller provides comparator
void *generic_min(void *arr, size_t n, size_t size,
int (*cmp)(const void *, const void *)) {
void *m = arr;
for (size_t i = 1; i < n; i++) {
void *p = (char *)arr + i * size;
if (cmp(p, m) < 0) m = p;
}
return m;
}
int cmp_int(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}
int main(void) {
Vec v = vec_new(sizeof(int));
for (int i = 0; i < 5; i++) vec_push(&v, &i);
printf('%d\n', *(int *)vec_get(&v, 2)); // 2
vec_free(&v);
int arr[] = {5, 3, 8, 1, 9};
int *m = generic_min(arr, 5, sizeof(int), cmp_int);
printf('min = %d\n', *m); // 1
}Signal Handling
Signals are asynchronous notifications sent to a process by the OS or other processes. Use sigaction (not signal) for portable, predictable handler installation. Only async-signal-safe functions may be called from a handler — the safest approach is to set a volatile sig_atomic_t flag and act on it in the main loop.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// sig_atomic_t is the only type guaranteed safe to write in a handler
volatile sig_atomic_t running = 1;
volatile sig_atomic_t reload = 0;
// SIGINT handler — graceful shutdown
void handle_sigint(int sig) {
(void)sig;
running = 0;
}
// SIGHUP handler — reload config (conventional for daemons)
void handle_sighup(int sig) {
(void)sig;
reload = 1;
}
// sigaction is preferred over signal() — more portable and predictable
void install_handlers(void) {
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // restart interrupted syscalls
sa.sa_handler = handle_sigint;
sigaction(SIGINT, &sa, NULL);
sa.sa_handler = handle_sighup;
sigaction(SIGHUP, &sa, NULL);
// Ignore SIGPIPE (broken pipe) — common for server processes
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, NULL);
}
// Block a signal temporarily (critical section)
void with_sigint_blocked(void) {
sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGINT);
sigprocmask(SIG_BLOCK, &block, &old);
// ... critical section ...
sigprocmask(SIG_SETMASK, &old, NULL); // restore
}
// Raise a signal to self
void cause_alarm(void) { raise(SIGALRM); }
int main(void) {
install_handlers();
printf('running — press Ctrl+C to stop\n');
while (running) {
if (reload) { printf('reloading config...\n'); reload = 0; }
pause(); // sleep until any signal arrives
}
printf('\nshutting down cleanly\n');
}Dynamic Loading
dlopen / dlsym / dlclose from dlfcn.h let a program load shared libraries at runtime without linking them at build time. This enables plugin architectures, optional feature loading, and hot-reload patterns. The symbol lookup returns a void* that must be cast to the correct function pointer type.
#include <stdio.h>
#include <dlfcn.h> // POSIX — Linux/macOS
// Dynamic loading lets you load shared libraries (.so / .dylib) at runtime.
// This enables plugin systems and optional feature loading.
typedef int (*add_fn)(int, int);
typedef void (*greet_fn)(const char *);
int main(void) {
// Open the shared library (RTLD_LAZY: resolve symbols on first use)
void *handle = dlopen('./libmath.so', RTLD_LAZY);
if (!handle) {
fprintf(stderr, 'dlopen: %s\n', dlerror());
return 1;
}
// Clear any prior error
dlerror();
// Look up a symbol by name — always use void* then cast
add_fn add = (add_fn)(uintptr_t)dlsym(handle, 'add');
const char *err = dlerror();
if (err) { fprintf(stderr, 'dlsym: %s\n', err); dlclose(handle); return 1; }
printf('add(3, 4) = %d\n', add(3, 4));
// Look up another symbol
greet_fn greet = (greet_fn)(uintptr_t)dlsym(handle, 'greet');
if (!dlerror()) greet('world');
// Close the library (decrements reference count)
dlclose(handle);
// Plugin pattern: iterate directory, dlopen each .so, call init()
// typedef void (*plugin_init)(void);
// plugin_init init = dlsym(h, 'plugin_init');
// if (!dlerror()) init();
return 0;
}
// Build: gcc -rdynamic main.c -ldl
// Lib: gcc -shared -fPIC -o libmath.so math.cNetwork Sockets
POSIX sockets provide a uniform interface for TCP and UDP networking. Key functions are socket, bind, listen, accept, connect, send, and recv. Use getaddrinfo for portable name resolution instead of gethostbyname, and always convert port numbers with htons.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
// TCP server — accept one connection and echo back
void run_server(uint16_t port) {
int srv = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY,
.sin_port = htons(port)
};
bind(srv, (struct sockaddr *)&addr, sizeof(addr));
listen(srv, 5);
struct sockaddr_in client_addr; socklen_t clen = sizeof(client_addr);
int conn = accept(srv, (struct sockaddr *)&client_addr, &clen);
char buf[1024];
ssize_t n = recv(conn, buf, sizeof(buf) - 1, 0);
buf[n] = '\0';
printf('received: %s\n', buf);
send(conn, buf, (size_t)n, 0); // echo
close(conn); close(srv);
}
// TCP client — connect and send a message
void run_client(const char *host, uint16_t port) {
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };
struct addrinfo *res;
if (getaddrinfo(host, NULL, &hints, &res) != 0) return;
int fd = socket(res->ai_family, res->ai_socktype, 0);
((struct sockaddr_in *)res->ai_addr)->sin_port = htons(port);
if (connect(fd, res->ai_addr, res->ai_addrlen) == 0) {
const char *msg = 'hello';
send(fd, msg, strlen(msg), 0);
char buf[256]; ssize_t n = recv(fd, buf, sizeof(buf)-1, 0);
buf[n] = '\0'; printf('reply: %s\n', buf);
}
freeaddrinfo(res); close(fd);
}Memory Layout & Alignment
The compiler inserts padding bytes between struct fields to satisfy platform alignment requirements. Reordering fields from largest to smallest minimises wasted space. offsetof reveals exact field offsets, and the container-of macro recovers an outer struct pointer from an inner member — the technique used by Linux kernel lists.
#include <stdio.h>
#include <stddef.h>
#include <stdalign.h>
// Padding — compiler inserts bytes to satisfy alignment requirements
typedef struct {
char a; // 1 byte
// 3 bytes padding
int b; // 4 bytes
char c; // 1 byte
// 7 bytes padding
double d; // 8 bytes
} Padded; // sizeof == 24
// Reordered — fields largest-first eliminates most padding
typedef struct {
double d; // 8 bytes
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
// 2 bytes padding
} Packed; // sizeof == 16
// offsetof — byte offset of a field within a struct
void print_offsets(void) {
printf('Padded: a=%zu b=%zu c=%zu d=%zu size=%zu\n',
offsetof(Padded, a), offsetof(Padded, b),
offsetof(Padded, c), offsetof(Padded, d), sizeof(Padded));
printf('Packed: d=%zu b=%zu a=%zu c=%zu size=%zu\n',
offsetof(Packed, d), offsetof(Packed, b),
offsetof(Packed, a), offsetof(Packed, c), sizeof(Packed));
}
// Custom aligned allocation (C11+)
alignas(64) double simd_data[8]; // cache-line aligned
// Stack vs heap layout
void layout_demo(void) {
int stack_var = 42; // on the stack
int *heap_var = malloc(4); // on the heap
*heap_var = 42;
printf('stack=%p heap=%p\n', (void*)&stack_var, (void*)heap_var);
free(heap_var);
}
// Container-of pattern — recover outer struct from inner member pointer
typedef struct { int x; int y; } Point;
typedef struct { Point pos; int id; } Entity;
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
int main(void) {
print_offsets();
Entity e = {{3, 4}, 7};
Point *p = &e.pos;
Entity *recovered = container_of(p, Entity, pos);
printf('id = %d\n', recovered->id); // 7
}Secure Coding
C gives the programmer complete control — and complete responsibility. The most common vulnerabilities are buffer overflows (use snprintf, not sprintf), integer overflows (check before arithmetic), use-after-free (NULL after free), format string injection (never pass user input as the format), and timing side-channels in secret comparisons (constant-time memcmp).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
// 1. Always bound-check writes — use snprintf, not sprintf
void safe_format(char *buf, size_t size, const char *name) {
snprintf(buf, size, 'Hello, %s!', name); // always null-terminates
}
// 2. Integer overflow — check before arithmetic
int safe_add(int a, int b, int *result) {
if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b))
return -1; // overflow
*result = a + b;
return 0;
}
// 3. Use-after-free prevention — NULL pointer after free
void safe_free(void **p) { if (p && *p) { free(*p); *p = NULL; } }
// 4. Avoid gets(), scanf('%s',...) — use fgets or scanf with width
void safe_read(char *buf, size_t size) {
if (fgets(buf, (int)size, stdin)) {
size_t len = strlen(buf);
if (len > 0 && buf[len-1] == '\n') buf[len-1] = '\0';
}
}
// 5. Constant-time comparison to prevent timing attacks
int ct_memcmp(const void *a, const void *b, size_t n) {
const unsigned char *pa = a, *pb = b;
unsigned char diff = 0;
for (size_t i = 0; i < n; i++) diff |= pa[i] ^ pb[i];
return diff != 0;
}
// 6. Validate array indices before use
int safe_index(const int *arr, size_t len, size_t idx, int *out) {
if (idx >= len) return -1;
*out = arr[idx]; return 0;
}
// 7. Wipe sensitive data before freeing
void wipe(void *p, size_t n) { volatile unsigned char *vp = p; while (n--) *vp++ = 0; }
int main(void) {
char buf[32]; safe_format(buf, sizeof(buf), 'world');
printf('%s\n', buf);
int res; if (safe_add(INT_MAX, 1, &res) != 0) printf('overflow detected\n');
}Make & Build Systems
Make is the traditional C build tool. A well-structured Makefile separates source, object, and binary directories; uses pattern rules (%.o: %.c) and automatic variables ($@, lt;); and generates header dependency files with -MMD -MP so changed headers trigger recompilation. Address Sanitizer and UBSan are invaluable during development.
# Makefile — a realistic C project build system
# Run: make → build release binary
# make debug → build with sanitizers
# make test → compile and run tests
# make clean → remove build artifacts
CC := gcc
CFLAGS := -Wall -Wextra -Wpedantic -std=c11 -O2
DBGFLAGS:= -g -O0 -fsanitize=address,undefined -fno-omit-frame-pointer
LDFLAGS :=
LIBS := -lm
SRCDIR := src
OBJDIR := build
BINDIR := bin
SRCS := $(wildcard $(SRCDIR)/*.c)
OBJS := $(patsubst $(SRCDIR)/%.c, $(OBJDIR)/%.o, $(SRCS))
TARGET := $(BINDIR)/app
.PHONY: all debug test clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(BINDIR)
$(CC) $(OBJS) $(LDFLAGS) $(LIBS) -o $@
$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -MMD -MP -c lt; -o $@
-include $(OBJS:.o=.d) # auto-generated header dependencies
debug: CFLAGS := $(DBGFLAGS)
debug: $(TARGET)
test: CFLAGS += -DUNIT_TEST
test: $(TARGET)
./$(TARGET) --test
$(OBJDIR) $(BINDIR):
mkdir -p $@
clean:
rm -rf $(OBJDIR) $(BINDIR)
# Explanation of key variables:
# $@ — target name
# lt; — first prerequisite (the .c file)
# $^ — all prerequisites
# -MMD -MP — generate .d dependency files for headers
# .PHONY — targets that are not real filesBest Practices
Naming & Style
Consistent naming removes cognitive overhead. Snake_case throughout, SCREAMING_SNAKE for constants, a p prefix for pointers, and is_/has_ for predicates make intent clear at a glance. File-scope helpers get static to prevent accidental linkage collisions.
// ── Naming & Style ──────────────────────────────────────────────────────────
// snake_case for variables, functions, and files
int user_count = 0;
float average_score = 0.0f;
void print_report(int page_num);
// SCREAMING_SNAKE_CASE for macros and compile-time constants
#define MAX_BUFFER_SIZE 4096
#define PI 3.14159265358979
// 'p' prefix for pointer variables
int *p_data = NULL;
char *p_message = NULL;
// 'is_' / 'has_' prefix for boolean-returning functions/variables
bool is_valid(const char *s) { return s && *s != '\0'; }
bool has_permission(int flags) { return (flags & PERM_READ) != 0; }
// struct tag matches the typedef name
typedef struct Buffer {
char *data;
size_t len;
size_t cap;
} Buffer;
// file-scoped helpers get 'static' (internal linkage, no collision risk)
static int compute_hash(const char *key);
static void grow_buffer(Buffer *b, size_t min_cap);
// 80-column soft limit; one statement per line; brace on same line (K&R)
for (int i = 0; i < n; i++) {
if (arr[i] < 0) {
arr[i] = 0;
}
}
// spaces around operators, no space between function name and '('
int result = a * b + c / d;
printf('%d\n', result);Pointer Safety
Uninitialised and dangling pointers are the most common source of C bugs. Always initialise to NULL, check after allocation, set to NULL after free, and use const for parameters the function must not modify. restrict conveys aliasing assumptions to both the reader and the optimiser.
// ── Pointer Safety ──────────────────────────────────────────────────────────
// 1. Always initialise — never leave a pointer uninitialised
int *p = NULL;
// 2. NULL-check after every malloc/calloc/realloc
int *arr = malloc(n * sizeof(int));
if (arr == NULL) { perror('malloc'); exit(EXIT_FAILURE); }
// 3. Set pointer to NULL immediately after free (prevents use-after-free)
free(arr);
arr = NULL;
// 4. Use const for pointer parameters that must not be modified
size_t count_zeros(const int *data, size_t n) {
size_t count = 0;
for (size_t i = 0; i < n; i++) if (data[i] == 0) count++;
return count;
}
// 5. Never return a pointer to a local (stack) variable
// BAD: int *bad(void) { int x = 42; return &x; } // dangling pointer!
// GOOD: heap-allocate, or pass a caller-provided buffer
int *make_array(size_t n) { return calloc(n, sizeof(int)); }
void fill_buffer(int *out, size_t n) { for (size_t i = 0; i < n; i++) out[i] = 0; }
// 6. restrict — promise no aliasing to enable auto-vectorisation
void vec_scale(float * restrict dst, const float * restrict src, float k, int n) {
for (int i = 0; i < n; i++) dst[i] = src[i] * k;
}
// 7. Use size_t for sizes; ptrdiff_t for pointer differences
size_t len = 256;
ptrdiff_t diff = p_end - p_start;
// 8. Do NOT arithmetic past the end of an array
int buf[8];
int *end = buf + 8; // one-past-end is valid to form, not to dereference
// *(buf + 8) — undefined behaviourMemory Management
Every malloc must have exactly one matching free. Use calloc for zero-initialised memory, assign realloc to a temporary before overwriting the original pointer, and free allocations in reverse order. GCC/Clang's cleanup attribute provides RAII-like automatic freeing.
// ── Memory Management ───────────────────────────────────────────────────────
#include <stdlib.h>
// Rule: every malloc must have exactly one matching free
typedef struct { char *name; int *scores; int n; } Student;
Student *student_new(const char *name, int n) {
Student *s = malloc(sizeof(Student));
if (!s) return NULL;
s->name = strdup(name); // malloc inside strdup
s->scores = calloc(n, sizeof(int)); // zero-initialised
if (!s->name || !s->scores) { free(s->name); free(s->scores); free(s); return NULL; }
s->n = n;
return s;
}
void student_free(Student *s) {
if (!s) return;
free(s->name); // free in reverse order of allocation
free(s->scores);
free(s); // then the container
}
// realloc safely: assign to a temp first — if realloc fails, original is preserved
void push(int **arr, size_t *cap, size_t *len, int val) {
if (*len == *cap) {
size_t new_cap = *cap ? *cap * 2 : 8;
int *tmp = realloc(*arr, new_cap * sizeof(int));
if (!tmp) { perror('realloc'); exit(EXIT_FAILURE); }
*arr = tmp;
*cap = new_cap;
}
(*arr)[(*len)++] = val;
}
// Prefer stack allocation for small, short-lived data
void process(void) {
char buf[256]; // stack: no malloc/free needed
int counts[16] = {0}; // zero-init at declaration
snprintf(buf, sizeof(buf), 'hello %d', 42);
}
// GCC/Clang cleanup attribute — RAII-like automatic free
#define auto_free __attribute__((cleanup(free_ptr)))
void free_ptr(void **p) { if (p && *p) { free(*p); *p = NULL; } }
void with_cleanup(void) {
auto_free char *tmp = malloc(64);
if (!tmp) return;
// tmp is freed automatically when this scope exits
snprintf(tmp, 64, 'auto-freed string');
printf('%s\n', tmp);
}Defensive Coding
Never trust input. Bound all string operations with snprintf and strncpy, parse numbers with strtol (not atoi), read lines with fgets (not gets), check integer arithmetic for overflow, and check every system-call return value. Compile-time _Static_assert and runtime assert document and enforce invariants.
// ── Defensive Coding ────────────────────────────────────────────────────────
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <assert.h>
// 1. snprintf over sprintf — always bounds-checks the destination
char buf[64];
snprintf(buf, sizeof(buf), 'user: %s', username); // truncates, never overflows
// sprintf(buf, 'user: %s', username); // BAD: no bounds check
// 2. strncpy + explicit terminator, or strlcpy where available
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';
// 3. strtol over atoi — detects errors via errno and end pointer
int parse_port(const char *s, int *out) {
char *end; errno = 0;
long v = strtol(s, &end, 10);
if (errno || end == s || *end || v < 1 || v > 65535) return -1;
*out = (int)v; return 0;
}
// 4. fgets over gets (gets is removed in C11)
char line[256];
if (fgets(line, sizeof(line), stdin)) {
line[strcspn(line, '\n')] = '\0'; // strip trailing newline
}
// 5. Integer overflow: check before arithmetic
int safe_mul(int a, int b, int *out) {
if (a != 0 && abs(b) > INT_MAX / abs(a)) return -1; // overflow
*out = a * b; return 0;
}
// 6. _Static_assert — validate assumptions at compile time (C11)
_Static_assert(sizeof(long) >= 8, 'need 64-bit long');
_Static_assert(CHAR_BIT == 8, 'need 8-bit bytes');
// 7. assert for pre/postconditions in debug builds
size_t safe_strlen(const char *s) {
assert(s != NULL); // precondition
size_t len = strlen(s);
assert(len < 65536); // postcondition / sanity
return len;
}
// 8. check every syscall return value
FILE *f = fopen('config.txt', 'r');
if (!f) { fprintf(stderr, 'fopen: %s\n', strerror(errno)); exit(1); }Header & Module Design
A good header is a minimal contract: declarations and types only, no definitions, no storage. Use #pragma once, forward-declare structs to hide internals, use extern for cross-unit globals, and version your API with a macro. Keep private helpers static in the implementation file so they never leak into the linker namespace.
// ── Header & Module Design ───────────────────────────────────────────────────
// ── widget.h — public interface ──────────────────────────────────────────────
#pragma once // preferred over manual include guards
// OR:
// #ifndef WIDGET_H
// #define WIDGET_H
// ...
// #endif
#include <stddef.h> // only include what this header directly uses
#include <stdbool.h>
// API version macro — lets callers detect and guard against breakage
#define WIDGET_API_VERSION 2
// Forward-declare the struct: callers don't need the internals
typedef struct Widget Widget;
// Public API — return codes, not raw pointers for error-prone paths
Widget *widget_new(const char *name, int capacity);
void widget_free(Widget *w);
bool widget_push(Widget *w, int value);
int widget_get(const Widget *w, size_t index);
size_t widget_size(const Widget *w);
// extern for cross-unit global — declaration only (no storage here)
extern int widget_instance_count;
// static inline for tiny helpers — definition goes in the header itself
static inline bool widget_empty(const Widget *w) {
return widget_size(w) == 0;
}
// ── widget.c — implementation (hidden from callers) ───────────────────────────
// #include 'widget.h'
// int widget_instance_count = 0; // actual definition (storage here)
// struct Widget { char name[64]; int *data; size_t len; size_t cap; };
// static void grow(Widget *w) { ... } // private helper; static = invisiblePerformance & Portability
Prefer fixed-width types for portable bit-exact data. Align hot structs to cache lines, use restrict and const to enable auto-vectorisation, minimise branches in hot loops, and use __builtin_expect for branch hints. Always compile with -Wall -Wextra -Werror and profile before micro-optimising.
// ── Performance & Portability ───────────────────────────────────────────────
#include <stdint.h>
#include <stdalign.h>
// 1. Fixed-width types for ABI stability and cross-platform correctness
uint8_t byte = 0xFF;
uint32_t flags = 0;
int64_t ticks = 0;
// Avoid bare 'int' when the width matters
// 2. Align hot structs to cache-line boundaries (64 bytes on x86/ARM)
typedef struct alignas(64) {
uint32_t counters[16];
} CacheLineAligned;
// 3. Minimise branches in hot loops — data-driven vs. if/else
// BAD: branch in every iteration
void scale_bad(float *arr, int n) {
for (int i = 0; i < n; i++) if (arr[i] < 0) arr[i] = 0;
}
// GOOD: branchless (conditional move)
void scale_good(float *arr, int n) {
for (int i = 0; i < n; i++) arr[i] = arr[i] < 0.0f ? 0.0f : arr[i];
}
// 4. restrict enables auto-vectorisation
void add_vecs(float * restrict dst,
const float * restrict a,
const float * restrict b, int n) {
for (int i = 0; i < n; i++) dst[i] = a[i] + b[i]; // SIMD-friendly
}
// 5. Mark read-only parameters const — documents intent, aids optimiser
double dot_product(const float * restrict a,
const float * restrict b, int n) {
double s = 0;
for (int i = 0; i < n; i++) s += a[i] * b[i];
return s;
}
// 6. __builtin_expect — hint branch predictor (GCC/Clang)
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
void process(int *data, int n) {
for (int i = 0; i < n; i++) {
if (unlikely(data[i] < 0)) { handle_error(); continue; }
data[i] *= 2;
}
}
// 7. Compile with -Wall -Wextra -Werror to catch problems early
// gcc -Wall -Wextra -Werror -O2 -std=c11 -o app src/*.c
// Profile with 'perf record ./app' + 'perf report' before micro-optimising