Open-source libraries used

1 libraries are bundled into this tool's code.

C Cheatsheet — A Concise Reference

A concise, self-contained reference for C (C11/C17): syntax, pointers, memory, strings, structs, and file I/O, plus threads, sockets, regex, time, processes, and build/debug essentials. Covers around 80% of day-to-day needs.

C

C C17 (ISO/IEC 9899:2018)

ISO C · Procedural · manual memory management · Static, weakly typed (with casts)

Recommended Learning Path

New to C? Read in this order: 1. Hello World — compile, run, exit codes 2. Variables & constants — types, sizes, const 3. Pointers & arrays — &, *, ->, pointer arithmetic 4. Control flow — if / switch / loops 5. Functions & variadics — pass-by-value vs pass-by-pointer 6. Strings & conversions — printf format table Consult File I/O and Error Handling when you need them. Once you're comfortable, return for structs, OO-style patterns, and the advanced sections. Have a specific task? Use the search box above — try "malloc", "socket", "pthread", "qsort", or "regex".

1.Hello World & the Build Environment

Compile, run and organize C programs: toolchain, IDE choice, multiple files, command-line arguments, exit codes.

Minimal Program

Every C program runs starting at main() and returns an int status to the OS: 0 means success, anything else means error. The #include at the top brings in declarations so the compiler knows what printf is.

1
2
3
4
5
6
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
return 0; // 0 = success to the OS
}

Compile & Run (gcc / clang)

C is compiled rather than interpreted: the compiler translates source into an executable, which you then run. Always enable warnings (-Wall -Wextra) and pin the language standard (-std=c17) so portability issues surface at compile time.

1
2
3
4
5
6
7
8
9
// $ gcc hello.c -o hello
// $ ./hello
//
// With warnings + standard version:
// $ gcc -std=c17 -Wall -Wextra -pedantic hello.c -o hello
// $ clang -std=c17 -Wall -Wextra hello.c -o hello // macOS
//
// Windows (MinGW):
// $ gcc hello.c -o hello.exe && hello.exe

Toolchain & IDE Checklist

You need a compiler plus an editor. Common setups: gcc on Linux, clang on macOS, MinGW-w64 or MSVC on Windows — all usable from VS Code or CLion. Run the version command once to confirm the toolchain works before debugging anything else.

1
2
3
4
// gcc (Linux) / clang (macOS) / MinGW-w64 (Windows)
// VS Code + C/C++ extension (tasks.json compiles)
// CLion / Visual Studio / Xcode
// Verify: $ gcc --version

Command-Line Arguments

main can receive what the user types on the command line: argc is the count, argv[] is the array of strings — argv[0] is always the program name. Treat arguments as strings and convert to numbers with strtol/strtod when needed.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc, char **argv) {
printf("%d args\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
// $ ./prog a b c -> argv[0]=./prog argv[1]=a ...

Multiple Files

Large programs are split into multiple .c files, with a shared .h header for declarations. Each .c is compiled separately and then linked; the header keeps declarations and definitions in sync so any signature change shows up everywhere it's used.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// utils.h — declarations (prototypes)
#ifndef UTILS_H
#define UTILS_H
int add(int a, int b);
#endif
//
// utils.c — definitions
#include "utils.h"
int add(int a, int b) { return a + b; }
//
// main.c
#include <stdio.h>
#include "utils.h"
int main(void) { printf("%d\n", add(2, 3)); return 0; }
//
// $ gcc main.c utils.c -o app

Exit Codes

The value main returns goes to the shell: 0 (EXIT_SUCCESS) for success, anything else for failure. Scripts and CI depend on this convention, so pick meaningful exit codes. Check with $? on Linux/macOS and %ERRORLEVEL% on Windows.

1
2
3
4
5
6
#include <stdlib.h>
int main(void) {
if (error) return EXIT_FAILURE; // 1
return EXIT_SUCCESS; // 0
}
// Shell: $ echo $? (Linux/macOS) or %ERRORLEVEL% (Windows)

Environment Variables

getenv() reads a variable from the process environment and returns its value, or NULL if unset. It's the standard way to inject configuration without rebuilding — but treat the result as untrusted input.

1
2
3
4
#include <stdlib.h>
#include <stdio.h>
const char *path = getenv("PATH");
if (path) printf("%s\n", path); // may be NULL if unset

Preprocessor Basics

The preprocessor runs before compilation: #define for macros, # and ## to splice tokens, #ifdef/#if for conditional code. It's a typeless, plain-text layer — prefer real functions and const in most cases.

1
2
3
4
5
6
7
8
9
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define STR(x) #x // "x" as string
#define CONCAT(a, b) a##b // tokens joined
#ifdef DEBUG
printf("debug build\n");
#endif
#if defined(__cplusplus)
// compiled as C++
#endif

2.Variables & Constants

Declarations, type sizes, fixed-width integers, const placement, storage classes, typedef, and enumerations.

Basic Declarations

A declaration writes the type first, then the name, and initializes at the declaration site (C99+). Initialize when you declare, so you don't later read uninitialized garbage.

1
2
3
4
int counter = 0;
double pi = 3.14159;
char letter = 'A';
_Bool flag = 1; // or <stdbool.h> -> bool

Type Sizes (Typical 64-bit Values)

C guarantees only minimum sizes — int may be 16 or 32 bits across platforms. Use sizeof to print the actual size on the target, and never hard-code byte counts.

1
2
3
4
5
6
7
8
9
// char 1 byte (-128..127)
// short 2 bytes (-32,768..32,767)
// int 4 bytes
// long 8 bytes (4 on Windows)
// long long 8 bytes (always >= 64 bit)
// float 4 bytes (6-7 sig digits)
// double 8 bytes (15-16 sig digits)
printf("int=%zu long=%zu ptr=%zu\n",
sizeof(int), sizeof(long), sizeof(void*));

Fixed-Width Types (C99+, <stdint.h>)

When memory layout must be exact — file formats, network protocols, hashes — reach for int32_t / uint64_t from <stdint.h>. They exist only when the platform can actually provide them, which is exactly why exact-width names are favored.

1
2
3
4
5
6
#include <stdint.h>
int32_t exact = 42; // exactly 32 bits
uint64_t big = 1ULL << 40;
int_least16_t small; // >= 16 bits
intptr_t addr; // big enough for a pointer
printf("%d\n", (int)sizeof(int32_t)); // always 4

Unsigned / Signed

Unsigned types are never negative and wrap around mod 2^N; signed types can be negative. Mixed in one expression, the signed operand is promoted to unsigned — a classic pitfall source (see the faq section).

1
2
3
unsigned int u = 4000000000U;
signed long long s = -9LL;
// Mixing signed + unsigned promotes to unsigned — beware (see faq)

const — Read-Only (Compile-Time Check)

const makes a variable read-only; the compiler rejects accidental writes at build time. Tag everything that shouldn't change with const — it documents intent and helps the optimizer.

1
2
const int MAX = 100;
// MAX = 5; // ERROR

const Pointer Positions (A Classic Confuser)

In a pointer declaration, const's position decides what it protects: before * protects the pointee, after * protects the pointer itself. Read the type right-to-left and the difference between these two lines becomes clear.

1
2
3
const int *p1; // pointer to const int — *p1 read-only, p1 reassignable
int *const p2; // const pointer to int — p2 read-only, *p2 writable
const int *const p3; // both read-only

Storage Classes

A storage class decides where a variable lives and how long. auto is the default for locals; static at file scope makes a name file-local; static on a local preserves its value across calls; extern refers to a name defined elsewhere.

1
2
3
4
5
static int file_scope = 0; // internal linkage (file-local)
extern int from_other; // defined in another file
static void helper(void) {} // file-local function
// 'static' on a local: persists across calls
void count(void) { static int n = 0; n++; printf("%d\n", n); }

typedef — A Type Alias

typedef gives a new name to a type. It doesn't create a new type, only an alias — but the alias keeps types short and consistent, hiding struct and function-pointer noise behind one readable word.

1
2
3
typedef unsigned long size_type;
typedef struct { int x, y; } Point; // anonymous struct + alias
Point p = { 1, 2 };

enum — Named Constants

enum names a set of related integer constants. Values start at 0 and step by 1, but you can assign explicit values or specify a range. Enums document themselves and work naturally with switch.

1
2
3
enum Color { RED, GREEN = 5, BLUE }; // 0, 5, 6
typedef enum { OK, WARN, ERR } Status;
Status s = OK;

Designated Initializers (C99+)

C99 lets you initialize members by name (.port = 8080) or array elements by index ([3] = 9), in any order. Members you don't mention are zeroed automatically — far clearer than a long positional list.

1
2
3
4
5
int arr[5] = { [0] = 1, [3] = 9 }; // 1 0 0 9 0
struct Config cfg = {
.host = "localhost",
.port = 8080,
};

Compound Literals (C99+)

A compound literal uses a cast-like syntax to inline-construct a temporary struct/array value. Convenient when you want to pass a one-shot value to a function without first declaring a named variable.

1
2
void draw(struct Point pt);
draw((struct Point){ .x = 3, .y = 4 });

volatile (Memory-Mapped I/O / Signals)

volatile tells the compiler a variable may change outside the program's normal flow — a hardware register, a signal handler, or another thread. It stops the compiler from caching or reordering accesses.

1
volatile uint32_t *reg = (uint32_t *)0xFFFF0000;

3.Structs, Unions & Aggregate Types

Struct layout, unions, bit-fields, sizeof, _Static_assert, and alignment — the building blocks of real-world C data.

struct — Heterogeneous Records

A struct groups related values of different types into one record. Members are laid out in declaration order with alignment padding inserted between them, so sizeof(struct) is often larger than the sum of member sizes.

1
2
3
4
struct Point { int x; int y; };
struct Point p = { 1, 2 }; // positional
struct Point q = { .y = 5 }; // designated (rest zero)
p.x = 10;

Pointer to struct — Access Members with ->

The arrow p->member is shorthand for (*p).member, dereferencing a struct pointer to access a member. Passing structs by pointer is cheap (one word) and lets the function modify the caller's record.

1
2
struct Point *pp = &p;
pp->x = 20; // same as (*pp).x

Anonymous struct + typedef

You can attach a typedef to a tagless struct and get a clean type name from a single declaration. It's the conventional way to name frequently used record types, sparing you the struct keyword everywhere.

1
2
3
struct { int a, b; } ab; // unnamed type, single var
typedef struct { int a, b; } Pair;
Pair pr = { 3, 4 };

union — Overlapping Storage (Size = Largest Member)

A union places all members in the same memory; its size equals the largest member. Only the most recently written member holds a valid value — reading others is undefined behavior, so keep a tag field alongside it.

1
2
3
4
5
6
7
union Value {
int i;
float f;
char bytes[4];
};
union Value v = { .f = 3.14f };
// v.i and v.f share the same memory (implementation-defined reading)

Bit-Fields — Packed Flags

Bit-fields pack small flags into the bits of an integer, saving memory for records filled with booleans. Layout is compiler-defined, so keep bit-fields within a single translation unit — don't cross ABI boundaries.

1
2
3
4
5
6
struct Flags {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int mode : 3;
};
struct Flags f = { 1, 0, 2 };

sizeof — Bytes Occupied by an Object

sizeof gives the number of bytes occupied by a type or object. For an array still in scope, sizeof a / sizeof a[0] yields the element count — but once the array decays to a pointer, the trick silently breaks (see faq).

1
2
3
4
5
size_t n = sizeof(int); // 4 on typical systems
size_t sz = sizeof p; // sizeof(struct Point)
int arr[10];
size_t len = sizeof arr / sizeof arr[0]; // 10 (array length)
// NOTE: sizeof(arr) inside a function that received it decays (see pointers)

_Static_assert (C11+) — Compile-Time Checks

_Static_assert(cond, msg) checks at compile time; if cond is false, the build fails. Use it to validate size and ABI assumptions the moment they're broken, rather than discovering them at runtime.

1
2
_Static_assert(sizeof(void*) == 8, "64-bit only");
_Static_assert(sizeof(int) >= 4, "int too small");

Alignment — _Alignas & _Alignof (C11+)

Alignment determines which addresses an object can occupy; the compiler pads structs so each member sits on its natural boundary. _Alignof reports the alignment value, _Alignas raises it when needed (SIMD, cache-line sharing).

1
2
3
4
5
#include <stdalign.h>
_Alignof(int) // alignment requirement, e.g. 4
struct S { char c; int i; }; // padded: sizeof(S) often 8, not 5
// Force packing (non-portable, avoid unless needed):
// #pragma pack(push, 1) ... #pragma pack(pop)

Flexible Array Member (C99+) — Trailing Array in a struct

A struct may end with an unsized array (data[]) whose length is decided at allocation time: a single malloc covers the struct plus the extra bytes. It's the C idiom for buffers whose size is known only at runtime.

1
2
3
4
5
6
struct Buffer {
size_t len;
char data[]; // last member only
};
struct Buffer *buf = malloc(sizeof(*buf) + 100);
buf->len = 100;

Enumerations as Distinct Types (C23)

C23 lets you pin an enum's underlying type (enum Status : unsigned char), so its size is no longer up to the compiler. Otherwise enums are compatible with int — which suffices for most code.

1
2
3
enum Status { ST_OK = 0, ST_ERR = 1 };
// C23 lets you pin the type:
// enum Status : unsigned char { ST_OK = 0, ST_ERR = 1 };

4.Pointers & Arrays

Address-of, dereference, pointer arithmetic, array decay, pointer vs array, const correctness.

Address-Of (&) and Dereference (*)

& produces a pointer to an object; * is the reverse — it reads or writes through the pointer. Together they let you access a value indirectly and modify it from another scope.

1
2
3
4
int x = 42;
int *p = &x; // p holds the address of x
printf("%d\n", *p); // 42 (*p reads x)
*p = 7; // writes through the pointer -> x == 7

NULL — The Null Pointer

NULL is the pointer that points to nothing and differs from every valid address. Dereferencing it is undefined behavior — usually a crash — so check for NULL before use.

1
2
3
4
int *p = NULL;
if (p) { /* non-null */ }
if (!p) { /* null */ }
// Dereferencing NULL is undefined behavior — always check.

Pointer Arithmetic (Scaled by sizeof(*p))

Adding an integer to a pointer moves it by elements, not bytes — p + 2 skips two ints. The subscript p[i] is just *(p + i). Going past the array's end is undefined behavior.

1
2
3
4
5
int arr[5] = { 10, 20, 30, 40, 50 };
int *p = arr; // == &arr[0]
*(p + 2) // arr[2] == 30
p[2] // same — [] is sugar for *(p + n)
*(p + 5) // UB — past the end

Array Decay (Expression Becomes Pointer)

In most expressions, an array name becomes a pointer to its first element. The upshot: inside a function you can't recover the array's length from the parameter alone — you must pass the length alongside the array.

1
2
3
4
5
void sum(int *a, int n);
int data[10];
sum(data, 10); // 'data' decays to int* — length must be passed
// sizeof(data) == 40 here (true array);
// inside sum(), sizeof(a) == 8 (pointer) — see faq

Pointer vs Array

A char array owns its bytes and is modifiable; a char* pointing at a string literal typically points to read-only memory. Writing through such a pointer is undefined behavior and often crashes.

1
2
3
4
// char s[] = "hi"; mutable array, 3 bytes (incl '\0')
// char *p = "hi"; pointer to string literal (read-only in practice)
s[0] = 'H'; // OK for array
// p[0] = 'H'; // UB (literal usually in read-only memory)

Pointers of Multiple Levels

A pointer to a pointer (int**) adds one more level of indirection. It shows up in arrays of strings, 2-D structures, and output parameters where the function allocates memory and the caller frees it.

1
2
3
4
int x = 1;
int *p = &x;
int **pp = &p; // pointer to pointer
**pp = 2; // writes x

Function Pointers

A function pointer holds a function's address and lets you call it indirectly. It underpins callbacks, dispatch tables, and the object-oriented patterns covered in the oop section.

1
2
3
4
5
6
7
int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add; // pointer to function
fp(1, 2); // call through it
// Cleaner with typedef:
typedef int (*BinOp)(int, int);
BinOp op = add;
op(2, 3);

Array of Pointers (e.g. argv)

An array of char* is the idiomatic container for a list of strings; argv is one. Each element points to its own NUL-terminated string, and the array is just a row of pointers.

1
2
3
const char *names[] = { "alice", "bob" };
// pointer-to-array (rare):
int (*row)[5]; // pointer to int[5]

const Correctness

Declaring a pointer parameter const — like const char* s — is a promise that the function won't modify what it points at. Callers can then pass string literals and arrays safely, and the compiler enforces the promise.

1
2
3
void f(const int *p); // promise not to modify *p
int arr[5] = {1,2,3,4,5};
f(arr); // int[] decays to int*, fits const int*

Pointer to Array vs Array of Pointers

int *a[5] is an array of 5 pointers; int (*b)[5] is a pointer to an array of 5 ints. Parentheses change everything. Read complex declarations right-to-left, or feed them to cdecl.

1
2
3
4
5
// int *a[5]; array of 5 pointers (to int)
// int (*b)[5]; pointer to an array of 5 ints
// int (*fn)(int, int); function pointer
// int *fn(int, int); function returning int*
// Use a parser / cdecl tool: https://cdecl.org

5.Control Flow

if/else, ternary, switch, for, while, do-while, break/continue, goto, and setjmp/longjmp.

if / else if / else

Branch on a condition: the first branch whose condition is true runs; if none match, else runs. else-if chains test alternatives in order — first match wins.

1
2
3
4
5
6
7
if (n > 0) {
puts("positive");
} else if (n == 0) {
puts("zero");
} else {
puts("negative");
}

Ternary (An Expression)

cond ? a : b is an expression, not a statement: it picks a or b by cond. Use it for compact value selection — if it doesn't fit on one line or hurts readability, switch to if/else.

1
2
const char *sign = n > 0 ? "pos" : "non-pos";
int max = (a > b) ? a : b;

switch — Falls Through Without break

switch dispatches on an integer value. Unless you break, execution falls through into the next case — either intentionally (stacked empty cases) or with each case broken. default handles unmatched values.

1
2
3
4
5
6
7
8
9
10
11
switch (status) {
case 200:
case 201:
puts("ok");
break;
case 404:
puts("missing");
break;
default:
puts("other");
}

for — Declare Counter in init (C99+)

for(init; cond; step) is the standard counting loop. Declaring the counter in init (C99+) confines it to the loop body, avoiding clashes with outer variables of the same name.

1
2
3
4
5
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
// Empty init / decrement: while-style
for (;;) { if (stop) break; }

while / do-while

while tests the condition before each iteration and may run zero times. do-while always runs its body once before testing — ideal for menus and prompts where you need to "ask at least once".

1
2
3
4
while (n < 10) { n++; }
do {
process(input);
} while (!done); // body runs at least once

break / continue

break exits the innermost loop or switch immediately; continue jumps to the next iteration. Used well, they flatten nested logic and remove flag variables; used badly, they hide control flow — so prefer them sparingly.

1
2
3
4
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip 3
if (i == 7) break; // stop at 7
}

goto — Escape Deep Nesting (Use Rarely and Carefully)

goto jumps to a labeled line. Its legitimate uses are escaping deep nesting and centralizing error cleanup — the goto-cleanup idiom. Beyond that, return or a flag is almost always clearer.

1
2
3
4
5
6
7
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (found(i, j)) goto done;
}
}
done:
printf("found at %d,%d\n", i, j);

setjmp / longjmp — Non-Local Jumps

setjmp saves an execution context; longjmp restores it from anywhere, unwinding back to the save point. All cleanup in between is skipped — no destructors run — so it's usually not worth it compared to goto or an explicit return.

1
2
3
4
5
6
7
8
#include <setjmp.h>
jmp_buf env;
if (setjmp(env) == 0) {
risky(); // may longjmp back here
} else {
puts("recovered");
}
// longjmp(env, 1) from anywhere restores the saved context

Loop Idioms

Worth knowing loop shapes: indexed loops over a count, pointer walks over an array, and infinite loops broken by break. Pick the one that most directly expresses the intent.

1
2
3
for (size_t i = 0; i < n; i++) // index
for (const int *p = arr; p < arr + n; p++) // pointer walk
for (;;) { if (eof) break; } // forever + break

Logical Short-Circuit Evaluation

&& and || evaluate left-to-right and stop as soon as the result is determined. That's why ptr && ptr->x is safe: the second operand runs only if the first is true, so you can guard a dereference inline.

1
2
if (ptr && ptr->val > 0) // safe: short-circuits on NULL
if (a || b) // b not evaluated if a is true

6.Functions & Variadic Arguments

Pass-by-value vs pass-by-pointer, function pointers and typedef, variadic arguments, recursion, and _Generic.

Pass-by-Value — Caller's Variable Stays Intact

All C arguments are passed by value — the callee gets a copy, and modifying it doesn't touch the caller's. Use this for plain scalars and small structs; switch to pointer form when you need to mutate or to spare large objects.

1
int add(int a, int b) { return a + b; }

Pass-by-Pointer (Modify the Caller)

To modify a caller's variable, pass its address. The callee dereferences the pointer and writes through it, and the change is visible to the caller — that's how scanf and friends produce their output.

1
2
3
4
void swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
swap(&x, &y);

const Parameter — A Read-Only Contract

A const parameter like const char* s is a read-only contract: the function promises not to modify what it points at. Callers can safely pass string literals, and the API's intent is obvious.

1
2
size_t len(const char *s) { return strlen(s); }
// Also lets callers pass string literals.

Array Parameter Decays (Pass the Length)

An array parameter decays to a pointer and the length doesn't ride along. Always pass the length explicitly — otherwise the function has no way to know where the data ends.

1
2
3
4
5
int sum(const int *xs, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) total += xs[i];
return total;
}

Function Pointer + typedef

typedef a function-pointer signature once, then declare variables and parameters with the alias. Dispatch tables and callbacks stop being a wall of parentheses and asterisks.

1
2
3
4
5
6
7
8
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
typedef int (*BinOp)(int, int);
int apply(BinOp op, int a, int b) { return op(a, b); }
int r = apply(add, 3, 4);
// Table of function pointers (mini dispatch):
BinOp ops[] = { add, mul };
int s = ops[1](3, 4); // 12

Variadic — printf-Style Functions

Functions with ... in the signature take a variable number of arguments, like printf. Use va_start/va_arg/va_end to read them; a fixed first parameter must declare how many follow, or the call can't be safely decoded.

1
2
3
4
5
6
7
8
9
10
11
#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
int total = 0;
for (int i = 0; i < count; i++) total += va_arg(ap, int);
va_end(ap); // must pair with va_start
return total;
}
int n = sum(4, 10, 20, 30, 40); // 100

Variadic Forwarding — the v* Family

To forward variadic arguments to another variadic function (say a logging wrapper to vfprintf), use the v* family and pass the va_list itself. Re-reading the list with va_arg afterwards will corrupt it.

1
2
3
4
5
6
void logf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}

Recursion

A recursive function calls itself on a smaller instance of the same problem until it hits the base case. Each call takes stack space, so prefer iteration when depth is large or unbounded.

1
2
3
4
5
6
7
int fact(int n) { return n < 2 ? 1 : n * fact(n - 1); }
// Iterative is usually safer (no stack overflow):
long fib_iter(int n) {
long a = 0, b = 1;
for (int i = 0; i < n; i++) { long t = a; a = b; b = t + b; }
return a;
}

_Generic (C11+) — Compile-Time Type Dispatch

_Generic (C11) picks a result expression at compile time based on the controlling expression's type — a lightweight way to fake function overloading and write type-safe macros.

1
2
3
4
5
6
#define abs_val(x) _Generic((x), \
int: abs_i, \
long: abs_l, \
double: abs_d, \
default: abs_x)(x)
int abs_i(int x); long abs_l(long x); double abs_d(double x);

noreturn (C11+) and inline

noreturn marks a function as never returning (e.g. fatal-error routines), letting the compiler infer unreachable code. inline is a hint, not a guarantee — the compiler may still emit a real call.

1
2
3
4
5
6
#include <stdnoreturn.h>
noreturn void die(const char *msg) {
fprintf(stderr, "%s\n", msg);
exit(EXIT_FAILURE);
}
inline int square(int x) { return x * x; } // hint to inline

7.Strings & Numeric Conversions

NUL-terminated strings, safe writes, strtol/strtok/sscanf conversions, and a printf format specifier table.

The Basics — A NUL-Terminated Character Array

A C string is a character array terminated by a NUL byte ('\0'). strlen counts up to the terminator; any buffer holding a string must reserve room for it, or writes will overflow.

1
2
3
4
#include <string.h>
#include <stdio.h>
char s[] = "hello"; // 6 bytes incl. '\0'
size_t n = strlen(s); // 5 (excludes '\0')

Copy / Compare / Concatenate (Unsafe vs Safe)

strcpy/strcat don't check bounds — if the source is too long, they silently overflow. Prefer snprintf for bounded, always-NUL-terminated writes; strcmp/strncmp for comparisons; strncat copies at most n characters plus a terminator.

1
2
3
4
5
6
7
strcpy(dst, src); // UNSAFE if src too big
strncpy(buf, src, sizeof buf - 1);
buf[sizeof buf - 1] = '\0'; // strncpy may not NUL-terminate
strcmp(a, b) // <0, 0, >0
strncmp(a, b, 4) // compare first 4 chars
strcat(buf, src); // appends (UNSAFE)
snprintf(buf, sizeof buf, "%s%s", a, b); // SAFE build

Searching

strchr finds the first occurrence of a character, strrchr the last, strstr the first occurrence of a substring. All return a pointer into the string (or NULL if not found).

1
2
3
char *p = strchr(buf, 'w'); // first 'w'
char *q = strstr(buf, "lo wo"); // substring
char *r = strrchr(buf, 'o'); // last 'o'

mem* — Raw Bytes (Handles Any Memory, Including NUL)

The mem* family (memcpy, memmove, memset, memcmp) operates on raw bytes and handles embedded NULs. memcpy and memmove differ only in overlapping regions — use memmove when overlap is possible.

1
2
3
4
memcpy(dst, src, n); // no overlap allowed
memmove(dst, src, n); // safe if overlapping
memset(arr, 0, sizeof arr);
int eq = memcmp(a, b, n);

Numeric Conversion: strtol/strtod

Use strtol/strtod to parse numbers: they report errors and tell you where parsing stopped. atoi silently returns 0 on garbage, masking invalid input — don't use it in correctness-seeking code.

1
2
3
4
5
6
7
#include <stdlib.h>
char *end;
errno = 0;
long v = strtol("42abc", &end, 10); // base 10 -> 42, end -> "abc"
long h = strtoll("0x1F", NULL, 16); // hex -> 31
double d = strtod("3.14e2", NULL); // -> 314.0
// atoi has no error detection: atoi("abc") == 0 silently

Robust Parsing — Validate the Entire String

A complete parse checks three things: errno not set after strtol, at least one digit consumed (end != s), and the entire string consumed (*end == '\0'). Skip any check and trailing garbage slips through.

1
2
3
4
5
6
7
8
9
int parse_int(const char *s, long *out) {
if (!s || !out) return -1;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
if (errno || end == s || *end != '\0') return -1;
*out = v;
return 0;
}

Tokenizing: strtok_r (Reentrant)

strtok mutates the string in place, writing '\0' at each delimiter; the _r-suffixed version is reentrant (thread-safe). If you still need the original string, copy it first.

1
2
3
4
5
char line[] = "a,b,c";
char *save = NULL;
for (char *tok = strtok_r(line, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
puts(tok);
}

sscanf — Parse With a Format String (Returns Match Count)

sscanf parses a string with a format string and returns the number of fields successfully matched. Check the return value: if it's lower than expected, the input isn't shaped the way you thought.

1
2
3
int year, month;
if (sscanf("2026-08", "%d-%d", &year, &month) == 2) { /* ok */ }
// %d %f %s %c %zu %x %n (%% = literal %)

printf Format Table

The printf family shares one format language: %d int, %ld long, %zu size_t, %f double, %s string, %p pointer, plus width and precision modifiers. A specifier must exactly match the argument's type — mismatch is undefined behavior.

1
2
3
4
5
6
7
8
// %d int %ld long %lld long long
// %u unsigned int %zu size_t %zd ssize_t
// %f double %lf double %e scientific
// %.2f 2 decimals %x / %X hex %o octal
// %c char %s string %p pointer
// %5d pad to 5 width %-5d left-justify %05d zero-pad
printf("%s=%d\n", "count", 42);
printf("%.2f %%\n", 3.14159); // 3.14 % (%% = literal %)

Building Strings Safely

snprintf writes at most n bytes and is always NUL-terminated, so it's the safe way to assemble strings. For growing text, track the length and realloc the buffer, or use a small dynamic-buffer helper.

1
2
3
4
char buf[64];
snprintf(buf, sizeof buf, "user-%d", id);
// Grow-able: use strcat carefully or implement a dynamic buffer.
// For heavy string work consider OpenSSL BIO or a small dyn-str helper.

8.Arrays, Sorting & Searching

Array iteration, qsort/bsearch with comparators, 2-D arrays, and arrays of structs.

Fixed-Size Array — Iterate With Length

A C array is a fixed-size, contiguous run of elements. Iterate with a length: sizeof a / sizeof *a works while the real array is still in scope, but the size vanishes as soon as you pass it to a function.

1
2
3
4
int arr[5] = { 3, 1, 4, 1, 5 };
for (size_t i = 0; i < sizeof arr / sizeof *arr; i++) {
printf("%d ", arr[i]);
}

Initialization Patterns

Initialize arrays positionally, by designated index ([2] = 9), or zero out the whole thing with {0}. A 2-D array is an array of arrays, stored in row-major order: m[i][j] is contiguous within its row.

1
2
3
int zeros[10] = { 0 }; // all zero
int spec[5] = { [0] = 9, [4] = 1 }; // designated
int grid[2][3] = { {1,2,3}, {4,5,6} }; // 2-D

qsort — Sort With a Comparator

qsort sorts in place using a comparator that returns <0, 0, or >0. The comparator receives pointers to elements, so cast the void* arguments back to the element type before comparing (and avoid overflow).

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
#include <stdio.h>
int cmp_int(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y); // safe for big/small ints
}
int nums[] = { 5, 2, 9, 1 };
qsort(nums, 4, sizeof(int), cmp_int); // 1 2 5 9

bsearch — Binary Search a Sorted Array

bsearch does a binary search — but only on a sorted array. It returns a pointer to a matching element, or NULL when none exists. On large arrays, sort then search beats linear scan.

1
2
3
int key = 5;
int *hit = bsearch(&key, nums, 4, sizeof(int), cmp_int);
if (hit) printf("found %d\n", *hit);

String Comparator (Array of char*)

Sorting an array of char* must compare the strings, not the pointer values. The comparator casts to char* const*, dereferences to get each string pointer, then calls strcmp.

1
2
3
4
5
6
7
int cmp_str(const void *a, const void *b) {
const char *x = *(const char *const *)a;
const char *y = *(const char *const *)b;
return strcmp(x, y);
}
const char *names[] = { "bob", "alice" };
qsort(names, 2, sizeof(char*), cmp_str);

Array of Structs — Sort by a Field

To sort an array of structs by one field, write a comparator that takes two struct pointers and compares that field (e.g. a 3-way compare of a->key with b->key). qsort then rearranges the whole records.

1
2
3
4
5
6
7
struct Item { int key; const char *name; };
int cmp_item(const void *a, const void *b) {
const struct Item *x = a, *y = b;
return (x->key > y->key) - (x->key < y->key);
}
struct Item items[] = { {2,"b"}, {1,"a"}, {3,"c"} };
qsort(items, 3, sizeof(struct Item), cmp_item);

Pointer-to-Array Parameter (Keeps 2-D Shape)

When passing a 2-D array to a function, the parameter type must include the column count — int m[][COLS] — so the compiler can compute each row's start. The first dimension (row count) may vary freely.

1
void print_grid(int rows, int cols, int m[][cols]);

Dynamic Array Pattern (see also mem section)

A growable collection is a malloc'd block, realloc'd when full: capacity and length are tracked separately, growth doubles the size, free when done. This is the C counterpart of higher-language vectors and lists.

1
2
3
4
5
6
#include <stdlib.h>
int *dyn = malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++) dyn[i] = i;
// grow:
dyn = realloc(dyn, 20 * sizeof(int));
free(dyn);

Variable-Length Arrays (C99, Optional)

A VLA is allocated on the stack using a runtime value (int vla[n]). It's optional in C11, and a large n can blow the stack — prefer malloc for large or portable needs.

1
// int n = 8; int vla[n]; // stack array sized at runtime

2-D Arrays — Row-Major Order

int m[ROWS][COLS] is a contiguous row-major block; m[i][j] is *(*(m + i) + j). Subscripting needs to know the row width to compute each row's start, so the shape must be fixed at compile time.

1
2
3
4
5
6
7
#define ROWS 2
#define COLS 3
int m[ROWS][COLS] = { {1,2,3}, {4,5,6} };
for (int i = 0; i < ROWS; i++)
for (int j = 0; j < COLS; j++)
printf("%d ", m[i][j]);
// Contiguous: m[i][j] == *(*(m + i) + j)

9.Dynamic Memory & Ownership

malloc/calloc/realloc/free, stack vs heap, alignment, ownership rules, and the goto-cleanup pattern.

Allocate & Free

malloc returns uninitialized heap memory, and every allocation must eventually be freed. Always check for NULL before use; set the pointer to NULL after free, so a later double-free becomes a safe no-op.

1
2
3
4
5
6
#include <stdlib.h>
#include <string.h>
int *p = malloc(10 * sizeof(int)); // uninitialized
if (!p) { perror("malloc"); exit(EXIT_FAILURE); } // ALWAYS check
free(p); // after free, p is dangling
p = NULL; // avoid use-after-free

calloc: count×size, Zero-Initialized

calloc(count, size) zero-initializes the memory, multiplies the two arguments for you, and detects overflow. Reach for it when you need initialized storage and know the element count.

1
int *z = calloc(10, sizeof(int)); // all zeros

realloc — Resize; May Move the Block

realloc resizes an allocation and may move the block to a new address. Capture the return value in a temporary and check: on failure the old block is still valid, but if you overwrite the pointer first you leak it.

1
2
3
4
5
int *q = malloc(4 * sizeof(int));
int *tmp = realloc(q, 8 * sizeof(int));
if (tmp) { q = tmp; } // on failure old block still valid
else { /* keep q, handle error */ }
free(q);

Stack vs Heap

The stack is auto-managed, fast, reclaimed on return — but small and short-lived. The heap is manual, large, and survives the function. Put big, long-lived buffers on the heap.

1
2
3
// Stack: fast, auto-freed, small (MBs), fixed size at compile/runtime
// Heap: large, manual free, survives return, slower
// int local[1'000'000]; may overflow the stack -> use malloc

Ownership — A Small Set of Rules

A minimal set of ownership discipline keeps memory problems under control: whoever allocates, frees; free each pointer once; set it to NULL after free; keep the old pointer when realloc fails. Convention is C's only resource manager.

1
2
3
4
// 1) Whoever allocates frees (or documents otherwise)
// 2) Free once, never free a non-heap pointer
// 3) After free set pointer to NULL to catch double-free
// 4) Realloc failure: keep the old pointer

Strings on the Heap

A heap string needs strlen(s) + 1 bytes — the extra one is the NUL terminator. malloc(strlen(s) + 1), then copy; forget the +1 and you have a one-byte overflow waiting to blow up.

1
2
3
char *name = malloc(strlen("hello") + 1);
strcpy(name, "hello");
free(name);

struct With an Internal Pointer

A struct that owns heap data has two allocations to free: the inner buffer first, then the struct itself. Decide and document who owns the inner pointer, and free in reverse order of allocation.

1
2
3
4
5
6
7
struct Line { char *data; size_t len; };
struct Line *line = malloc(sizeof *line);
line->data = malloc(100);
line->len = 0;
// free order: innermost first
free(line->data);
free(line);

goto Cleanup (Single Exit Point)

C has no destructors, so goto-cleanup is C's RAII: one exit point per function, one cleanup block that every path falls through to. Each resource is released exactly once on every path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int process(const char *path) {
FILE *f = NULL;
char *buf = NULL;
int rc = -1;
f = fopen(path, "r");
if (!f) { perror("fopen"); goto cleanup; }
buf = malloc(4096);
if (!buf) { perror("malloc"); goto cleanup; }
if (fread(buf, 1, 4096, f) == 0) goto cleanup;
// ... work ...
rc = 0;
cleanup:
free(buf);
if (f) fclose(f);
return rc; // single return
}

Alignment / Over-Alignment (C11)

aligned_alloc allocates with a specified alignment (SIMD, cache-line sharing); the size must be a multiple of the alignment. Plain malloc guarantees only the platform default alignment.

1
2
3
#include <stdlib.h>
// aligned_alloc(size, alignment) — size must be a multiple of alignment
// double *a = aligned_alloc(alignof(double) * 4, alignof(double) * 4);

Memory Leak / Overflow Tooling (see build section)

Leaks, overflows, and use-after-frees hide until the process dies — use valgrind or AddressSanitizer to find them mechanically. Both are covered in the build section and are the cheapest safety nets you can add.

1
2
3
// valgrind ./prog
// gcc -fsanitize=address,undefined -g prog.c -o prog
// AddressSanitizer catches leaks, OOB, use-after-free at runtime

10.Object-Oriented Style Patterns

C is procedural, but structs + function pointers can emulate classes, inheritance, and polymorphism.

"Class" = struct + vtable

C's emulation of a class: a struct for the data, a table of function pointers (vtable) for the behavior. Methods are just plain functions taking an explicit self/this pointer.

1
2
3
4
5
6
7
8
9
struct Animal {
const char *name;
void (*speak)(const struct Animal *self);
};
static void dog_speak(const struct Animal *a) {
printf("%s: woof\n", a->name);
}
struct Animal rex = { "Rex", dog_speak };
rex.speak(&rex);

Inheritance via Embedding

Embed the base struct as the first member and you get inheritance: a derived struct shares its address with its base, so a derived value can stand in anywhere a base is expected, and base methods work unchanged.

1
2
3
struct Dog { struct Animal base; int breed; };
struct Dog d = { { "Rex", dog_speak }, 1 };
d.base.speak(&d.base);

Constructor / Destructor Pairs

Pair an init function that allocates and initializes with a free function that releases and reclaims — C's version of constructors and destructors. Every created object must be destroyed exactly once.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Vec {
int *data;
size_t len, cap;
};
struct Vec *vec_new(void) {
struct Vec *v = malloc(sizeof *v);
if (v) { v->data = NULL; v->len = v->cap = 0; }
return v;
}
void vec_free(struct Vec *v) {
free(v->data);
free(v);
}

Opaque Pointer — Hide the Implementation (PIMPL)

The header only forward-declares the struct; the definition lives in the .c file. Callers can hold the pointer but can't see inside — that's PIMPL: hide the implementation so internal changes don't break them.

1
2
3
4
5
6
7
8
9
// foo.h:
struct Foo; // forward declaration
typedef struct Foo Foo;
Foo *foo_new(void);
void foo_destroy(Foo *f);
int foo_get(const Foo *f);
// foo.c:
struct Foo { int value; }; // definition hidden
Foo *foo_new(void) { return calloc(1, sizeof(struct Foo)); }

Interface = a struct of Function Pointers

A struct stuffed with function pointers is an interface: any implementation that fills it can plug in, and each function takes a void* self for instance context. That's C's flavor of polymorphism.

1
2
3
4
5
struct Stream {
int (*read)(void *self, char *buf, int n);
int (*write)(void *self, const char *buf, int n);
void *self; // context
};

Type-Erased Data (void *)

void* erases the type and lets one container hold heterogeneous values. To safely recover the type, you usually add a tag field recording what the pointer really points at.

1
2
typedef struct { void *data; } Box;
// + a tag for runtime type checks if needed

Method Chaining by Returning *this

Each setter returns the object itself (*this), so calls chain: builder->setA(x)->setB(y). It makes builder-style configuration compact, at the cost of return values that callers can ignore.

1
2
3
4
struct Builder *set_name(struct Builder *b, const char *n) {
b->name = n; return b;
}
set_name(set_name(new_builder(), "app"), "x");

Singleton (Lazy, Not Thread-Safe)

A lazy singleton allocates its instance into a static pointer on first use. This naive version isn't thread-safe — two threads can race on the first allocation — so add locking once it becomes shared.

1
2
3
4
5
static Config *cfg = NULL;
Config *get_config(void) {
if (!cfg) cfg = config_new();
return cfg;
}

Reference Counting (Simple Version)

retain/release give an object a reference count, freeing it when it hits zero. It's the foundation of Objective-C and for C objects it's a lighter ownership model than garbage collection.

1
2
void retain(Object *o) { o->refs++; }
void release(Object *o) { if (--o->refs == 0) free(o); }

Full OOP Is Possible (glib)

Full-blown OOP frameworks exist in C — glib and GObject prove it — but they bring real complexity. A minimal vtable plus an opaque pointer covers most needs; reach for the heavy artillery only when the project truly requires it.

1
2
// Prefer a minimal vtable + opaque pointer.
// For callbacks: use void* context args to avoid globals.

11.Error Handling

No exceptions: errno, return codes + out parameters, goto-cleanup, error enums, and assertions.

errno After a Failing Call

Library and system calls set the global errno on failure; you must read or copy it immediately after the call — any later call can overwrite it. strerror(errno) turns the code into a human-readable message.

1
2
3
4
5
6
7
8
9
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
if (mkdir("foo", 0755) != 0) {
fprintf(stderr, "mkdir: %s\n", strerror(errno));
return 1;
}
// Always read errno immediately after the call (or copy it).

Return Code + Out Parameter (A Common C Idiom)

C's mainstream error idiom: return 0 on success, a negative error code on failure, and write the result through an out pointer. On failure the result pointer stays NULL/unchanged so the caller can check.

1
2
3
4
5
6
7
8
9
10
11
12
int parse_int(const char *s, long *out) {
if (!s || !out) return -1;
errno = 0;
char *end = NULL;
long v = strtol(s, &end, 10);
if (errno || end == s || *end != '\0') return -1;
*out = v;
return 0; // 0 == success
}
long n;
if (parse_int("42", &n) == 0) printf("got %ld\n", n);
else fprintf(stderr, "bad input\n");

Error Enum — Explicit Error Categories

Enumerate the kinds of errors your function can produce — OK, INVALID_INPUT, NOMEM, IO — and return them. Named codes document themselves, work nicely in switch, and force you to think through every failure mode.

1
2
3
4
5
enum Error { ERR_OK = 0, ERR_INVALID, ERR_NOMEM, ERR_IO };
enum Error do_thing(int x) {
if (x < 0) return ERR_INVALID;
return ERR_OK;
}

assert — For Debugging (Removed with NDEBUG)

assert(cond) aborts when cond is false, catching bugs during development. It's compiled out under -DNDEBUG, so never use it to guard behavior the user depends on — only invariants you assume true.

1
2
3
#include <assert.h>
assert(ptr != NULL);
assert(index < size && "index out of range");

perror — Print errno Description

perror prints your message plus the current errno's description on one line of stderr. It's the fastest way to report why a system call failed — no formatting of errno required.

1
2
FILE *f = fopen(path, "r");
if (!f) { perror(path); return 1; } // 'path: No such file'

_Static_assert — Compile-Time

_Static_assert checks assumptions at compile time, not at runtime. Use it to validate sizes, offsets, and ABI constraints so they don't fail silently inside a production binary.

1
_Static_assert(sizeof(int) == 4, "expected 4-byte int");

Thread-Safe strerror_r (POSIX)

strerror is not thread-safe — it overwrites its own internal buffer. strerror_r (POSIX) writes into a caller-provided buffer, so concurrent threads each get their own message.

1
2
3
char ebuf[256];
strerror_r(errno, ebuf, sizeof ebuf);
fprintf(stderr, "err: %s\n", ebuf);

Check System-Call Return Values

System calls fail — files, sockets, memory all do. Ignoring the return value turns a small failure into a later crash or data corruption; even successful fread/fwrite may return short counts, so always check.

1
2
3
4
5
int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
// Check fread/fwrite: count may be short on errors
size_t got = fread(buf, 1, sizeof buf, f);
if (got < sizeof buf && ferror(f)) { /* real error */ }

Distinguishing Error from EOF with ferror/feof

When a read loop ends, ferror() and feof() tell you why — a real I/O error or end-of-file. EOF is a normal state, not an error — distinguish them with ferror.

1
2
if (ferror(f)) puts("read error");
else if (feof(f)) puts("end of file");

12.File I/O

stdio streams, line-by-line and whole-file reading, binary I/O, POSIX file descriptors, mmap, and directory traversal.

Open / Close

fopen returns a FILE* stream, or NULL on failure; fclose flushes and frees it. Modes are r/w/a plus + for read+write and b for binary. Never keep using the pointer after a failed open.

1
2
3
4
5
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) { perror("fopen"); return 1; }
// modes: "r" "w" "a" "r+" "w+" "a+" (+"b" for binary: "rb" "wb")
fclose(f);

Read the Whole File (Small Files)

For small files: seek to end, ftell for the size, rewind, malloc size+1, then fread it all in. Confirm the bytes read match the request — the file may have changed between calls.

1
2
3
4
5
6
7
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
char *buf = malloc(sz + 1);
if (buf && fread(buf, 1, sz, f) == (size_t)sz) {
buf[sz] = '\0';
}

Read Line by Line (Idiomatic)

fgets safely reads a line into a fixed buffer, stopping at a newline or the buffer limit. Use strcspn to strip the trailing newline — it'll otherwise stay in the buffer. This is the idiomatic way to process text line by line.

1
2
3
4
5
char line[256];
while (fgets(line, sizeof line, f)) {
line[strcspn(line, "\r\n")] = '\0'; // strip trailing newline
process(line);
}

Writing

fprintf/fputs/fputc write formatted text, strings, and single characters to a stream. Errors can surface at write time or during fclose, so check fclose's result to avoid missing a failed flush.

1
2
3
fprintf(f, "%s=%d\n", "key", 42);
fputs("line\n", f);
fputc('x', f);

Format to a String First (Safe)

Format the whole output into a buffer with snprintf first, then write it once. This avoids interleaved writes from concurrent sources, lets you validate before writing, and keeps the write atomic.

1
2
3
char out[64];
snprintf(out, sizeof out, "user-%d", id);
fputs(out, f);

Binary I/O — Read & Write Raw Bytes

fread/fwrite move raw bytes and suit fixed-size records: read exactly sizeof(record) and handle the return. A short read is either an error or EOF — use ferror/feof to tell which.

1
2
3
4
uint32_t header[4];
size_t n = fread(header, sizeof header[0], 4, f);
if (n != 4) { /* short read: ferror(f) or feof(f) */ }
fwrite(header, sizeof header[0], 4, out);

POSIX File Descriptors

Beneath stdio, Linux/macOS expose open/read/write/close on int file descriptors: finer control, no buffering, more pitfalls. Cross over with fdopen (fd to FILE*) or fileno (FILE* to fd).

1
2
3
4
5
6
7
#include <unistd.h>
#include <fcntl.h>
int fd = open("data.bin", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
char b[4096];
ssize_t got = read(fd, b, sizeof b); // may be < sizeof b
close(fd);

mmap — Map a File Into Memory

mmap maps a file directly into your address space, so reads and writes become plain memory accesses — fast for large files. munmap releases it; the file can't be empty, and the mapped size is page-aligned.

1
2
3
4
5
6
7
8
9
#include <sys/mman.h>
#include <sys/stat.h>
int fd = open("big.bin", O_RDONLY);
struct stat st; fstat(fd, &st);
const void *data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
// use data[0..st.st_size-1] ...
munmap((void *)data, st.st_size);
close(fd);

Directory Traversal

opendir/readdir/closedir list directory entries one by one. Skip the implicit '.' and '..' entries, then use stat to check each name's file type and size.

1
2
3
4
5
6
7
#include <dirent.h>
DIR *d = opendir(".");
struct dirent *e;
while ((e = readdir(d))) {
if (e->d_name[0] != '.') printf("%s\n", e->d_name);
}
closedir(d);

stat — File Metadata

stat/lstat/fstat return file metadata — size, modification time, permissions, type. tmpfile() gives you an unnamed temp file that's auto-deleted on close, ideal for scratch storage.

1
2
3
4
5
6
7
8
9
10
#include <sys/stat.h>
struct stat st2;
if (stat("data.txt", &st2) == 0) {
printf("size=%ld mtime=%ld\n", (long)st2.st_size, (long)st2.st_mtime);
}
// tmpfile(): unnamed temp file, auto-deleted on close
FILE *t = tmpfile();
fprintf(t, "scratch\n");
rewind(t);
fclose(t);

13.Common Pitfalls

Ten classic C pitfalls — correct versions are tagged GOOD, incorrect ones BAD.

Buffer Overflow

Writing past the end of a fixed buffer corrupts adjacent memory and is a top-tier security bug. Bound every write: strncpy plus a manual NUL, snprintf, or fgets with a length. Sanitizers catch issues you'd miss by eye.

1
2
3
4
5
char buf[10];
strcpy(buf, src); // BAD — writes past buf if src is long
strncpy(buf, src, sizeof buf - 1);
buf[sizeof buf - 1] = '\0'; // GOOD — bounded, NUL-terminated
snprintf(buf, sizeof buf, "%s", src); // GOOD — simplest

Use-After-Free / Double-Free

Use-after-free is undefined behavior and often exploitable; freeing the same memory twice corrupts the allocator. Set the pointer to NULL right after free — free(NULL) is a safe no-op.

1
2
3
4
5
int *p = malloc(sizeof(int));
free(p);
*p = 42; // BAD — dangling pointer (UB)
p = NULL; // GOOD — marks it freed
free(p); // safe (free(NULL) is a no-op)

Memory Leak

Every allocation must be freed on every path; leaks silently grow until the process dies. AddressSanitizer and valgrind report them by default — run them during development, not after the crash.

1
2
3
int *p = malloc(4 * sizeof(int));
return; // BAD — leaks
free(p); // GOOD — before every return

= vs ==

A single = is assignment; == is comparison. if (x = 0) assigns 0 and is always false — a silent, hard-to-spot bug. Compile with -Wall, and parenthesize assignments inside conditions.

1
2
3
if (x = 0) { /* ... */ } // BAD — assigns, condition is false
if (x == 0) { /* ... */ } // GOOD
if ((x = foo()) != 0) { /* ... */ } // GOOD — assign in condition with parens

Signed Overflow Is UB

Signed integer overflow is undefined behavior: compilers may assume it never happens and optimize accordingly. Widen to long long before summing large signed values, or use the unsigned wraparound you meant.

1
2
3
int a = 2000000000, b = 2000000000;
int c = a + b; // BAD — signed overflow = UB
long long c2 = (long long)a + b; // GOOD — widen first

Uninitialized Variable

Reading an uninitialized local is undefined behavior, usually yielding garbage. Initialize every variable at its declaration — that's what gives the compiler's -Wuninitialized something real to flag.

1
2
3
int x;
printf("%d\n", x); // BAD — indeterminate value
int x = 0; // GOOD — always initialize

Array Decay in sizeof

An array parameter inside a function is a pointer, and sizeof gives 8 (pointer size) instead of the array size. sizeof a / sizeof a[0] works only while the real array is still in scope — pass the length explicitly.

1
2
3
4
5
void f(int arr[10]) {
sizeof(arr); // BAD — size of pointer (8), not 40
}
// GOOD — pass the length explicitly:
void g(const int *arr, size_t n);

Off-by-One Error

Loop bounds are a classic off-by-one: n elements means iterate i < n (indices 0..n-1), not i <= n which steps one past the end. The last legal index of an n-sized array is n-1.

1
2
for (int i = 0; i <= n; i++) // BAD — runs n+1 times
for (int i = 0; i < n; i++) // GOOD

printf Format Mismatch

Mismatched format specifier and argument type is undefined behavior — %s paired with an int can crash outright. Use %zu for size_t, match %d / %ld / %lld to the type, and turn on -Wformat to catch mismatches.

1
2
3
printf("%s\n", x); // BAD — %s expects char*, x is int
printf("%d\n", x); // GOOD
printf("%zu\n", sizeof x); // GOOD — size_t needs %zu

Signed / Unsigned Comparison

When signed meets unsigned, the signed operand is promoted to unsigned and negative numbers become huge positives. Cast both sides to the same signed type explicitly, or just don't mix them.

1
2
3
4
5
int i = -1;
unsigned u = 0;
if (i < u) { /* ... */ } // BAD — i promoted to unsigned, huge
if ((long long)i < (long long)u) { /* */ } // GOOD — compare as signed
// Also: (int)1 < (unsigned)-1 is FALSE — surprising, use explicit casts

14.Threads & Concurrency

pthread create/join, mutexes, condition variables, atomics, and C11 threads — compile and link with -pthread.

Build & Headers

Threaded programs compile with -pthread, which links the pthread library. Include <pthread.h> and treat every pthread_* return value as an error to check — these APIs don't set errno.

1
2
3
// Build: $ gcc -pthread prog.c -o prog
#include <pthread.h>
#include <stdio.h>

Create & Join

pthread_create starts a thread running a function that receives a void* argument; pthread_join blocks until it returns. The argument you pass in and the void* you get back are the channel for cross-thread data.

1
2
3
4
5
6
7
8
9
void *worker(void *arg) {
int id = *(int *)arg;
printf("thread %d\n", id);
return NULL;
}
pthread_t t;
int id = 1;
pthread_create(&t, NULL, worker, &id);
pthread_join(t, NULL); // wait for t

Pass Data Out via void*

A thread returns its result as void* — typically a pointer to a malloc'd value. The caller joins, casts the returned pointer back, reads the value, and frees it.

1
2
3
4
5
6
7
8
9
10
11
void *compute(void *arg) {
int *out = malloc(sizeof(int));
*out = 42;
return out;
}
pthread_t t2;
pthread_create(&t2, NULL, compute, NULL);
void *res;
pthread_join(t2, &res);
int value = *(int *)res; // 42
free(res);

Mutex — Guard Shared State

Mutexes serialize access to shared state: lock before, unlock after. Keep critical sections short — locks are bottlenecks, and holding them invites deadlock.

1
2
3
4
5
6
7
8
9
10
11
12
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
long shared = 0;
void *inc(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mtx);
shared++;
pthread_mutex_unlock(&mtx);
}
return NULL;
}
// init at runtime with attributes:
// pthread_mutex_init(&mtx, NULL);

Condition Variable — Wait / Signal

Condition variables let a thread sleep until another signals that some predicate has changed. Always wait in a while loop (spurious wakeups), hold the mutex while waiting, and re-check the predicate on wakeup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pthread_mutex_t cv_m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
int ready = 0;
// Producer:
void *produce(void *a) {
pthread_mutex_lock(&cv_m);
ready = 1;
pthread_cond_signal(&cv); // wake one waiter
pthread_mutex_unlock(&cv_m);
return NULL;
}
// Consumer:
void *consume(void *a) {
pthread_mutex_lock(&cv_m);
while (!ready) // loop — spurious wakeups
pthread_cond_wait(&cv, &cv_m);
pthread_mutex_unlock(&cv_m);
return NULL;
}

Detached Thread — No Join Needed

A detached thread cleans up its own resources on exit and never needs joining. Only detach when you're sure you'll never wait on it — and never hand a detached thread a pointer into the caller's stack; it may already be gone.

1
2
3
4
5
6
pthread_t t3;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&t3, &attr, worker, NULL);
pthread_attr_destroy(&attr);

C11 Threads (Portable)

C11's <threads.h> (thrd_t) is a thinner, portable alternative to pthreads — less capable, often just a thin wrapper over the OS API. On POSIX, pthreads remain the standard choice for serious work.

1
2
3
4
5
// #include <threads.h>
// int run(void *a) { return 0; }
// thrd_t th;
// thrd_create(&th, run, NULL);
// thrd_join(th, NULL);

Atomics (C11) — <stdatomic.h>

atomic_int and atomic_fetch_add provide lock-free loads, stores, and increments with well-defined memory order — faster than a mutex for simple shared counters, and more correct than a plain int (no races).

1
2
3
4
5
#include <stdatomic.h>
atomic_int counter = 0;
atomic_fetch_add(&counter, 1);
int now = atomic_load(&counter);
// Lock-free check: atomic_is_lock_free(&counter)

Thread-Local Storage (C11)

_Thread_local (C11, GCC's __thread) gives each thread its own copy of a variable. It's great for per-thread caches and for error slots that must not clobber each other.

1
2
3
#include <threads.h>
_Thread_local int tls_count = 0; // each thread its own copy
// GCC/Clang also: __thread

Error Handling in Threads

pthread functions return errno-style codes instead of setting errno. Capture the return value and format the diagnosis with strerror(rc) — unchecked failures usually surface as mysterious hangs or crashes.

1
2
3
4
5
int rc = pthread_create(&t, NULL, worker, NULL);
if (rc != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(rc));
}
// pthread functions return an errno-style code (not set errno)

Deadlock Avoidance

Deadlock arises when you "hold what you need and wait for what you don't". Lock in a fixed global order, keep critical sections small, prefer one lock per resource, and use timed locks when bounded waiting is required.

1
2
3
// Lock in a fixed global order.
// Try pthread_mutex_timedlock for bounded waits.
// Prefer one lock per resource; keep critical sections short.

15.Networking (Sockets)

getaddrinfo, TCP client and server, a minimal HTTP request — no extra library linking needed on Linux/macOS.

Build & Headers

POSIX sockets need <sys/socket.h>, <netinet/in.h>, and <arpa/inet.h>; no extra linking on Linux/macOS. Use getaddrinfo to resolve addresses, not hand-filled sockaddr structs.

1
2
3
4
5
6
// Build: $ gcc -std=c17 prog.c -o prog (Linux/macOS; Windows uses Winsock)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

TCP Client — Resolve, Connect, Send/Recv

A TCP client uses getaddrinfo to resolve the server, then creates a socket and calls connect. The address list can be long — try each entry in turn, keep the first one that succeeds, and close the ones that fail as you go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int connect_tcp(const char *host, const char *port) {
struct addrinfo hints = {0}, *res;
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
int g = getaddrinfo(host, port, &hints, &res);
if (g != 0) { fprintf(stderr, "%s\n", gai_strerror(g)); return -1; }
int fd = -1;
for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
close(fd); fd = -1;
}
freeaddrinfo(res);
return fd;
}

Send / Receive

send/recv move bytes on a connected socket but never guarantee one call does it all. Loop until the whole message is read or EOF, tracking how many bytes still remain.

1
2
3
4
5
6
7
int fd = connect_tcp("example.com", "80");
const char *req = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
send(fd, req, strlen(req), 0);
char buf[4096];
ssize_t n = recv(fd, buf, sizeof buf - 1, 0); // may be partial
buf[n] = '\0';
close(fd);

TCP Server — The socket Flow

A server creates a socket, sets SO_REUSEADDR, binds a port, listens, and loops on accept. Each accept returns a new socket dedicated to that client — the listening socket is never read from.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int srv = socket(AF_INET, SOCK_STREAM, 0);
int one = 1;
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8080) };
addr.sin_addr.s_addr = INADDR_ANY;
bind(srv, (struct sockaddr *)&addr, sizeof addr);
listen(srv, 16);
for (;;) {
int cfd = accept(srv, NULL, NULL);
char req[1024];
recv(cfd, req, sizeof req - 1, 0);
const char *resp = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
send(cfd, resp, strlen(resp), 0);
close(cfd);
}

Resolve Hostname -> IP

getaddrinfo turns a hostname and port into a list of candidate addresses, handling IPv4/IPv6 and service names along the way. Try them one by one until one succeeds, then free the list with freeaddrinfo.

1
2
3
4
5
6
struct addrinfo *r;
getaddrinfo("example.com", NULL, &(struct addrinfo){.ai_family=AF_INET}, &r);
struct sockaddr_in *sa = (struct sockaddr_in *)r->ai_addr;
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &sa->sin_addr, ip, sizeof ip);
freeaddrinfo(r);

Blocking vs Non-Blocking

Sockets are blocking by default: recv waits until data arrives. O_NONBLOCK makes the call return EAGAIN when nothing's ready, letting poll / epoll / select supervise many sockets from one thread.

1
2
3
// fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
// Non-blocking recv returns -1 with errno==EAGAIN/EWOULDBLOCK when idle.
// For many clients: poll()/select()/epoll (Linux) — see man poll.

UDP — sendto/recvfrom (Connectionless)

UDP is connectionless: sendto/recvfrom carry the destination address on each call — no handshake, no order, no delivery guarantee. Great for latency-sensitive traffic that can tolerate loss.

1
2
3
int us = socket(AF_INET, SOCK_DGRAM, 0);
sendto(us, "ping", 4, 0, (struct sockaddr *)&addr, sizeof addr);
recvfrom(us, buf, sizeof buf, 0, NULL, NULL);

Byte Order — htons/htonl & Network Order

Network byte order is big-endian; your host may be little-endian. When filling sockaddr fields and reading numeric protocol fields off the wire, convert with htons/htonl/ntohs/ntohl.

1
2
// uint16_t port = htons(8080); host -> network
// uint16_t hp = ntohs(port); network -> host

Windows (Winsock) Differences:

Windows uses winsock2.h, requires WSAStartup before any socket use, and closes sockets with closesocket() instead of close(). The rest of the API matches POSIX sockets, so the code is largely portable.

1
2
3
// #include <winsock2.h> + ws2_32.lib
// WSAStartup(MAKEWORD(2,2), &wsaData); ... WSACleanup();
// close() -> closesocket()

Error Codes

Socket calls return -1 and set errno on failure — ECONNREFUSED, ETIMEDOUT, ECONNRESET — while getaddrinfo returns its own codes for gai_strerror. Check every step; a silent failure here becomes a hang later.

1
2
3
// socket() -> -1 + errno
// connect() -> -1 + errno (ECONNREFUSED, ETIMEDOUT, EHOSTUNREACH)
// getaddrinfo() -> gai_strerror(rc)

HTTPS Needs a TLS Library

Raw sockets only get you TCP. Encryption requires a TLS library — with OpenSSL, build an SSL_CTX, wrap the socket with SSL_new/SSL_connect, and use SSL_read/SSL_write — or use libcurl for a higher-level HTTP+TLS API.

1
// Raw sockets only give you TCP. See libcurl for a higher-level API.

16.Time & Date

Wall-clock time, UTC/local conversions, strftime formatting, and high-resolution monotonic timing.

Unix Epoch Seconds

time(NULL) returns the number of whole seconds since 1970-01-01 00:00 UTC (a time_t) — a timezone-free, universal instant that is what you want for storing timestamps.

1
2
3
4
#include <time.h>
#include <stdio.h>
time_t now = time(NULL); // seconds since 1970-01-01 UTC
printf("%lld\n", (long long)now);

Convert to UTC / Local struct tm

gmtime and localtime split a time_t into a struct tm (year, month, day, hour, ...) — in UTC and in the local timezone respectively. Mind the offsets: tm_year counts years since 1900 and tm_mon runs from 0 to 11.

1
2
3
4
5
struct tm *utc = gmtime(&now); // UTC
struct tm *loc = localtime(&now); // local timezone
int year = utc->tm_year + 1900; // tm_year is years since 1900
int mon = utc->tm_mon + 1; // tm_mon is 0-11
int day = utc->tm_mday;

Format With strftime

strftime formats a struct tm into readable text using %Y %m %d %H:%M:%S and friends — printf for dates. It is the standard way to build log lines and human-readable timestamps.

1
2
3
4
5
char out[64];
strftime(out, sizeof out, "%Y-%m-%d %H:%M:%S", loc);
printf("%s\n", out); // 2026-08-02 14:05:09
// %Y year %m month %d day %H hour %M minute %S second
// %a weekday %A full name %z timezone offset %s epoch

Parse a Date Back to time_t

strptime (POSIX) parses text into a struct tm, and mktime converts that into a time_t. Set tm_isdst to -1 so mktime works out daylight saving time itself instead of guessing wrong.

1
2
3
4
struct tm t = {0};
strptime("2026-08-02", "%Y-%m-%d", &t); // POSIX
// t.tm_isdst = -1; // let mktime resolve DST
// time_t epoch = mktime(&t);

Elapsed Wall-Clock Time

difftime(b, a) returns the wall-clock seconds between two time_t values. It is simple and good enough for coarse durations, but the wall clock can jump — use a monotonic clock when you are measuring.

1
2
3
4
time_t a = time(NULL);
/* work */
time_t b = time(NULL);
printf("%.0f s\n", difftime(b, a)); // difftime returns double

High-Resolution Monotonic Clock

clock_gettime(CLOCK_MONOTONIC) measures elapsed time that is unaffected by NTP and daylight saving adjustments — use it for benchmarks, timeouts and any timing that must never jump.

1
2
3
4
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); // immune to wall-clock changes
long ms = ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
// CLOCK_MONOTONIC: elapsed time. CLOCK_REALTIME: wall clock.

Nanosecond Sleep (C11 <time.h>)

nanosleep suspends the thread with timespec (nanosecond) precision. When a signal interrupts it, it reports the remaining time so the call can go back and finish sleeping.

1
2
struct timespec req = { .tv_sec = 0, .tv_nsec = 5000000 }; // 5 ms
nanosleep(&req, NULL);

CPU Time Used by This Process

clock() returns the CPU time consumed by the current process, not wall-clock time. Comparing CPU time against wall-clock time shows whether a task is waiting on I/O rather than computing.

1
2
3
clock_t c = clock(); // CPU clock ticks
/* work */
printf("%.2f s\n", (double)(clock() - c) / CLOCKS_PER_SEC);

Get the Timezone Offset

tm_gmtoff (GNU/glibc) holds the local time's offset from UTC in seconds (positive east), letting you render an RFC 822-style +0800 offset without guessing.

1
2
struct tm *l = localtime(&now);
printf("%ld\n", l->tm_gmtoff); // seconds east of UTC (GNU)

ISO 8601 Without strftime

Use snprintf to assemble an ISO 8601 timestamp from the fields of a UTC struct tm. The output is locale-independent and unambiguous — ideal for APIs and logs.

1
2
3
4
char iso[32];
snprintf(iso, sizeof iso, "%04d-%02d-%02dT%02d:%02d:%02dZ",
utc->tm_year + 1900, utc->tm_mon + 1, utc->tm_mday,
utc->tm_hour, utc->tm_min, utc->tm_sec);

17.Processes & Signals

fork, exec, wait, inter-process pipes, signal handling, and a minimal daemon.

Platform & Headers

fork/exec/wait and signals are POSIX (Linux/macOS); on Windows you use CreateProcess, which is a different model. Calls in this section need <unistd.h>, <sys/wait.h>, and <signal.h>.

1
2
3
4
5
// POSIX only (Linux/macOS). Build: $ gcc prog.c -o prog
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>

fork — Clone the Current Process

fork clones the current process. Both copies resume from the same return point: the child sees pid 0, the parent sees the child's pid. The child should _exit() — returning from main would run the parent's cleanup and flush stdio twice.

1
2
3
4
5
6
7
8
9
10
11
12
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
// child — this code runs only in the child
printf("child: pid=%d\n", getpid());
_exit(0); // child should _exit, not return
} else {
// parent
int status;
waitpid(pid, &status, 0); // block until child exits
if (WIFEXITED(status)) printf("exit=%d\n", WEXITSTATUS(status));
}

exec — Replace the Process Image

exec replaces the current process image with another program: the pid stays the same, the code is fresh. It returns only on failure (then call _exit(127), the shell convention). Environment, file descriptors, and working directory are preserved.

1
2
3
4
5
6
if (pid == 0) {
char *argv[] = { "ls", "-l", NULL };
execvp(argv[0], argv); // search PATH
perror("execvp");
_exit(127); // only reached on failure
}

fork/exec/wait to Run a Command

Running an external command is exactly what the shell does: fork (clone), execvp (swap image), waitpid (reap). WEXITSTATUS pulls out the child's exit code, WIFSIGNALED tells you whether it was killed by a signal.

1
2
3
4
5
6
7
int run(const char *cmd, char *const args[]) {
pid_t pid = fork();
if (pid == 0) { execvp(cmd, args); _exit(127); }
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

Pipe — Child Writes, Parent Reads

pipe(fd) creates a pair of connected descriptors: one end writes, the other reads. After fork, each process closes the end it isn't using, and the read end sees EOF when the write end is closed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int fd[2];
pipe(fd);
pid_t p = fork();
if (p == 0) {
close(fd[0]); // child: close read end
write(fd[1], "hello\n", 6);
close(fd[1]);
_exit(0);
}
close(fd[1]); // parent: close write end
char buf[128];
ssize_t n = read(fd[0], buf, sizeof buf - 1);
buf[n] = '\0';
close(fd[0]);
waitpid(p, NULL, 0);

Signal Handlers

signal() registers a handler for an asynchronous signal. Inside the handler, only call async-signal-safe functions — write, _exit, signal, kill — never printf or malloc, which are non-reentrant.

1
2
3
4
5
6
7
void on_sigint(int sig) {
write(2, "interrupted\n", 12); // async-signal-safe only
_exit(130);
}
signal(SIGINT, on_sigint);
// Safe functions in a handler: write, _exit, signal, kill
// NOT safe: printf, malloc, most libc — use sigaction + sigqueue for real code.

sigaction — A Robust API

sigaction is the robust replacement for signal(): it can block other signals while the handler runs, set SA_RESTART to auto-resume interrupted system calls, and tell the handler which signal fired. Prefer it for new code.

1
2
3
4
5
struct sigaction sa = {0};
sa.sa_handler = on_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGTERM, &sa, NULL);

Block Signals Temporarily

sigprocmask defers signals around a critical section, then unblocks them — so handlers can't jump in while shared state is half-mutated. Pair it with a handler that processes "saved events" and signal use becomes safe.

1
2
3
4
5
6
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
/* critical section */
sigprocmask(SIG_UNBLOCK, &set, NULL);

Kill a Process / Check Existence

kill(pid, sig) sends a signal to a process; kill(pid, 0) just checks that the process exists and you may signal it, without actually delivering. A negative pid targets the whole process group.

1
2
kill(pid, SIGTERM); // send signal to pid
// kill(pid, 0) == 0 -> process exists (may still be zombie)

Zombies — Reap Your Children

A child that exits before its parent waits becomes a zombie, holding a pid until wait/waitpid reaps it. Ignore SIGCHLD to let children be auto-reaped, or reap them explicitly in a handler.

1
2
3
4
// Child that exits before parent waits becomes a zombie (shows as Z in ps).
// Reap via wait/waitpid, or install a SIGCHLD handler:
signal(SIGCHLD, SIG_IGN); // simple: auto-reap
// Or in the handler: while (waitpid(-1, NULL, WNOHANG) > 0) {}

Daemon Skeleton (Double fork)

A daemon detaches from the controlling terminal: fork so the child isn't the process-group leader, setsid to open a new session, fork again, chdir('/'), and redirect stdio to /dev/null or a log file.

1
2
3
4
5
// if (fork() != 0) _exit(0); // 1st fork
// setsid(); // new session, detach tty
// if (fork() != 0) _exit(0); // 2nd fork — never session leader
// chdir("/"); umask(0);
// redirect stdin/out/err to /dev/null or a log file

18.POSIX Regular Expressions

regcomp/regexec/regerror: compile patterns, match and extract capture groups, iterate all matches.

Setup & Headers

POSIX regex (regcomp/regexec) ships in Linux/macOS libc — no extra library needed. With REG_EXTENDED you get ERE syntax; the default BRE is an older, different dialect.

1
2
3
// POSIX regex (<regex.h>), Linux/macOS.
#include <regex.h>
#include <stdio.h>

Compile & Match

First regcomp compiles the pattern once (and check the return value!), then regexec matches against a string. Use regfree to release the compiled pattern — every regcomp pairs with one regfree.

1
2
3
4
5
6
7
8
9
10
11
12
regex_t re;
int rc = regcomp(&re, "^[a-z]+[0-9]+$", REG_EXTENDED);
if (rc != 0) { // REG_EXTENDED = modern ERE syntax
char err[128];
regerror(rc, &re, err, sizeof err);
fprintf(stderr, "regex: %s\n", err);
return 1;
}
rc = regexec(&re, "abc123", 0, NULL, 0);
if (rc == 0) puts("matched");
else if (rc == REG_NOMATCH) puts("no match");
regfree(&re); // always release

Capture Groups

Parenthesized groups land in regmatch_t entries: m[1] holds the start/end offsets of group 1 within the matched text. Use the %.*s precision trick with the offset pair to print a captured slice.

1
2
3
4
5
6
7
8
9
10
regmatch_t m[3]; // m[0]=whole, m[1]=group1, m[2]=group2
regex_t r2;
regcomp(&r2, "(\\w+)@(\\w+)\\.(\\w+)", REG_EXTENDED);
const char *text = "mail [email protected] now";
if (regexec(&r2, text, 3, m, 0) == 0) {
// m[i].rm_so/rm_eo are offsets into 'text'
printf("user: %.*s\n",
(int)(m[1].rm_eo - m[1].rm_so), text + m[1].rm_so);
}
regfree(&r2);

Iterate All Matches (Loop With REG_NOTBOL)

To find all matches, loop regexec through the string, advancing past each match. After the first iteration pass REG_NOTBOL so ^ doesn't re-anchor mid-string.

1
2
3
4
5
6
7
8
9
10
regex_t r3;
regcomp(&r3, "\\d+", REG_EXTENDED);
const char *s = "a1 b22 c333";
regmatch_t match;
int off = 0;
while (regexec(&r3, s + off, 1, &match, off ? REG_NOTBOL : 0) == 0) {
printf("num: %.*s\n", (int)(match.rm_eo - match.rm_so), s + off + match.rm_so);
off += match.rm_eo; // advance past the match
}
regfree(&r3);

Useful Flags

REG_EXTENDED enables ERE syntax (use it); REG_ICASE is case-insensitive matching; REG_NOSUB skips capture tracking for speed; REG_NEWLINE makes ^/$ match line boundaries instead of the whole string.

1
2
3
4
5
// REG_EXTENDED: ERE syntax (+, ?, |, (), {m,n})
// REG_ICASE: case-insensitive matching
// REG_NOSUB: skip capture info, faster
// REG_NEWLINE: '.' won't cross newlines; ^/$ anchor per line
// regexec(..., REG_NOTBOL): next char is not a line start

ERE Syntax Quick Reference

ERE quick reference: . any char, ^ $ anchors, [] character classes, * + ? repetition, {m,n} bounded repetition, (a|b) alternation, backreferences, and POSIX classes inside brackets like [[:alpha:]].

1
2
3
4
// . any char ^ $ anchors [abc] [a-z] [^0-9] classes
// * + ? {2,4} repetition
// (a|b) groups \\1 backref \\d \\w \\s (GNU)
// POSIX classes: [[:alpha:]] [[:digit:]] [[:space:]] [[:alnum:]]

Replacement — None Built-in; Assemble Output by Hand

POSIX regex has no built-in replacement. Assemble the output: loop matches, copy the text between them, insert the replacement at each match — the same pattern as iterating all matches.

1
2
3
// Step 1: regexec to find each match
// Step 2: copy text up to match, append replacement, continue
// Step 3: copy the tail. See regexec iteration in §3.

Escape User Input

To embed user input in a pattern, prefix each metacharacter ([\\^$.|?*+()[]{}) with a backslash. Otherwise a crafted input changes what your pattern means — this is an injection bug, not just a logic issue.

1
2
// Prefix every char in [\\^$.|?*+()[]{} with '\\' before regcomp
// to treat the input literally.

Check regcomp and Always regfree

regcomp fails on a bad pattern — always check the return value and format with regerror. And pair every regcomp with a regfree, or matching repeatedly inside a loop leaks compiled patterns.

1
2
// regcomp can fail (bad pattern) — see §1 error path.
// Every regcomp must pair with a regfree.

Limits & Performance

Reuse a compiled regex_t — recompiling per match is wasted work. POSIX regex engines backtrack, and pathological patterns against untrusted input can be slow — for heavy or adversarial use, prefer PCRE2 or RE2.

1
2
3
// regcomp/regexec are compiled once; reuse the regex_t.
// Patterns are backtracking-based; pathological input can be slow
// (quadratic). For heavy use prefer PCRE2 or RE2.

Compile-Time Matching for switch-like Dispatch

Pre-compile patterns at startup and chain regexec calls in order. When the input can match several shapes, it reads like a clean routing table — and beats a wall of string compares.

1
2
3
// if (regexec(&re_ipv4, s, 0, NULL, 0) == 0) {}
// else if (regexec(&re_ipv6, s, 0, NULL, 0) == 0) {}
// Pre-compile all patterns at startup, reuse them.

19.Build & Debug

Compiler flags, a minimal Makefile, gdb, sanitizers, and valgrind — the tools that find the bugs the FAQ warns about.

Common gcc/clang Flags

-std=c17 pins the language, -Wall -Wextra turns on useful warnings (always on), -g adds debug info, -O0/-O2 picks optimization, -fsanitize=address,undefined adds runtime checks. Free, and catches real bugs.

1
2
3
4
5
6
7
8
9
10
// -std=c17: language standard
// -Wall -Wextra: enable warnings (always)
// -Werror: warnings become errors (CI)
// -pedantic: reject non-ISO extensions
// -g: debug info (needed for gdb / asan traces)
// -O0 / -O1 / -O2: optimization levels
// -fsanitize=address,undefined: runtime checkers
// -o out: output name -I include dir
// -L libdir -lm: link libm -pthread
$ gcc -std=c17 -Wall -Wextra -g -O0 prog.c -o prog

Makefile — A Minimal Build Driver

A minimal Makefile puts the compiler, flags, target name, and object list into variables, leans on the implicit rule %.o:%.c, and provides a clean target. make rebuilds only what changed — that's the whole point.

1
2
3
4
5
6
7
8
9
10
11
// CC = gcc
// CFLAGS = -std=c17 -Wall -Wextra -g
// TARGET = app
// OBJS = main.o utils.o
// $(TARGET): $(OBJS)
// \t$(CC) $(CFLAGS) -o $@ $(OBJS)
// %.o: %.c
// \t$(CC) $(CFLAGS) -c $<
// clean:
// \trm -f $(TARGET) $(OBJS)
// $ make && ./app

Debug Build & Release Build

Debug builds with -g -O0 keep full symbols and no optimization; release builds with -O2 -DNDEBUG, which also compiles out assert. Keep both, so you can reproduce a crash under the exact config a user ran.

1
2
3
// Debug: gcc -g -O0 -Wall -Wextra
// Release: gcc -O2 -DNDEBUG (asserts compiled out)
// Profile: gcc -O2 -pg + gprof ./app

gdb Basics

gdb ./prog, then: break main, run, next/step through lines, print x to inspect, backtrace for the call stack, continue, quit. Pair with -batch -ex to dump crashes from CI non-interactively.

1
2
3
4
5
6
7
8
9
10
11
// $ gcc -g prog.c -o prog
// $ gdb ./prog
// (gdb) break main set breakpoint
// (gdb) run start
// (gdb) next / step line / into
// (gdb) print x inspect variable
// (gdb) backtrace call stack (bt)
// (gdb) list show source
// (gdb) continue resume (c)
// (gdb) quit
// Non-interactive: $ gdb -batch -ex 'run' -ex 'bt' ./prog

AddressSanitizer (ASan)

Compile with -fsanitize=address,undefined and the program reports overflows, use-after-free, and leaks at runtime with precise stack traces. Faster and clearer than valgrind — the first tool to turn on.

1
2
3
// $ gcc -fsanitize=address,undefined -g prog.c -o prog
// $ ./prog -> detailed report on crash (use-after-free, overflow)
// Leak detection: $ ASAN_OPTIONS=detect_leaks=1 ./prog

valgrind Memory Check

valgrind --leak-check=full ./prog reports invalid reads/writes, use-after-free, and definite leaks with no recompile — but it's 20-50× slower. Use it when you can't rebuild or need more detail.

1
2
3
4
// $ gcc -g prog.c -o prog
// $ valgrind --leak-check=full ./prog
// Reports: invalid reads/writes, use-after-free, definite leaks.
// Slower (~20-50x) — use on failure, not in production.

UBSan Flags

-fsanitize=undefined catches signed overflow, misaligned access, and other undefined behavior at runtime with precise reports. Combine with -fsanitize=address for one build that covers both memory and UB.

1
2
3
// -fsanitize=undefined detects: signed overflow, shift UB,
// misaligned access, null deref, etc.
// Combine: -fsanitize=address,undefined

Compile to Assembly / Preprocess

gcc -S emits assembly, gcc -E shows the preprocessed source, gcc -c produces a non-linked object file. Each is a small window onto what the compiler and preprocessor actually did.

1
2
3
// $ gcc -S prog.c -> prog.s (assembly)
// $ gcc -E prog.c -> preprocessed source
// $ gcc -O2 -c prog.c -> prog.o (object, no link)

Static Analysis (Optional)

gcc -fanalyzer, clang --analyze, and cppcheck find bugs without running the program — null dereferences, leaks, uninitialized uses. Cheap to add to CI, and they catch bug classes tests miss.

1
2
3
// $ gcc -fanalyzer prog.c GCC analyzer (GCC 10+)
// $ clang --analyze prog.c Clang static analyzer
// cppcheck prog.c third-party

Build Systems

Make fits small to medium projects; CMake generates Make or Ninja files and is portable across platforms; Meson is a faster, more modern alternative. For a single file, a Makefile or even a shell alias is enough.

1
2
3
4
// Make (above) — small/medium projects
// CMake — portable, generates Make/Ninja files
// Meson — faster, more modern; Meson + Ninja
// For single files, just a Makefile or even a one-line shell alias.

Official Links

Direct links to the official docs and resources.

About this Cheatsheet

This page is a self-contained cheatsheet for ISO C (C11/C17), covering the language and the parts of the standard library that handle around 80% of systems-programming day-to-day use, plus the concurrency, sockets, regex, and build tools real projects actually rely on. For authoritative references, see the C11 standard (ISO/IEC 9899:2011) and the C section of cppreference. 19 sections, each focused on a single theme — from your first program and pointers, through strings, memory ownership, and common pitfalls. Each section splits into 6-10 sub-topics with 5-15 line snippets. Code blocks are deliberately short and self-explanatory, with a copy button on each so you can paste them straight into a compiler. Everything runs in your browser — no uploads, no tracking. This page is part of GuruToolkit's free developer-toolset collection. The snippets here are free to use, with no warranty.

Version 2.3.0