Skip to content

C Cheatsheet

Low-level language powering systems, embedded, and OS development.

01

Getting Started

Hello World

Every C program starts in main(). #include <stdio.h> brings in the standard I/O library (printf, scanf). main returns 0 on success, non-zero on failure. The void keyword explicitly declares that main takes no parameters.

c
#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

Variables & Types

C is statically typed. Common types: int, float, double, char. float needs the f suffix. char[] is a string (null-terminated array). long and short are size modifiers. unsigned means non-negative. Sizes vary by platform; use <stdint.h> for fixed widths.

c
int age = 30;
float height = 5.7f;
double pi = 3.14159;
char grade = 'A';
char name[] = "Alice";
long big = 100000L;
unsigned int count = 42;
printf("%s is %d\n", name, age);

Input & Output

scanf needs the address (&) of the variable to store input. Always limit string input length (%49s for a 50-char buffer) to prevent buffer overflow. scanf stops reading strings at whitespace; use fgets for full lines.

c
int n;
printf("Enter a number: ");
scanf("%d", &n);
printf("You entered %d\n", n);

char name[50];
printf("Enter name: ");
scanf("%49s", name);  // limit to prevent overflow
printf("Hi, %s!\n", name);

printf Format Specifiers

Format specifiers control output: %d int, %f float, %c char, %s string, %x hex, %p pointer. Width and precision (e.g. %5.2f) control alignment and decimals. Mismatching the specifier and type causes undefined behavior.

c
printf("%d\n", 42);        // integer
printf("%f\n", 3.14);       // float/double
printf("%.2f\n", 3.14159);  // 3.14 (2 decimals)
printf("%c\n", 'A');        // char
printf("%s\n", "hello");    // string
printf("%x\n", 255);        // ff (hex)
printf("%5d\n", 42);        // right-aligned, width 5
printf("%-5d|\n", 42);      // left-aligned

Preprocessor & Headers

The preprocessor runs before compilation. #include pastes header files, #define creates macros and constants. Always use include guards (#ifndef/#define/#endif) in headers to prevent double inclusion. Macros are text substitution—use parentheses around parameters.

c
#include <stdio.h>   // system header
#include "myheader.h"  // local header

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

#ifndef GUARD_H
#define GUARD_H
// header content
#endif
02

Strings & String.h

String Basics

C strings are null-terminated char arrays. strlen counts chars before '\0'; sizeof returns the buffer size. strcpy copies until the null terminator—always ensure the destination is large enough to avoid overflow.

c
#include <string.h>
char s[20] = "Hello";
printf("Length: %zu\n", strlen(s));   // 5
printf("Size: %zu\n", sizeof(s));     // 20

char dest[20];
strcpy(dest, s);   // copy
printf("%s\n", dest);  // Hello

Concatenation & Comparison

strcat appends (destination must have room). strcmp compares lexicographically: returns 0 if equal, negative if first < second, positive if first > second. Never use == to compare strings (that compares pointers, not content).

c
#include <string.h>
char s[30] = "Hello";
strcat(s, ", World!");   // s = "Hello, World!"
printf("%s\n", s);

int cmp = strcmp("apple", "banana");
// returns <0 if a<b, 0 if equal, >0 if a>b
if (strcmp(s, "Hello") == 0) {
    printf("Equal!\n");
}

sprintf & sscanf

sprintf formats into a string buffer (like printf but to a string). sscanf parses a string into variables (like scanf but from a string). Use snprintf instead of sprintf to prevent buffer overflow by specifying max size.

c
char buf[100];
int age = 30;
char name[] = "Alice";
sprintf(buf, "%s is %d years old", name, age);
printf("%s\n", buf);

int a, b;
sscanf("10 20", "%d %d", &a, &b);
printf("a=%d, b=%d\n", a, b);  // a=10, b=20

strchr, strstr & strtok

strchr finds a character, strstr finds a substring. strtok splits a string by delimiters but modifies the original string (inserts null terminators) and is not thread-safe—pass NULL on subsequent calls to continue tokenizing.

c
#include <string.h>
char s[] = "Hello, World!";
char *p = strchr(s, 'W');   // find first 'W'
printf("%s\n", p);          // World!

char *sub = strstr(s, "World");
printf("%s\n", sub);        // World!

char tokens[] = "a,b,c";
char *tok = strtok(tokens, ",");
while (tok) {
    printf("%s\n", tok);
    tok = strtok(NULL, ",");
}

fgets & Safe Input

fgets is the safe way to read strings—it takes a size limit to prevent overflow. Unlike scanf, it reads spaces. The newline is included in the result; strcspn finds and removes it. Always prefer fgets over gets (which is removed from C11).

c
char line[100];
printf("Enter text: ");
fgets(line, sizeof(line), stdin);
// removes trailing newline
line[strcspn(line, "\n")] = 0;
printf("You said: %s\n", line);
03

Numbers & Math

Integer Types & Limits

Use <stdint.h> for fixed-width types (int32_t, int64_t) when exact sizes matter. <limits.h> provides INT_MAX, INT_MIN, etc. for platform-specific bounds. The LL suffix marks long long literals. Sizes of int/long vary by platform.

c
#include <stdint.h>
#include <limits.h>
int32_t a = 100;
int64_t big = 9223372036854775807LL;
uint8_t byte = 255;

printf("INT_MAX = %d\n", INT_MAX);     // 2147483647
printf("INT_MIN = %d\n", INT_MIN);     // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX);   // 4294967295

Floating Point

float is 4 bytes (6-7 digits precision), double is 8 bytes (15-16 digits). Never compare floats with == due to rounding errors—use fabs(a - b) < epsilon. <float.h> provides DBL_MAX, DBL_EPSILON for bounds and precision.

c
#include <float.h>
double d = 3.141592653589793;
float f = 3.14f;

printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);

if (d == 0.1 + 0.2) {
    // likely false! floating point imprecision
}

Math Functions

<math.h> provides standard math functions. pow and sqrt return double. fabs is the float version of abs (abs is for int). Link with -lm on some systems. For financial calculations, avoid floating point—use integer cents instead.

c
#include <math.h>
double x = 2.5;
pow(x, 3);      // 15.625
sqrt(x);        // 1.581
fabs(-5.0);     // 5.0
floor(3.7);     // 3.0
ceil(3.2);      // 4.0
fmod(10.5, 3);  // 1.5
exp(1);         // 2.718 (e^1)
log(2.718);     // 1.0 (natural log)

Random Numbers

rand() returns a pseudo-random int from 0 to RAND_MAX. Seed with srand() once at program start (using time(NULL)). rand() % N has modulo bias and low quality; for serious use, read /dev/urandom or use a third-party PRNG library.

c
#include <stdlib.h>
#include <time.h>

srand(time(NULL));  // seed once at start
int r = rand() % 100;       // 0-99
int dice = rand() % 6 + 1;  // 1-6

float fr = (float)rand() / RAND_MAX;  // 0.0 - 1.0

Type Conversion & Casting

Casting is (type)value. Integer division truncates—use a float operand to get a float result. atoi/atof convert strings to numbers but do no error checking; prefer strtol/strtod which report parse errors via errno.

c
int i = 65;
char c = (char)i;        // 'A'
double d = 3.99;
int truncated = (int)d;  // 3

// Implicit promotion
int a = 5;
double result = a / 2.0;  // 2.5 (promoted to double)
int bad = a / 2;          // 2 (integer division)

// String to number
int n = atoi("42");
double f = atof("3.14");
04

Control Flow

If / Else

if/else if/else is the standard conditional. C treats 0 as false and any non-zero value as true. Use braces even for single statements to prevent bugs when adding lines later. There is no boolean type in C89; C99 adds _Bool and <stdbool.h>.

c
int score = 85;
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 70) {
    printf("C\n");
} else {
    printf("F\n");
}

Switch

switch jumps to a matching case label. Always use break to prevent fall-through (cases 6 and 7 share code intentionally). default handles unmatched values. switch only works on integer and char types, not strings or floats.

c
int day = 3;
switch (day) {
    case 1: printf("Mon\n"); break;
    case 2: printf("Tue\n"); break;
    case 3: printf("Wed\n"); break;
    case 6:
    case 7: printf("Weekend\n"); break;
    default: printf("Invalid\n");
}

For Loop

The for loop has init; condition; update. sizeof(nums)/sizeof(nums[0]) computes array length at compile time. Declaring i inside the for loop requires C99 or later. The loop body executes zero times if the condition is initially false.

c
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// Iterate an array
int nums[] = {10, 20, 30};
int n = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < n; i++) {
    printf("%d\n", nums[i]);
}

While & Do-While

while checks before executing (may run zero times). do-while executes the body first, then checks (runs at least once). do-while is ideal for input validation and menu loops where the prompt must appear before the condition can be checked.

c
int count = 0;
while (count < 3) {
    printf("%d\n", count++);
}

int x;
do {
    printf("Enter positive: ");
    scanf("%d", &x);
} while (x <= 0);  // runs at least once

Break, Continue & goto

break exits the nearest loop/switch; continue skips to the next iteration. C has no labeled break, so goto is the idiomatic way to break out of deeply nested loops. goto is otherwise discouraged but acceptable for cleanup patterns and nested-loop exits.

c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    printf("%d ", i);      // 0 1 2 4 5 6
}

// goto for breaking nested loops
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (found) goto done;
    }
}
done: printf("exited\n");
05

Functions

Define & Call

Functions must be declared (prototype) or defined before use. void return type means no return value. const char *name means the function won't modify the string. C passes arguments by value; use pointers to simulate pass-by-reference.

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

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    int sum = add(3, 4);
    greet("Alice");
    return 0;
}

Recursion

Recursion calls itself with a smaller input. Every recursive function needs a base case to stop. The naive fib above is O(2^n)—exponential. Use memoization or iteration for efficiency. Deep recursion can overflow the call stack.

c
int factorial(int n) {
    if (n <= 1) return 1;       // base case
    return n * factorial(n - 1); // recursive case
}

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
// factorial(5) == 120

Function Pointers

Function pointers store the address of a function, enabling callbacks and dynamic dispatch. The syntax int (*op)(int, int) declares a pointer to a function taking two ints and returning int. Used in qsort, event handlers, and plugin systems.

c
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int) = add;
printf("%d\n", op(3, 4));  // 7

op = sub;
printf("%d\n", op(3, 4));  // -1

// As a parameter
int apply(int (*f)(int, int), int a, int b) {
    return f(a, b);
}

Variadic Functions

Variadic functions accept a variable number of arguments using <stdarg.h>. va_start initializes, va_arg retrieves the next argument, va_end cleans up. You need a way to know the count (e.g., a count parameter or a sentinel value). printf works this way.

c
#include <stdarg.h>
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;
}
// sum(3, 10, 20, 30) == 60

static & inline

static on a function/variable limits it to the current translation unit (file). static on a local variable makes it persist across calls (like a global but scoped). inline suggests the compiler embed the function body; modern compilers ignore it and decide themselves.

c
// static: internal linkage (file-local)
static int counter = 0;
static int next_id(void) { return ++counter; }

// inline: hint to expand inline
static inline int square(int x) { return x * x; }

// static local: persists across calls
int call_count(void) {
    static int n = 0;
    return ++n;
}
06

Arrays & Pointers

Arrays

Arrays are fixed-size, zero-indexed, and stored contiguously in memory. sizeof(arr)/sizeof(arr[0]) computes the length but only works on actual arrays, not pointers (arrays decay to pointers when passed to functions, losing size info).

c
int nums[5] = {1, 2, 3, 4, 5};
printf("%d\n", nums[0]);     // 1
printf("%d\n", nums[4]);     // 5

int len = sizeof(nums) / sizeof(nums[0]);  // 5

// Array of strings
char *fruits[] = {"apple", "banana", "cherry"};
printf("%s\n", fruits[1]);   // banana

Pointers

Pointers store memory addresses. & gets the address, * dereferences. Always initialize pointers (use NULL if not yet assigned). Dereferencing a NULL or uninitialized pointer is undefined behavior (usually a crash). Check for NULL before dereferencing.

c
int x = 10;
int *ptr = &x;     // ptr holds address of x
printf("%p\n", (void*)ptr);  // address
printf("%d\n", *ptr);        // 10 (dereference)

*ptr = 20;         // modify x through pointer
printf("%d\n", x);  // 20

int *p = NULL;     // null pointer (points to nothing)
if (p) { /* safe to dereference */ }

Pointer Arithmetic

Pointer arithmetic scales by the element size: p+1 moves to the next element, not the next byte. This makes p[i] equivalent to *(p+i). Subtracting two pointers to the same array gives the element count. Pointer arithmetic is only valid within an array.

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

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30

p += 3;              // now points to arr[3]
printf("%d\n", *p);  // 40

int diff = (p - arr); // 3 (number of elements)

Arrays vs Pointers

Array names decay to pointers when passed to functions or used in expressions, losing size information. This is why you must pass array length separately. sizeof(arr) gives the full array size only when arr is a true array, not a pointer.

c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // arr decays to &arr[0]

// These are equivalent
printf("%d\n", arr[2]);
printf("%d\n", ptr[2]);
printf("%d\n", *(arr + 2));

// But sizeof differs
printf("%zu\n", sizeof(arr));   // 20 (5 * 4 bytes)
printf("%zu\n", sizeof(ptr));   // 8 (pointer size)

Multidimensional Arrays

2D arrays are arrays of arrays, stored row-major. grid[i][j] accesses row i, column j. When passing to functions, the number of columns must be specified: void foo(int arr[][3], int rows). For dynamic 2D arrays, use arrays of pointers.

c
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

printf("%d\n", grid[0][1]);  // 2
printf("%d\n", grid[1][2]);  // 6

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", grid[i][j]);
    }
    printf("\n");
}
07

Structs & Unions

Structs

Structs group related variables of different types. Members are accessed with the dot operator (.). Initialize with brace notation. Structs are passed by value (copied); pass by pointer (struct Point *) to avoid copying and to modify the original.

c
struct Point {
    int x;
    int y;
};

struct Point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y);  // (3, 4)

p.x = 10;
p.y = 20;
printf("(%d, %d)\n", p.x, p.y);  // (10, 20)

typedef

typedef creates an alias for a type, so you can write Student instead of struct Student. It's commonly used with structs to simplify syntax. typedef can also alias function pointer types, making callbacks much more readable.

c
typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

Student s = {"Alice", 20, 3.8};
printf("%s: %d, GPA %.1f\n", s.name, s.age, s.gpa);

// typedef for other types
typedef unsigned long ulong;
typedef int (*CompareFn)(const void*, const void*);

Pointers to Structs

When you have a pointer to a struct, use the arrow operator (->) to access members. ptr->x is shorthand for (*ptr).x. Pass struct pointers to functions for efficiency (avoids copying large structs) and to allow modification.

c
typedef struct {
    int x, y;
} Point;

Point p = {3, 4};
Point *ptr = &p;

// Arrow operator (->) for pointer members
printf("%d\n", ptr->x);   // 3
ptr->y = 10;
printf("%d\n", p.y);      // 10

// Equivalent: (*ptr).x

Unions

Unions overlay multiple types on the same memory—only one member is valid at a time. Setting one member overwrites the others. Useful for type punning (reinterpreting bits) and saving memory when only one of several types is needed at once.

c
union Value {
    int i;
    float f;
    char bytes[4];
};

union Value v;
v.i = 65;
printf("%d\n", v.i);          // 65
printf("%c\n", v.bytes[0]);   // 'A' (same memory)

v.f = 3.14f;
printf("%d\n", v.i);  // reinterpreted bits!

Bit Fields & Enums

Bit fields pack multiple small fields into a single int, saving memory (common in protocols and hardware registers). Enums define named integer constants (0, 1, 2... by default). Use enums instead of #define for better debugging and type safety.

c
struct Flags {
    unsigned int bold : 1;
    unsigned int italic : 1;
    unsigned int size : 6;  // 0-63
};

struct Flags f = {1, 0, 12};
printf("bold=%d, size=%d\n", f.bold, f.size);

enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
printf("%d\n", c);  // 1
08

Memory Management

malloc & free

malloc allocates heap memory and returns a void pointer (or NULL on failure). Always check for NULL. Every malloc must be paired with a free to avoid memory leaks. Setting the pointer to NULL after free prevents use-after-free bugs.

c
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "malloc failed\n");
    return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 2;
free(arr);  // release memory
arr = NULL; // avoid dangling pointer

calloc & realloc

calloc allocates and zeroes memory (safer than malloc which has garbage). realloc resizes: it may move the block, returning a new pointer. If realloc fails, it returns NULL but the original block is still valid—use a temp pointer to avoid leaks.

c
#include <stdlib.h>
// calloc: zero-initialized
int *arr = calloc(5, sizeof(int));  // all zeros

// realloc: resize
arr = realloc(arr, 10 * sizeof(int));
if (!arr) { /* handle failure, original still valid */ }

free(arr);

Stack vs Heap

Stack memory is automatic (allocated/freed with function calls) and fast but limited (often 1-8 MB). Heap memory is manually managed via malloc/free, much larger, but slower and prone to leaks. Use stack for small, short-lived data; heap for large or long-lived data.

c
// Stack: automatic, fast, limited size
int local_var = 42;
int arr[100];  // on the stack

// Heap: manual, large, slower
int *heap_arr = malloc(1000000 * sizeof(int));

// Stack frame is freed when function returns
// Heap memory persists until explicitly freed

Memory Leaks & Dangling Pointers

Memory leaks occur when you lose the only pointer to allocated memory (can't free it). Dangling pointers point to freed memory—dereferencing is undefined behavior. Double-freeing is also undefined. Tools like Valgrind and AddressSanitizer detect these bugs.

c
// Memory leak: lost the pointer, can't free
void leak(void) {
    int *p = malloc(100 * sizeof(int));
    // function returns without free -> leaked!
}

// Dangling pointer: using freed memory
int *p = malloc(sizeof(int));
free(p);
*p = 42;  // UNDEFINED BEHAVIOR!

// Double free: also undefined
free(p);  // crash likely

Dynamic Arrays & Strings

Dynamic allocation lets you create strings/arrays whose size is determined at runtime. The caller is responsible for freeing the memory. Always allocate strlen+1 for strings (the null terminator). This pattern (allocate, return, caller frees) is common in C APIs.

c
#include <stdlib.h>
#include <string.h>

// Dynamic string copy
char *dup_str(const char *s) {
    char *copy = malloc(strlen(s) + 1);  // +1 for null
    if (copy) strcpy(copy, s);
    return copy;  // caller must free
}

char *name = dup_str("Alice");
printf("%s\n", name);
free(name);
09

File I/O

fopen & fclose

fopen opens a file and returns a FILE pointer (or NULL on failure). Modes: r (read), w (write/truncate), a (append), r+ (read/write), b (binary). Always check for NULL. fclose flushes buffers and closes the file. fgets reads one line safely.

c
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) {
    perror("fopen failed");
    return 1;
}

char line[256];
while (fgets(line, sizeof(line), f)) {
    printf("%s", line);
}

fclose(f);

fprintf & fscanf

fprintf and fscanf work like printf/scanf but on files. fscanf is fragile—format mismatches cause issues. For robust parsing, read lines with fgets then parse with sscanf. Always close files when done to flush buffers and release resources.

c
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fclose(f);

FILE *in = fopen("output.txt", "r");
char name[50];
int age;
fscanf(in, "Name: %49s\n", name);
fscanf(in, "Age: %d\n", &age);
printf("%s, %d\n", name, age);
fclose(in);

fread & fwrite (Binary)

fread/fwrite read/write raw bytes—ideal for binary data and structs. The arguments are: buffer, element size, count, file. Binary files are compact but not portable across architectures (endianness, struct padding). Always open binary files with 'b' mode.

c
typedef struct { int id; float score; } Record;

Record r = {1, 95.5f};
FILE *f = fopen("data.bin", "wb");
fwrite(&r, sizeof(Record), 1, f);
fclose(f);

Record r2;
FILE *in = fopen("data.bin", "rb");
fread(&r2, sizeof(Record), 1, in);
printf("id=%d, score=%.1f\n", r2.id, r2.score);
fclose(in);

fseek, ftell & rewind

fseek moves the file position: SEEK_SET (from start), SEEK_CUR (relative), SEEK_END (from end). ftell returns the current position. rewind is shorthand for fseek(f, 0, SEEK_SET). These enable random access in files, useful for databases and indexed lookups.

c
FILE *f = fopen("data.txt", "r");
fseek(f, 0, SEEK_END);   // jump to end
long size = ftell(f);     // get position = file size
printf("Size: %ld bytes\n", size);

rewind(f);                // back to start
// or: fseek(f, 0, SEEK_SET);

fseek(f, 10, SEEK_SET);   // 10 bytes from start
char c = fgetc(f);
printf("Char at 10: %c\n", c);
fclose(f);

stderr & Standard Streams

Every C program has three streams: stdin (keyboard), stdout (screen), stderr (screen, unbuffered). Writing errors to stderr separates them from normal output, enabling redirection: program 2> errors.log. stderr is unbuffered so messages appear before crashes.

c
#include <stdio.h>
// Three standard streams: stdin, stdout, stderr
fprintf(stdout, "Normal output\n");
fprintf(stderr, "Error: something went wrong\n");

int c;
while ((c = fgetc(stdin)) != EOF) {
    fputc(c, stdout);  // echo input
}

// stderr is unbuffered (appears immediately)
// stdout is line-buffered (flushes on newline)
10

Preprocessor & Macros

#define Constants & Macros

#define creates text substitution macros. Constants like PI improve readability and maintainability. Function-like macros must wrap parameters in parentheses to avoid precedence bugs: SQUARE(2+3) without parens would be 2+3*2+3=11. Prefer const variables and inline functions over macros.

c
#define MAX_SIZE 100
#define PI 3.14159
#define VERSION "2.0"

// Function-like macro
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int area = SQUARE(5);    // 25
int big = MAX(3, 7);     // 7

Conditional Compilation

Conditional compilation (#if, #ifdef, #ifndef) includes/excludes code at compile time. This is used for platform-specific code, debug builds, and feature flags. #ifdef checks if a macro is defined; #if evaluates its value. #elif and #else provide alternatives.

c
#define DEBUG 1

#if DEBUG
    printf("Debug: x=%d\n", x);
#endif

#ifdef _WIN32
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#endif

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

Include Guards

Include guards prevent double inclusion of headers, which would cause redefinition errors. The #ifndef/#define/#endif pattern is standard C. #pragma once is a simpler, widely-supported alternative (not standard but works on all major compilers).

c
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

struct Point { int x, y; };
void init_point(struct Point *p);

#endif // MYHEADER_H

// Alternative (non-standard but widely supported):
#pragma once

#pragma & Compiler Hints

#pragma provides compiler-specific directives. #pragma once is a simpler include guard. #pragma pack controls struct memory layout (important for binary protocols). __attribute__ (GCC/Clang) annotates functions for optimization, deprecation, and warnings.

c
#pragma once              // include guard
#pragma pack(1)            // struct packing (no padding)
#pragma GCC diagnostic ignored "-Wunused-variable"

// Common pragmas
#pragma message("Compiling " __FILE__)

// C99 __attribute__ (GCC/Clang)
__attribute__((deprecated)) void old_func(void);
__attribute__((noreturn)) void fatal(void);

Stringification & Token Pasting

# (stringification) turns a macro argument into a string literal. ## (token pasting) concatenates tokens into a new identifier. The two-level STR/XSTR pattern ensures macros are expanded before stringification. These are used in code generation and logging macros.

c
#define STR(x) #x
#define XSTR(x) STR(x)
#define CONCAT(a, b) a##b

printf("%s\n", STR(Hello World));  // "Hello World"
printf("%s\n", XSTR(VERSION));      // expands VERSION first

int CONCAT(foo, bar) = 42;  // creates variable foobar
printf("%d\n", foobar);     // 42
11

Bit Operations

Basic Bitwise Operators

Bitwise operators manipulate individual bits. AND (&) masks bits (keeps only set bits), OR (|) sets bits, XOR (^) toggles bits, NOT (~) inverts all bits. Left shift (<<) multiplies by powers of 2, right shift (>>) divides (for unsigned). Always use unsigned types for bit manipulation — signed right shift is implementation-defined (may sign-extend). Bit operations are extremely fast (single CPU cycle) and used in flags, hardware registers, compression, and cryptography. Binary literals (0b prefix) are C23/C++14; use hex (0x) or decimal in older C.

c
#include <stdio.h>

int main() {
    unsigned int a = 0b1100;  // 12
    unsigned int b = 0b1010;  // 10

    // AND: both bits must be 1
    printf("%u\n", a & b);   // 8  (0b1000)

    // OR: either bit is 1
    printf("%u\n", a | b);   // 14 (0b1110)

    // XOR: bits differ (exclusive or)
    printf("%u\n", a ^ b);   // 6  (0b0110)

    // NOT: flip all bits
    printf("%u\n", ~a);      // 4294967283 (on 32-bit)

    // Left shift: multiply by 2^n
    printf("%u\n", a << 2);  // 48 (12 * 4)

    // Right shift: divide by 2^n (unsigned)
    printf("%u\n", a >> 1);  // 6  (12 / 2)

    return 0;
}

Setting, Clearing & Toggling Bits

Bit flags pack multiple boolean options into a single integer, saving memory. The three core operations: SET (|= mask), CLEAR (&= ~mask), TOGGLE (^= mask), CHECK (& mask). Use #define with (1 << n) for readable flag names. This pattern is ubiquitous in system programming (file permissions, device control, configuration options). For example, Unix file permissions (rwxr-xr-x = 0755) use bit flags. Always use unsigned integers for flags to avoid sign-extension issues. This is more memory-efficient than an array of bools (1 bit vs 8 bits per flag).

c
#include <stdio.h>

// Flag definitions (powers of 2)
#define FLAG_READ    (1 << 0)  // 0b0001
#define FLAG_WRITE   (1 << 1)  // 0b0010
#define FLAG_EXECUTE (1 << 2)  // 0b0100
#define FLAG_ADMIN   (1 << 3)  // 0b1000

int main() {
    unsigned int permissions = 0;

    // SET a bit (OR with mask)
    permissions |= FLAG_READ | FLAG_WRITE;  // 0b0011

    // CHECK if a bit is set (AND, compare to 0)
    if (permissions & FLAG_READ) {
        printf("Read permission granted\n");
    }

    // CLEAR a bit (AND with inverted mask)
    permissions &= ~FLAG_WRITE;  // 0b0001

    // TOGGLE a bit (XOR with mask)
    permissions ^= FLAG_EXECUTE;  // 0b0101 (execute now on)
    permissions ^= FLAG_EXECUTE;  // 0b0001 (execute now off)

    // SET multiple bits at once
    permissions = FLAG_READ | FLAG_EXECUTE | FLAG_ADMIN;

    printf("Permissions: 0x%X\n", permissions);  // 0xD
    return 0;
}

Bit Manipulation Tricks

Bit tricks exploit binary representation for speed. x & 1 tests odd/even (faster than modulo). x & (x-1) clears the lowest set bit — useful for checking powers of 2 and counting bits. __builtin_popcount (GCC/Clang) or __popcnt (MSVC) count set bits in one instruction on modern CPUs. XOR swap (a^=b; b^=a; a^=b) avoids a temp variable but is slower on modern CPUs and less readable — avoid it. The 'round up to power of 2' trick propagates the highest set bit to all lower bits, then adds 1. These tricks are useful in embedded systems, game engines, and performance-critical code.

c
#include <stdio.h>

int main() {
    int x = 42;

    // Check if odd/even (faster than x % 2)
    if (x & 1) printf("odd\n"); else printf("even\n");

    // Check if power of 2 (only one bit set)
    // x & (x-1) clears the lowest set bit
    if (x && !(x & (x - 1))) printf("power of 2\n");

    // Count set bits (popcount / Hamming weight)
    unsigned int n = 0b10110110;
    int count = 0;
    while (n) { count += n & 1; n >>= 1; }
    printf("Set bits: %d\n", count);  // 6
    // Or use __builtin_popcount(n) (GCC/Clang)

    // Swap two values without temp (XOR swap)
    int a = 5, b = 10;
    a ^= b; b ^= a; a ^= b;
    // a=10, b=5 (avoid in practice — less readable)

    // Get lowest set bit
    unsigned int lowest = x & (-x);  // isolates lowest 1-bit

    // Round up to next power of 2
    unsigned int v = 5;
    v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;

    return 0;
}

Bit Fields in Structs

Bit fields pack multiple small values into a single struct, saving memory. The colon syntax (unsigned int field : N) specifies the bit width. The compiler handles bit extraction/insertion automatically. This is useful for memory-constrained systems, network protocols, and hardware register mapping. However, bit field layout is implementation-defined (byte order, alignment, padding) — don't use bit fields for cross-platform binary compatibility. Use explicit bit masking (#define + & |) for portable binary formats. Unnamed fields (: 5) add padding. The total size is rounded up to the struct's alignment.

c
#include <stdio.h>

// Bit fields: pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31  (5 bits)
    unsigned int month : 4;   // 0-15  (4 bits)
    unsigned int year  : 12;  // 0-4095 (12 bits)
    unsigned int is_leap : 1; // 0 or 1 (1 bit)
};  // Total: 22 bits (padded to 32)

struct Flags {
    unsigned int visible  : 1;
    unsigned int editable : 1;
    unsigned int locked   : 1;
    unsigned int          : 5;  // unnamed padding (5 bits)
    unsigned int priority : 4;  // 0-15
};

int main() {
    struct Date d = { 15, 6, 2024, 0 };
    printf("Size: %zu bytes\n", sizeof(d));  // 4 bytes
    printf("Date: %u/%u/%u\n", d.day, d.month, d.year);

    struct Flags f = { .visible = 1, .editable = 0, .locked = 1, .priority = 7 };
    printf("Size: %zu bytes\n", sizeof(f));  // 4 bytes

    return 0;
}

Practical Bit Manipulation (RGB Color)

Packing multiple values into one integer is common in graphics, networking, and embedded systems. RGB colors pack three 8-bit channels into 24 bits (0xRRGGBB). Shifting left (<<) positions each channel, OR (|) combines them. Shifting right (>>) and masking (& 0xFF) extracts individual channels. This saves memory (1 int vs 3 bytes) and enables atomic operations. The same pattern applies to network byte ordering, hardware register access, and data compression. Always use fixed-width types (uint8_t, uint32_t) for portability — int size varies by platform.

c
#include <stdio.h>
#include <stdint.h>

// Pack RGB into a single 32-bit integer (0xRRGGBB)
uint32_t make_color(uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// Extract components
uint8_t get_red(uint32_t color)   { return (color >> 16) & 0xFF; }
uint8_t get_green(uint32_t color) { return (color >> 8)  & 0xFF; }
uint8_t get_blue(uint32_t color)  { return color & 0xFF; }

// Blend two colors (50/50 mix)
uint32_t blend(uint32_t c1, uint32_t c2) {
    uint8_t r = (get_red(c1) + get_red(c2)) / 2;
    uint8_t g = (get_green(c1) + get_green(c2)) / 2;
    uint8_t b = (get_blue(c1) + get_blue(c2)) / 2;
    return make_color(r, g, b);
}

int main() {
    uint32_t red   = make_color(255, 0, 0);    // 0xFF0000
    uint32_t blue  = make_color(0, 0, 255);    // 0x0000FF
    uint32_t purple = blend(red, blue);          // 0x7F007F

    printf("Red:   0x%06X\n", red);
    printf("Blue:  0x%06X\n", blue);
    printf("Mix:   0x%06X (R=%d G=%d B=%d)\n",
           purple, get_red(purple), get_green(purple), get_blue(purple));

    return 0;
}
12

Signal Handling

Basic Signal Handling

Signals are software interrupts sent to a process (e.g., Ctrl+C sends SIGINT, division by zero sends SIGFPE). signal() registers a handler function. Inside a handler, only async-signal-safe functions are allowed — printf, malloc, and most stdlib functions are NOT safe because the main program may be interrupted mid-call. Use write() for output. Common signals: SIGINT (Ctrl+C), SIGTERM (termination request), SIGKILL (force kill, can't be caught), SIGSEGV (segfault), SIGALRM (timer). Prefer sigaction() over signal() for portability and control.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// Signal handler function (must match signature)
void handler(int sig) {
    // WARNING: only async-signal-safe functions allowed here!
    // printf is NOT safe — use write() instead
    const char *msg = "Caught SIGINT\n";
    write(STDOUT_FILENO, msg, 14);
}

int main() {
    // Register handler for Ctrl+C (SIGINT)
    signal(SIGINT, handler);

    // Ignore SIGINT entirely
    // signal(SIGINT, SIG_IGN);

    // Reset to default behavior (terminate)
    // signal(SIGINT, SIG_DFL);

    printf("PID %d waiting. Press Ctrl+C...\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

sigaction (Portable Signal Handling)

sigaction() is the modern, portable way to handle signals (signal() behavior varies by platform). The sa_sigaction handler receives a siginfo_t with details: si_pid (sender PID), si_uid (sender UID), si_signo (signal number), si_code (reason). SA_SIGINFO flag enables the three-argument handler. sa_mask blocks specified signals during handler execution (prevents nested interrupts). Other flags: SA_RESTART (auto-restart interrupted syscalls), SA_NOCLDWAIT (no zombie children). Always use sigaction() in production code — signal() is unreliable on some platforms.

c
#include <stdio.h>
#include <signal.h>
#include <string.h>

void handler(int sig, siginfo_t *info, void *context) {
    // siginfo_t provides details about the signal
    printf("Signal %d from PID %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = handler;  // use sa_sigaction (not sa_handler)
    sa.sa_flags = SA_SIGINFO;   // enable siginfo_t parameter

    // Block other signals during handler execution
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);  // block SIGQUIT during handler

    // Register (more portable than signal())
    sigaction(SIGINT, &sa, NULL);

    // Send a signal to self
    raise(SIGINT);  // like kill(getpid(), SIGINT)

    printf("Done\n");
    return 0;
}

Sending Signals & Alarm

alarm(seconds) schedules a SIGALRM after the specified time — useful for timeouts. pause() blocks until any signal arrives. volatile sig_atomic_t is the only safe way to share data between a signal handler and main code — volatile prevents compiler optimization, sig_atomic_t guarantees atomic access. kill(pid, signal) sends a signal to another process. raise(sig) sends a signal to yourself. SIGKILL (9) and SIGSTOP can't be caught or ignored — they always work. SIGTERM (15) is the polite termination request (programs can catch it to clean up). Use alarm() for simple timeouts; use setitimer()/timer_create() for more control.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_alarm = 0;

void alarm_handler(int sig) {
    got_alarm = 1;  // safe: sig_atomic_t is atomic
}

int main() {
    signal(SIGALRM, alarm_handler);

    // Set a timer: deliver SIGALRM after 3 seconds
    alarm(3);
    printf("Waiting for alarm...\n");

    // Wait for the alarm
    while (!got_alarm) {
        pause();  // sleep until any signal arrives
    }
    printf("Alarm fired!\n");

    // Send signal to another process
    // kill(pid, SIGTERM);  // request termination
    // kill(pid, SIGKILL);  // force kill (can't be caught)

    // Send signal to self
    raise(SIGUSR1);

    return 0;
}

Common Signals Reference

Understanding signals is essential for Unix programming. SIGKILL (9) and SIGSTOP can't be caught — they're the last resort. SIGTERM is the standard graceful shutdown signal (catch it to save state). SIGINT is Ctrl+C (interactive interrupt). SIGCHLD fires when a child exits — if you don't wait() for it, the child becomes a zombie. Setting SIGCHLD to SIG_IGN auto-reaps children (or use SA_NOCLDWAIT). SIGPIPE fires when writing to a closed pipe/socket — most servers ignore it (signal(SIGPIPE, SIG_IGN)) and check write() return values instead. Use _exit() (not exit()) in signal handlers — exit() runs atexit handlers which may not be signal-safe.

c
#include <signal.h>

// Common POSIX signals:
// SIGINT  (2)  - Ctrl+C interrupt (terminate)
// SIGQUIT (3)  - Ctrl+\ quit (core dump)
// SIGKILL (9)  - Force kill (CANNOT be caught/ignored)
// SIGSEGV (11) - Segmentation fault (invalid memory access)
// SIGPIPE (13) - Write to broken pipe
// SIGTERM (15) - Termination request (graceful shutdown)
// SIGSTOP (19) - Pause process (CANNOT be caught/ignored)
// SIGCONT (18) - Resume paused process
// SIGCHLD (17) - Child process exited
// SIGALRM (14) - Timer alarm
// SIGUSR1 (10) - User-defined signal 1
// SIGUSR2 (12) - User-defined signal 2
// SIGFPE  (8)  - Arithmetic error (divide by zero)
// SIGBUS  (7)  - Bus error (misaligned access)

// Graceful shutdown pattern:
void cleanup_handler(int sig) {
    // Save state, close files, release resources
    // Then exit cleanly
    _exit(0);  // use _exit in signal handlers (not exit)
}

// In main:
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);

// Prevent zombie children:
signal(SIGCHLD, SIG_IGN);  // auto-reap children

Self-Pipe Trick (Signal-Safe Wakeup)

The self-pipe trick solves a fundamental problem: signal handlers can't safely do complex work, but you need to respond to signals in your main loop. The solution: the handler writes a byte to a pipe, and the main loop uses select()/poll() to detect it. This integrates signals with the event loop safely. The handler only calls write() (async-signal-safe). Modern alternatives: signalfd() (Linux-specific, turns signals into file descriptors directly) or pselect() (atomically blocks signals during select). This pattern is used in event-driven servers (nginx, Redis) to handle signals without race conditions.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

int pipe_fd[2];  // [0]=read, [1]=write

void handler(int sig) {
    // Write one byte to the pipe — wakes up select()/poll()
    write(pipe_fd[1], &sig, sizeof(sig));
}

int main() {
    pipe(pipe_fd);
    // Make read end non-blocking
    fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);

    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    printf("Waiting (select-based)...\n");

    while (1) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(pipe_fd[0], &readfds);

        // select() blocks until pipe is writable (signal received)
        int ready = select(pipe_fd[0] + 1, &readfds, NULL, NULL, NULL);
        if (ready > 0 && FD_ISSET(pipe_fd[0], &readfds)) {
            int sig;
            read(pipe_fd[0], &sig, sizeof(sig));
            printf("Handled signal %d in main loop\n", sig);
            if (sig == SIGTERM) break;
        }
    }
    return 0;
}
13

Process Fork & Exec

fork() Basics

fork() creates an exact copy of the current process — the only difference is the return value: 0 in the child, child's PID in the parent. Both processes continue from the fork() call. The child gets a copy of the parent's memory (copy-on-write optimizes this). Always check all three cases: pid < 0 (error), pid == 0 (child), pid > 0 (parent). waitpid() blocks until the child exits and retrieves its status. WIFEXITED checks if it exited normally, WEXITSTATUS gets the exit code. If you don't wait(), the child becomes a zombie until reaped.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // create a child process

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // CHILD process (fork returned 0)
        printf("Child: PID=%d, Parent PID=%d\n",
               getpid(), getppid());
        sleep(2);
        printf("Child exiting\n");
        return 42;  // child exit code
    } else {
        // PARENT process (fork returned child's PID)
        printf("Parent: PID=%d, Child PID=%d\n",
               getpid(), pid);

        int status;
        waitpid(pid, &status, 0);  // wait for child

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n",
                   WEXITSTATUS(status));
        }
    }
    return 0;
}

exec Family (Replace Process Image)

exec replaces the current process image with a new program — the PID stays the same, but the code, data, and stack are replaced. exec only returns on failure. The naming convention: 'l' = list arguments (variadic, NULL-terminated), 'v' = vector/array of arguments, 'p' = search PATH for the executable, 'e' = custom environment. The first argument is conventionally the program name (argv[0]). fork()+exec() is the Unix way to launch programs — fork creates the process, exec loads the new program. This separation enables setting up file descriptors, environment, and signals between fork and exec.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: replace self with a new program
        // exec never returns on success (only on failure)

        // execlp: search PATH, list arguments
        execlp("ls", "ls", "-la", "/tmp", NULL);

        // execvp: search PATH, array of arguments
        char *args[] = {"ls", "-la", "/tmp", NULL};
        execvp("ls", args);

        // execl: full path, list arguments
        execl("/bin/ls", "ls", "-la", NULL);

        // Only reached if exec failed
        perror("exec failed");
        _exit(1);
    } else {
        wait(NULL);  // parent waits for child
        printf("Child finished\n");
    }
    return 0;
}

// exec variants:
// execl  (path, arg1, arg2, ..., NULL)        — list args, full path
// execlp (file, arg1, arg2, ..., NULL)        — list args, search PATH
// execv  (path, argv[])                        — array args, full path
// execvp (file, argv[])                        — array args, search PATH
// execve (path, argv[], envp[])                — array args, custom env

Zombie & Orphan Processes

Zombies occur when a child exits but the parent hasn't called wait() — the kernel keeps the process table entry (PID, exit status) until reaped. Zombies waste PIDs and can exhaust the process table. Fix: always wait() for children, or set SIGCHLD to SIG_IGN (kernel auto-reaps). Orphans occur when the parent exits before the child — init/systemd (PID 1) adopts the orphan and reaps it when it exits. The double-fork pattern (fork, child forks again, first child exits) creates a daemon that's automatically reparented to init, detaching from the terminal. Monitor zombies with 'ps aux | grep Z' or 'top'.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child PID=%d\n", getpid());
        _exit(0);  // child exits immediately
    }

    // If parent doesn't wait(), child becomes a ZOMBIE
    // (process table entry remains until reaped)
    sleep(5);  // parent sleeps — child is now a zombie
    // Run 'ps' during this window to see the zombie (state 'Z')

    // Reap the zombie:
    int status;
    waitpid(pid, &status, 0);
    printf("Zombie reaped\n");

    // Orphan: if parent exits before child
    pid_t pid2 = fork();
    if (pid2 == 0) {
        sleep(3);  // parent will exit first
        printf("Orphan adopted by init (PID 1), new parent=%d\n",
               getppid());
        _exit(0);
    }
    // Parent exits immediately — child becomes orphan
    // init/systemd (PID 1) adopts and reaps it

    // Prevent zombies: ignore SIGCHLD
    // signal(SIGCHLD, SIG_IGN);  // kernel auto-reaps children
    // Or use SA_NOCLDWAIT with sigaction

    return 0;
}

Daemon Process Creation

Daemons are background processes that run without a terminal (e.g., web servers, databases). The daemonization steps: fork+exit to detach from the shell, setsid() to create a new session (no controlling terminal), fork again for safety, chdir('/') to avoid holding a filesystem, set umask for predictable file permissions, and close/redirect stdio to /dev/null. The double-fork is a Unix convention to prevent the daemon from reacquiring a terminal via open(). Modern systems provide systemd service files for daemon management, but understanding manual daemonization is still important for embedded systems and portable code. Log to files (not stdout) since stdout is /dev/null.

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize() {
    // 1. Fork and exit parent (child continues in background)
    pid_t pid = fork();
    if (pid > 0) exit(0);  // parent exits
    if (pid < 0) exit(1);

    // 2. Create new session (detach from controlling terminal)
    setsid();

    // 3. Fork again (prevent reacquiring a terminal)
    pid = fork();
    if (pid > 0) exit(0);
    if (pid < 0) exit(1);

    // 4. Change working directory to / (don't hold filesystem)
    chdir("/");

    // 5. Set umask to 0 (full control over file permissions)
    umask(0);

    // 6. Close standard file descriptors (detach from terminal)
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 7. Redirect them to /dev/null (in case code writes to them)
    open("/dev/null", O_RDWR);  // fd 0 = stdin
    dup(0);                      // fd 1 = stdout
    dup(0);                      // fd 2 = stderr
}

int main() {
    daemonize();
    // Now running as a daemon (background, no terminal)
    while (1) {
        // Daemon work here (e.g., log to file, listen on socket)
        sleep(60);
    }
    return 0;
}

Inter-Process Communication (IPC) Overview

IPC lets processes communicate. Pipes are simplest (parent-child, unidirectional). Named pipes (FIFOs) work between unrelated processes via a filesystem path. Shared memory is fastest (zero-copy) but requires synchronization (semaphores/mutexes). Sockets are the most flexible (bidirectional, network-capable). Message queues provide structured, message-boundary communication. Signals are minimal (just a number). Choose based on your needs: pipes for simple parent-child, shared memory for high-performance data sharing, sockets for network communication. System V IPC (shmget, semget) is older; POSIX IPC (shm_open, sem_open) is cleaner but less universally available.

c
#include <stdio.h>
// C provides several IPC mechanisms:

// 1. PIPES: unidirectional byte stream between parent/child
//    pipe(fd) creates fd[0]=read, fd[1]=write
//    Only works between related processes (fork)

// 2. NAMED PIPES (FIFOs): like pipes but have a filesystem path
//    mkfifo("/tmp/myfifo", 0666);
//    Works between unrelated processes

// 3. SHARED MEMORY: fastest IPC (both processes access same RAM)
//    shmget/shmat (System V) or shm_open/mmap (POSIX)

// 4. MESSAGE QUEUES: structured messages (not byte stream)
//    msgget/msgsnd/msgrcv (System V) or mq_open (POSIX)

// 5. SEMAPHORES: synchronization (not data transfer)
//    semget/semop (System V) or sem_open (POSIX)

// 6. SOCKETS: bidirectional, works across machines (network)
//    socket/bind/listen/accept/connect

// 7. SIGNALS: minimal data (just signal number)
//    kill(pid, SIGUSR1)

// Choosing IPC:
// - Same machine, related processes → pipes
// - Same machine, unrelated processes → named pipes, shared memory
// - Different machines → sockets
// - Need synchronization → semaphores, mutexes
// - Need structured messages → message queues
14

Pipes & IPC

Anonymous Pipes (Parent-Child)

Pipes provide unidirectional communication between related processes (created by fork). pipe(fd) creates two file descriptors: fd[0] for reading, fd[1] for writing. Critical: close the unused end in each process — the parent closes the read end, the child closes the write end. If the write end isn't closed, the child's read() blocks forever (waiting for more data). read() returns 0 (EOF) only when all write ends are closed. Pipes have a fixed buffer (typically 64KB) — write() blocks if the buffer is full. Pipes are ideal for parent-child communication and piping shell commands (ls | grep).

c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];  // fd[0]=read end, fd[1]=write end
    pipe(fd);   // create pipe

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: read from pipe
        close(fd[1]);  // close unused write end

        char buf[256];
        int n = read(fd[0], buf, sizeof(buf));
        buf[n] = '\0';
        printf("Child received: %s", buf);

        close(fd[0]);
    } else {
        // PARENT: write to pipe
        close(fd[0]);  // close unused read end

        const char *msg = "Hello from parent!\n";
        write(fd[1], msg, strlen(msg));

        close(fd[1]);  // close write end → child's read returns 0 (EOF)
        wait(NULL);
    }
    return 0;
}

Named Pipes (FIFOs)

Named pipes (FIFOs) are pipes with a filesystem name — they work between unrelated processes. mkfifo() creates the pipe file; open() blocks until both a reader and writer are connected (synchronization built-in). FIFOs persist until unlink()'d (unlike anonymous pipes which vanish when processes exit). They're useful for simple IPC between separate programs. The blocking behavior on open() ensures the writer doesn't start until a reader is ready. Use O_NONBLOCK for non-blocking opens. FIFOs are unidirectional — for bidirectional communication, use two FIFOs or sockets. Named pipes are commonly used in shell scripts and system services.

c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

// Process A (writer):
int writer_main() {
    mkfifo("/tmp/myfifo", 0666);  // create named pipe

    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello via FIFO!", 15);
    close(fd);
    return 0;
}

// Process B (reader) — can be a completely separate program:
int reader_main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[256];
    int n = read(fd, buf, sizeof(buf));
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}

// Named pipes persist in the filesystem (use unlink to remove):
// unlink("/tmp/myfifo");

// open() blocks until BOTH a reader and writer are connected
// (unless O_NONBLOCK is used)

Shared Memory (Fastest IPC)

Shared memory is the fastest IPC — both processes map the same physical RAM, so data transfer is zero-copy. shmget() creates a segment, shmat() attaches it to the process's address space, shmdt() detaches, shmctl(IPC_RMID) destroys. The critical caveat: shared memory provides NO synchronization — if both processes access it simultaneously, you get data races. You MUST use semaphores, mutexes, or other synchronization to coordinate access. ftok() generates a key from a file path (both processes must agree on the key). Always destroy shared memory when done (it persists after processes exit, leaking memory). POSIX shared memory (shm_open/mmap) is a cleaner alternative.

c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp/shmfile", 65);  // generate unique key

    // Create shared memory segment
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: attach and read
        char *shared = (char *)shmat(shmid, NULL, 0);
        sleep(1);  // wait for parent to write
        printf("Child reads: %s\n", shared);
        shmdt(shared);  // detach
    } else {
        // PARENT: attach and write
        char *shared = (char *)shmat(shmid, NULL, 0);
        strcpy(shared, "Hello from shared memory!");
        printf("Parent wrote to shared memory\n");
        shmdt(shared);
        wait(NULL);

        // Destroy shared memory after use
        shmctl(shmid, IPC_RMID, NULL);
    }
    return 0;
}

// WARNING: shared memory has NO synchronization!
// Use semaphores or mutexes to prevent race conditions.

dup2 & Redirection

dup2(oldfd, newfd) makes newfd a copy of oldfd — this is how shell redirection works. To redirect stdout to a pipe: dup2(pipe_write, STDOUT_FILENO) — now printf/write to stdout goes into the pipe. To redirect stdin from a pipe: dup2(pipe_read, STDIN_FILENO) — now scanf/read from stdin comes from the pipe. This is exactly how the shell implements pipes (ls | sort), redirection (ls > file), and input (sort < file). After dup2, close the original fd (it's been duplicated). This pattern is fundamental to building Unix pipelines programmatically and is used by shells, popen(), and process management libraries.

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

// Implement shell-like pipeline: ls | sort
int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // First child: ls (writes to pipe instead of stdout)
        close(pipefd[0]);                    // close read end
        dup2(pipefd[1], STDOUT_FILENO);      // stdout → pipe write
        close(pipefd[1]);                    // close original (dup'd)
        execlp("ls", "ls", NULL);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // Second child: sort (reads from pipe instead of stdin)
        close(pipefd[1]);                    // close write end
        dup2(pipefd[0], STDIN_FILENO);       // stdin → pipe read
        close(pipefd[0]);                    // close original (dup'd)
        execlp("sort", "sort", NULL);
    }

    // Parent: close both ends and wait
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

popen (High-Level Pipe)

popen() is a high-level wrapper around fork+pipe+exec+shell — it runs a command via /bin/sh and returns a FILE* for reading output ('r') or writing input ('w'). It's much simpler than manual fork/pipe/exec but runs through a shell, so NEVER pass untrusted input (shell injection risk). Use fgets/fprintf on the returned FILE* like a regular file. pclose() closes the pipe and waits for the child to exit (returns its status). For untrusted input, use fork+execvp directly (no shell). popen is perfect for quick scripts, system administration tools, and reading command output. For bidirectional communication, use socketpair() or two pipes.

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

int main() {
    // popen opens a process with a pipe (like shell command | ...)
    FILE *fp = popen("ls -la /tmp", "r");
    if (fp == NULL) {
        perror("popen failed");
        return 1;
    }

    // Read command output line by line
    char buf[256];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf(">> %s", buf);
    }

    pclose(fp);  // closes pipe and waits for child

    // Writing to a process (like ... | command):
    FILE *wp = popen("grep hello", "w");
    fprintf(wp, "hello world\n");
    fprintf(wp, "goodbye\n");
    pclose(wp);  // grep outputs "hello world"

    return 0;
}

// popen is simpler than fork+pipe+exec but:
// - Runs via /bin/sh (shell injection risk with user input!)
// - Less control over the child process
// - Use fork+exec directly for untrusted input
15

Makefile & Build Tools

Basic Makefile Structure

Make automates compilation. A Makefile has rules: target (file to build), prerequisites (dependencies), and recipe (shell commands, TAB-indented). Variables (CC, CFLAGS) centralize configuration. Automatic variables: $@ (target name), $< (first prerequisite), $^ (all prerequisites). Pattern rules (%.o: %.c) generalize compilation for all source files. .PHONY declares targets that aren't files (clean, all, install). The first rule is the default (make with no args builds 'all'). Make tracks file timestamps — it only rebuilds if a prerequisite is newer than the target. This incremental building saves time on large projects.

c
# Makefile — build automation for C/C++ projects
# Rule syntax: target: prerequisites
#               recipe (must start with TAB, not spaces)

# Variables
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
TARGET = myapp
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)  # substitute .c with .o

# Default target (first rule)
all: $(TARGET)

# Link object files into executable
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# Compile each .c to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Clean build artifacts
clean:
	rm -f $(OBJS) $(TARGET)

# Run the program
run: $(TARGET)
	./$(TARGET)

# Phony targets (not files)
.PHONY: all clean run

# Usage:
# $ make        — builds 'all' (the default)
# $ make clean  — removes build files
# $ make run    — builds and runs
# $ make -j4    — parallel build (4 jobs)

Automatic Variables & Pattern Rules

Automatic variables make Makefiles concise and maintainable. $@ (target), $< (first prerequisite), and $^ (all prerequisites) are the most common. Pattern rules (%.o: %.c) let you write one rule for all source files — the % matches any string. $(wildcard) finds files matching a glob, $(patsubst) transforms strings — together they auto-discover sources. The @ prefix suppresses echoing the command. Static pattern rules (target: %.o: %.c) apply to a specific list. Understanding these features eliminates repetitive rules and makes Makefiles scale to large projects. Always use TAB (not spaces) for recipe indentation — Make is strict about this.

c
# Automatic variables in recipes:
# $@   — the target filename
# $<   — the first prerequisite
# $^   — all prerequisites (no duplicates)
# $+   — all prerequisites (with duplicates)
# $?   — prerequisites newer than the target
# $*   — the stem (matching % part)

# Example showing all automatic variables:
program: main.o utils.o
	@echo "Target: $@"        # program
	@echo "First dep: $<"     # main.o
	@echo "All deps: $^"      # main.o utils.o
	@echo "Newer deps: $?"    # (whichever changed)
	gcc -o $@ $^

# Pattern rule: compile any .c to .o
%.o: %.c
	gcc -c $< -o $@
# $< = source (.c file), $@ = target (.o file)

# Static pattern rule (specific files):
$(OBJS): %.o: %.c
	gcc -c $< -o $@

# Built-in functions:
SRCS = $(wildcard src/*.c)           # find all .c files
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))  # path substitution
DIRS = $(sort $(dir $(SRCS)))         # unique directories

Dependencies & Header Files

Tracking header dependencies is crucial — without it, changing a .h file doesn't trigger recompilation of .c files that include it, leading to stale builds. The solution: gcc -MMD -MP generates .d files listing all dependencies (including headers). -include pulls these into the Makefile. -MP adds phony targets for headers (prevents errors if a header is deleted). This is the standard approach for C/C++ projects. Without this, you'd have to manually list every header dependency — unmanageable for large projects. The first build won't have .d files (the '-' in -include suppresses the error); they're created during compilation and used on subsequent builds.

c
# When a header file changes, dependent .c files must recompile
# Make doesn't track this automatically — use gcc -MMD

CC = gcc
CFLAGS = -Wall -MMD -MP  # generate .d dependency files

SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)    # dependency files

all: myapp

myapp: $(OBJS)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Include auto-generated dependency files
-include $(DEPS)  # '-' means don't error if missing

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

.PHONY: all clean

# How it works:
# 1. gcc -MMD creates main.d next to main.o
# 2. main.d contains: main.o: main.c utils.h parser.h
# 3. -include pulls these in, so Make knows header dependencies
# 4. If utils.h changes, main.o rebuilds automatically

Multi-Directory Project Makefile

Real projects span multiple directories. This Makefile auto-discovers sources (wildcard), maps them to a build directory (patsubst), and creates directories as needed. The | syntax creates order-only prerequisites — $(BUILDDIR) is created before compilation, but its timestamp change doesn't trigger rebuilds (without |, creating the directory would make everything rebuild every time). -Iinclude tells gcc where to find headers. -MMD generates dependency files in the build directory. This structure keeps source, build, and binary directories separate — easy to clean (rm -rf build) and doesn't pollute the source tree. For very large projects, consider CMake or Meson.

c
# Project structure:
# project/
#   src/       — source files
#   include/   — header files
#   build/     — object files (created by make)
#   bin/       — final executable

CC = gcc
CFLAGS = -Wall -Iinclude -g
SRCDIR = src
INCDIR = include
BUILDDIR = build
BINDIR = bin

TARGET = $(BINDIR)/myapp
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) -o $@ $^

# Order-only prerequisite: create build dir before compiling
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -MMD -c $< -o $@

$(BUILDDIR):
	mkdir -p $(BUILDDIR)

$(BINDIR):
	mkdir -p $(BINDIR)

-include $(DEPS)

clean:
	rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

# Order-only prerequisites (| syntax) create directories
# without triggering rebuilds when the dir timestamp changes

CMake Basics (Alternative to Make)

CMake is a meta-build system — it generates Makefiles (or Ninja, Visual Studio, Xcode projects) from a CMakeLists.txt file. It's the de facto standard for C/C++ projects because it handles cross-platform compilation, dependency detection, and IDE integration. Key commands: project() sets the project name, add_executable() defines a build target, target_include_directories() adds header paths, target_link_libraries() links libraries. Out-of-source builds (mkdir build && cd build && cmake ..) keep the source tree clean. CMake auto-detects compilers and flags per platform. For new C/C++ projects, prefer CMake over raw Makefiles — it's more maintainable and portable.

c
# CMakeLists.txt — CMake is a cross-platform build generator
# It generates Makefiles (or Ninja, VS, Xcode projects)

cmake_minimum_required(VERSION 3.10)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

# Add executable from source files
add_executable(myapp src/main.c src/utils.c src/parser.c)

# Include directory
target_include_directories(myapp PRIVATE include)

# Link a library
target_link_libraries(myapp m)  # math library (-lm)

# Build type flags
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")

# Usage:
# $ mkdir build && cd build
# $ cmake ..            # generate Makefiles
# $ make                # build
# $ make install        # install (optional)
# $ cmake .. -DCMAKE_BUILD_TYPE=Debug  # debug build

# Out-of-source builds keep source tree clean
# CMake is the standard for C/C++ cross-platform projects
16

Function Pointers Deep Dive

Function Pointer Syntax & Usage

Function pointers store the address of a function, enabling runtime dispatch. The syntax int (*fp)(int, int) is notoriously confusing — read it as 'fp is a pointer to a function taking (int, int) returning int'. typedef simplifies this: typedef int (*math_func)(int, int) creates a readable alias. Function names decay to pointers (like array names), so 'add' and '&add' are equivalent. Function pointers enable callbacks, event handlers, strategy patterns, and dispatch tables (arrays of function pointers for switch-like dispatch). They're the foundation of qsort's comparator and GUI event systems.

c
#include <stdio.h>

// Function pointer syntax: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // Declare a function pointer
    int (*operation)(int, int);

    // Assign (function name decays to pointer)
    operation = add;          // or &add
    printf("5 + 3 = %d\n", operation(5, 3));  // 8

    operation = subtract;
    printf("5 - 3 = %d\n", operation(5, 3));  // 2

    // typedef for readability
    typedef int (*math_func)(int, int);
    math_func fn = add;
    printf("Result: %d\n", fn(10, 20));

    // Array of function pointers (dispatch table)
    math_func ops[] = {add, subtract};
    printf("op[0](4,2)=%d  op[1](4,2)=%d\n",
           ops[0](4, 2), ops[1](4, 2));

    return 0;
}

Callbacks (qsort Example)

qsort is the classic example of function pointers as callbacks. The comparator receives const void* pointers (generic) and returns an integer indicating order. qsort calls your comparator to decide element ordering — you control the sort behavior by passing different functions. This is the strategy pattern in C: the algorithm (qsort) is fixed, but the comparison logic is injected. void* enables generic programming (sort any type). The comparator must be a pure function (no side effects) and consistent (if a<b and b<c then a<c). This pattern is used throughout the C standard library (bsearch, atexit, signal).

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

// Comparator function for qsort
// Returns: negative if a<b, 0 if equal, positive if a>b
int compare_asc(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int compare_desc(const void *a, const void *b) {
    return (*(int *)b - *(int *)a);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer as the comparator
    qsort(arr, n, sizeof(int), compare_asc);
    printf("Ascending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    // Same array, different comparator (descending)
    qsort(arr, n, sizeof(int), compare_desc);
    printf("Descending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

Structs with Function Pointers (OOP in C)

C can simulate OOP using structs with function pointers — this is how C++ vtables work internally. The struct contains a pointer to a 'vtable' (virtual function table) holding function pointers. Each 'subclass' (Circle, Square) has its own vtable with its implementations. Casting a Circle* to Shape* enables polymorphism — print_shape() calls the right area() function through the vtable. This pattern is used in real C code: Linux kernel (device drivers), GObject (GTK), and SQLite. It provides encapsulation, inheritance (via struct embedding), and polymorphism. While verbose compared to C++, it gives full control over memory layout and virtual dispatch.

c
#include <stdio.h>

// Simulating OOP with structs + function pointers
typedef struct Shape Shape;

// Virtual function table (vtable)
typedef struct {
    double (*area)(Shape *);
    double (*perimeter)(Shape *);
    void (*describe)(Shape *);
} ShapeVTable;

struct Shape {
    const ShapeVTable *vtable;  // pointer to virtual functions
    char name[32];
};

// Circle implementation
typedef struct {
    Shape base;      // inherit from Shape
    double radius;
} Circle;

double circle_area(Shape *s) {
    return 3.14159 * ((Circle *)s)->radius * ((Circle *)s)->radius;
}

double circle_perimeter(Shape *s) {
    return 2 * 3.14159 * ((Circle *)s)->radius;
}

static const ShapeVTable circle_vtable = {
    circle_area, circle_perimeter, NULL
};

Circle *circle_create(double r) {
    Circle *c = malloc(sizeof(Circle));
    c->base.vtable = &circle_vtable;
    strcpy(c->base.name, "Circle");
    c->radius = r;
    return c;
}

// Polymorphic function (works with any Shape)
void print_shape(Shape *s) {
    printf("%s: area=%.2f, perimeter=%.2f\n",
           s->name, s->vtable->area(s), s->vtable->perimeter(s));
}

Event-Driven Programming with Callbacks

Function pointers enable event-driven architecture in C — the publish/subscribe pattern. Handlers register via on_event() (subscribe), and emit_event() calls all registered handlers (publish). This decouples event producers from consumers — the emitter doesn't know what the handlers do. This pattern is fundamental to GUI frameworks (button click → handler), game engines (collision → callback), and async I/O (data ready → read handler). The handler signature (event name + void* data) is generic enough for any event type. In production, add error handling (what if a handler crashes?), priority ordering, and the ability to unsubscribe. This is how libuv, libevent, and Node.js work under the hood.

c
#include <stdio.h>

// Event system using function pointers
typedef void (*EventHandler)(const char *event, void *data);

// Simple event emitter
#define MAX_HANDLERS 10
static EventHandler handlers[MAX_HANDLERS];
static int handler_count = 0;

void on_event(EventHandler handler) {
    if (handler_count < MAX_HANDLERS) {
        handlers[handler_count++] = handler;
    }
}

void emit_event(const char *event, void *data) {
    for (int i = 0; i < handler_count; i++) {
        handlers[i](event, data);  // call each registered handler
    }
}

// Concrete handlers
void log_handler(const char *event, void *data) {
    printf("[LOG] Event: %s\n", event);
}

void alert_handler(const char *event, void *data) {
    if (strcmp(event, "error") == 0) {
        printf("[ALERT] Error occurred!\n");
    }
}

int main() {
    // Register handlers (subscribe)
    on_event(log_handler);
    on_event(alert_handler);

    // Emit events (publish)
    emit_event("click", NULL);
    emit_event("error", NULL);
    emit_event("scroll", NULL);

    return 0;
}

Function Pointer Pitfalls

Function pointers have several pitfalls. Calling a NULL function pointer crashes (segfault) — always check for NULL before calling. Casting to the wrong signature is undefined behavior (the calling convention may differ). Comparing function pointers for equality is valid (same function), but ordering (<, >) is undefined. Use typedef consistently — function pointer syntax is error-prone, and typedefs make declarations readable and maintainable. In C, function pointers are the only way to achieve runtime polymorphism and callbacks, so mastering them is essential. C++ adds std::function, lambdas, and virtual functions as safer alternatives.

c
#include <stdio.h>

// PITFALL 1: Calling a NULL function pointer (crash!)
void bad_call() {
    void (*fp)(void) = NULL;
    fp();  // SEGFAULT — always check for NULL
    if (fp) fp();  // safe
}

// PITFALL 2: Wrong signature (undefined behavior)
void takes_int(int x) { printf("%d\n", x); }
void wrong_sig() {
    void (*fp)(void) = (void (*)(void))takes_int;  // WRONG cast
    fp();  // UB: missing argument, garbage value
}

// PITFALL 3: Function pointer to a local function (dangling)
typedef int (*callback_t)(int);
callback_t get_callback() {
    // Returning pointer to local function is OK (functions aren't local)
    // But returning a pointer to a local VARIABLE is not
    return NULL;  // functions have static storage, safe to return
}

// PITFALL 4: Comparing function pointers
int f1(int x) { return x; }
int f2(int x) { return x; }
void compare_fps() {
    int (*p1)(int) = f1;
    int (*p2)(int) = f1;
    if (p1 == p2) printf("Same function\n");  // OK
    // Comparing p1 == f2 is valid but they're different functions
}

// GOOD: Always use typedef for complex function pointers
// typedef int (*comparator_t)(const void *, const void *);
// This makes declarations readable and consistent
17

Variable Arguments (varargs)

Basic Variadic Functions (stdarg)

Variadic functions accept a variable number of arguments using stdarg.h. va_list holds the argument list, va_start initializes it (requires the last named parameter before ...), va_arg retrieves the next argument with a specified type, va_end cleans up. The function must know how many arguments to read — either via a count parameter (like printf's format string) or a sentinel value (NULL terminator). The '...' must always be the last parameter. va_arg doesn't type-check — passing the wrong type is undefined behavior. This is how printf, fprintf, and execl work.

c
#include <stdio.h>
#include <stdarg.h>

// Variadic function: takes variable number of arguments
// The '...' must be the LAST parameter
int sum(int count, ...) {
    va_list args;           // argument list type
    va_start(args, count);  // initialize (needs last named param)

    int total = 0;
    for (int i = 0; i < count; i++) {
        int val = va_arg(args, int);  // get next argument (as int)
        total += val;
    }

    va_end(args);  // cleanup
    return total;
}

int main() {
    printf("%d\n", sum(3, 10, 20, 30));      // 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));   // 15
    printf("%d\n", sum(0));                    // 0
    return 0;
}

// The 'count' parameter tells the function how many args follow.
// Without it, the function can't know when to stop.

Implementing a Custom printf

vprintf/vfprintf/vsprintf are variadic helpers that take a va_list instead of ... — they let you build custom printf-like functions. The log_msg example wraps printf with a log level prefix. The print_values example shows how to handle mixed types: pass a type tag before each value, then switch on the tag to call va_arg with the right type. This is necessary because va_arg requires the exact type — there's no runtime type information. The type-tag pattern is used in polymorphic C APIs (e.g., SQLite's bind functions). Always match va_arg types exactly — int vs long, float vs double (floats are promoted to double in varargs).

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Custom logging function with format string
void log_msg(const char *level, const char *format, ...) {
    printf("[%s] ", level);

    va_list args;
    va_start(args, format);

    // vprintf: like printf but takes va_list instead of ...
    vprintf(format, args);

    va_end(args);
    printf("\n");
}

// Variadic function with mixed types
void print_values(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        int type = va_arg(args, int);  // type tag
        switch (type) {
            case 0:  // int
                printf("int: %d\n", va_arg(args, int));
                break;
            case 1:  // double
                printf("double: %f\n", va_arg(args, double));
                break;
            case 2:  // string
                printf("string: %s\n", va_arg(args, char *));
                break;
        }
    }
    va_end(args);
}

int main() {
    log_msg("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
    log_msg("ERROR", "Failed to open %s (code %d)", "config.txt", 13);

    print_values(2, 0, 42, 2, "hello", 1, 3.14);
    return 0;
}

Sentinel-Terminated Variadic Functions

Sentinel-terminated variadic functions use a special value (usually NULL) to mark the end of arguments, instead of a count. This is cleaner for string-heavy APIs — the caller doesn't need to count arguments. The exec family (execl, execlp) uses NULL as the sentinel. The downside: if the caller forgets the NULL, the function reads garbage memory (undefined behavior). Some compilers (GCC) support __attribute__((sentinel)) to warn about missing sentinels. Always document that NULL is required. The buffer size parameter prevents buffer overflows — always pass the destination size and check bounds before strcat.

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Sentinel-terminated: last argument is NULL
void concat_strings(char *dest, size_t size, ...) {
    va_list args;
    va_start(args, size);

    dest[0] = '\0';  // start with empty string
    size_t used = 0;

    while (1) {
        char *s = va_arg(args, char *);
        if (s == NULL) break;  // sentinel — stop reading

        size_t len = strlen(s);
        if (used + len < size) {
            strcat(dest, s);
            used += len;
        }
    }

    va_end(args);
}

int main() {
    char result[256];
    concat_strings(result, sizeof(result),
                   "Hello, ", "world", "! ", "How are you?", NULL);
    printf("%s\n", result);
    // Hello, world! How are you?

    // Common C APIs using sentinels:
    // execl("/bin/ls", "ls", "-l", NULL);  // exec family
    // sqlite3_exec(db, sql, callback, NULL, NULL);
    return 0;
}

Forwarding Variadic Arguments

Forwarding variadic arguments requires va_copy (not assignment) — va_list may be an opaque type that can't be copied with =. va_copy lets you traverse the argument list multiple times (e.g., first to measure, then to print). The LOG macro uses __VA_ARGS__ to forward all arguments to fprintf. The ##__VA_ARGS__ GCC extension removes the preceding comma when no variadic args are provided (so LOG("msg") works without trailing comma). This pattern is ubiquitous in C logging macros. C99 requires at least one argument before ...; C11/C23 and GCC allow zero. For type-safe alternatives in C++, use variadic templates or std::format.

c
#include <stdio.h>
#include <stdarg.h>

// Wrapper that forwards varargs to another function
// Use va_copy for the copy (needed for multiple passes)
void custom_printf(const char *format, ...) {
    va_list args1, args2;
    va_start(args1, format);
    va_copy(args2, args1);  // copy for second use

    // First pass: count characters that would be printed
    int len = vsnprintf(NULL, 0, format, args1);
    printf("[len=%d] ", len);

    // Second pass: actually print
    vprintf(format, args2);

    va_end(args1);
    va_end(args2);
}

// Macro forwarding (common pattern for logging)
#define LOG(fmt, ...) \
    fprintf(stderr, "[LOG] %s:%d: " fmt "\n", \
            __FILE__, __LINE__, ##__VA_ARGS__)

// The ## operator removes the comma if __VA_ARGS__ is empty
// (GCC extension, widely supported)

int main() {
    custom_printf("Value: %d, Name: %s\n", 42, "test");
    LOG("Simple message");           // no extra args
    LOG("With value: %d", 100);      // with args
    return 0;
}

Variadic Macros (C99)

C99 variadic macros use __VA_ARGS__ to capture all arguments matching the ... in the macro definition. ##__VA_ARGS__ (GCC extension, now standard in C20) removes the comma when no variadic args are passed. The COUNT macro uses a clever trick: it maps N arguments to N, 5, 4, 3, 2, 1 and the Nth position gives the count. The do { ... } while (0) idiom in ASSERT makes the macro behave like a single statement (safe in if/else without braces). #stringifies macro arguments. Variadic macros are essential for logging, debugging, and generic programming in C. They're the foundation of many library APIs and the Linux kernel's logging system.

c
#include <stdio.h>

// C99 variadic macros: __VA_ARGS__ captures all extra args
#define DEBUG_PRINT(fmt, ...) \
    printf("DEBUG: " fmt "\n", ##__VA_ARGS__)

#define MAX(...) (max_of(__VA_ARGS__))

// Assert macro with message
#define ASSERT(cond, fmt, ...) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n" fmt "\n", \
                    #cond, ##__VA_ARGS__); \
            exit(1); \
        } \
    } while (0)

// Count arguments (GCC __VA_OPT__ or recursive macros)
#define COUNT(...) COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)
#define COUNT_N(_1, _2, _3, _4, _5, N, ...) N

// Stringification of all args
#define STR(...) #__VA_ARGS__

int main() {
    DEBUG_PRINT("x = %d", 42);
    DEBUG_PRINT("no args");  // ## removes comma

    ASSERT(x > 0, "x was %d", x);

    printf("Count: %d\n", COUNT(a, b, c));  // 3
    printf("Stringified: %s\n", STR(hello, world));  // "hello, world"

    return 0;
}
18

Bit Manipulation Tricks

Common Bit Tricks

Bit manipulation operates directly on binary representations. n & 1 checks the least significant bit for even/odd. Left shift (<<) multiplies by 2; right shift (>>) divides. XOR swap avoids a temporary variable but is less readable. n & (n-1) clears the lowest set bit, useful for power-of-2 checks and popcount. These tricks are fast but prioritize readability in application code.

c
// Check if odd
int is_odd(int n) { return n & 1; }

// Multiply/divide by powers of 2
int doubled = x << 1;    // x * 2
int halved = x >> 1;     // x / 2
int times8 = x << 3;     // x * 8

// Swap without temp variable
void swap(int *a, int *b) {
    *a ^= *b; *b ^= *a; *a ^= *b;
}

// Check if power of 2
int is_pow2(int n) { return n > 0 && (n & (n-1)) == 0; }

// Count set bits (population count)
int popcount(unsigned int n) {
    int count = 0;
    while (n) { n &= (n-1); count++; }
    return count;
}

Bit Flags & Masks

Bit flags pack multiple boolean options into a single integer, saving memory. Each flag is a power of 2 (one bit). OR (|) sets flags, AND (&) checks flags, XOR (^) toggles, AND NOT (&= ~) clears. This pattern is ubiquitous in system programming: file permissions (O_RDONLY, O_CREAT), socket options, and GPU state. Use named constants for readability.

c
#define FLAG_READ    (1 << 0)  // 0x01
#define FLAG_WRITE   (1 << 1)  // 0x02
#define FLAG_EXECUTE (1 << 2)  // 0x04

// Set flags
unsigned int perms = FLAG_READ | FLAG_WRITE;

// Check if flag is set
if (perms & FLAG_WRITE) { /* write allowed */ }

// Toggle a flag
perms ^= FLAG_EXECUTE;

// Clear a flag
perms &= ~FLAG_APPEND;

// Check if ALL flags in mask are set
int has_all = (perms & (FLAG_READ|FLAG_WRITE))
            == (FLAG_READ|FLAG_WRITE);

Bit Fields in Structs

Bit fields pack small values into a minimal number of bits within structs. The colon syntax specifies the bit width. This saves memory for data structures with many small fields (dates, flags, hardware registers). However, bit field layout is implementation-dependent: byte order, padding, and alignment vary across compilers. Avoid bit fields for portable data formats; use explicit bit masks instead.

c
// Pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31 (5 bits)
    unsigned int month : 4;   // 0-15 (4 bits)
    unsigned int year  : 23;  // 0-8M (23 bits)
};  // Total: 32 bits = 4 bytes

struct Date today = { 21, 6, 2025 };
printf("Size: %zu bytes\n", sizeof(today));  // 4

today.day = 15;
if (today.month == 12) {
    today.year++;
    today.month = 1;
}

Endianness Conversion

Endianness determines byte order: little-endian (x86, ARM default) stores LSB first; big-endian (network, some MIPS) stores MSB first. Network protocols use big-endian (network byte order). Use htonl/ntohl for portable network code. Manual byte swaps with shifts and masks work on any platform. Detect endianness at compile time with __BYTE_ORDER__ for optimized code paths.

c
#include <arpa/inet.h>  // htonl, ntohl

// Host to network byte order (big-endian) and back
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val);
uint32_t back = ntohl(net_val);

// Manual byte swap (portable)
uint32_t swap32(uint32_t v) {
    return ((v & 0xFF000000) >> 24) |
           ((v & 0x00FF0000) >> 8)  |
           ((v & 0x0000FF00) << 8)  |
           ((v & 0x000000FF) << 24);
}

// Detect endianness at compile time
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    printf("Little-endian system\n");
#endif

Bitwise Hacks

Branchless bit hacks avoid conditional jumps for performance in tight loops. The abs trick uses arithmetic right shift to create a mask. next_pow2 fills all bits below the highest set bit, then adds 1. Bit reversal uses divide-and-conquer: swap nibbles, then pairs, then single bits. These are useful in cryptography, hashing, and DSP. Modern CPUs often have built-in instructions (POPCNT, LZCNT) that are faster.

c
// Absolute value without branching
int abs_val(int n) {
    int mask = n >> (sizeof(int)*8 - 1);
    return (n ^ mask) - mask;
}

// Round up to next power of 2
unsigned int next_pow2(unsigned int n) {
    n--;
    n |= n >> 1;  n |= n >> 2;
    n |= n >> 4;  n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

// Reverse bits in a byte
uint8_t reverse_byte(uint8_t b) {
    b = (b >> 4) | (b << 4);
    b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2);
    b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
    return b;
}
19

Signal Handling Advanced

Signal Sets & Blocking

sigprocmask blocks signals so they are queued (not lost) and delivered later. This protects critical sections from interruption. SIG_BLOCK adds to the mask, SIG_UNBLOCK removes, SIG_SETMASK replaces. Use sigpending to check queued signals. Only block signals briefly; long blocking can miss important events. Signal masks are per-process and inherited across fork.

c
#include <signal.h>

sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

// Block signals (they will be queued)
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// Critical section - signals are deferred
do_critical_work();

// Unblock - pending signals are now delivered
sigprocmask(SIG_SETMASK, &oldmask, NULL);

// Check for pending signals
sigpending(&mask);
if (sigismember(&mask, SIGINT)) {
    printf("SIGINT is pending\n");
}

Safe Signal Handlers

Signal handlers must be async-signal-safe: only use reentrant functions (write, _exit, signal). Avoid printf, malloc, and most library functions—they may be interrupted mid-operation and corrupt state. Use volatile sig_atomic_t for handler-set flags. SA_RESTART automatically restarts interrupted system calls. sigaction is preferred over signal for portable, well-defined behavior.

c
#include <signal.h>
#include <unistd.h>

// Only safe type in handlers
volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    // ONLY use async-signal-safe functions!
    // write() is safe; printf() is NOT
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg)-1);
    got_signal = 1;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // Restart interrupted syscalls
    sigaction(SIGINT, &sa, NULL);

    while (!got_signal) pause();
    return 0;
}

Self-Pipe & signalfd

signalfd (Linux) converts signals to file descriptors, integrating them into event loops (epoll, select). Block the signal first, then create the signalfd. The self-pipe trick is portable: the handler writes a byte to a pipe, and the main loop reads it. Both approaches move signal handling out of restricted handler context into normal code, where you can safely call any function.

c
#include <sys/signalfd.h>

// signalfd: handle signals as file descriptors (Linux)
int setup_signalfd() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);  // Must block first

    int fd = signalfd(-1, &mask, SFD_CLOEXEC);
    return fd;
}

// In event loop:
struct signalfd_siginfo si;
read(signalfd_fd, &si, sizeof(si));
printf("Got signal %d\n", si.ssi_signo);

// Self-pipe trick (portable):
// Write to pipe in handler, read in main loop
int pipefd[2];
void pipe_handler(int sig) {
    write(pipefd[1], &sig, sizeof(sig));
}

Timers & SIGALRM

setitimer delivers SIGALRM at intervals. ITIMER_REAL uses real time; ITIMER_VIRTUAL uses CPU time; ITIMER_PROF uses CPU + system time. The timer repeats until cancelled. For modern code, prefer timer_create with SIGEV_THREAD for per-thread timers, or use a dedicated timer fd (timerfd_create on Linux) for integration with event loops. Always handle the signal to avoid default termination.

c
#include <sys/time.h>

volatile sig_atomic_t timer_fired = 0;

void timer_handler(int sig) { timer_fired = 1; }

int main() {
    struct sigaction sa = {0};
    sa.sa_handler = timer_handler;
    sigaction(SIGALRM, &sa, NULL);

    // Set interval timer: 2 sec initial, 1 sec repeat
    struct itimerval timer;
    timer.it_value.tv_sec = 2;
    timer.it_interval.tv_sec = 1;
    setitimer(ITIMER_REAL, &timer, NULL);

    int count = 0;
    while (count < 5) {
        pause();
        if (timer_fired) {
            printf("Timer %d\n", ++count);
            timer_fired = 0;
        }
    }
    return 0;
}

Sending Signals

kill sends a signal to a process by PID. kill(0, sig) sends to the entire process group. raise sends a signal to the calling process. sigqueue sends a signal with attached data (siginfo). Always use waitpid to reap child processes and check exit status. WIFSIGNALED distinguishes signal death from normal exit. Sending SIGTERM (not SIGKILL) allows graceful shutdown.

c
#include <signal.h>
#include <sys/wait.h>

pid_t child = fork();
if (child == 0) {
    while (1) { sleep(1); }
} else {
    sleep(3);
    kill(child, SIGTERM);  // Send SIGTERM to child

    int status;
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status))
        printf("Killed by signal %d\n", WTERMSIG(status));
}

// Send signal to self
raise(SIGSTOP);  // Stop (resume with SIGCONT)

// Send to process group
kill(0, SIGUSR1);  // 0 = own process group

// Send with data (sigqueue)
union sigval value = { .sival_int = 42 };
sigqueue(child, SIGUSR1, value);
20

Process Management Advanced

fork & exec Patterns

The fork-exec pattern creates a child process (fork) and replaces its image with a new program (exec). fork duplicates the process; exec loads a new program. The child must call _exit (not exit) on exec failure to avoid flushing parent buffers. waitpid blocks until the child exits. WEXITSTATUS extracts the exit code. This is how shells run commands.

c
#include <unistd.h>
#include <sys/wait.h>

int run_program(const char *path, char *const args[]) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return -1; }

    if (pid == 0) {
        // Child: exec replaces process image
        execvp(path, args);
        perror("execvp");  // Only on failure
        _exit(127);
    }

    // Parent: wait for child
    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

// Usage
char *args[] = {"ls", "-la", "/tmp", NULL};
int result = run_program("ls", args);

Daemonization

setsid creates a new session and process group, detaching from the controlling terminal. The double-fork pattern prevents the daemon from reacquiring a terminal. After daemonizing, close standard file descriptors and redirect to /dev/null or log files. chdir to / prevents blocking unmounts. umask(0) ensures predictable file permissions. This is the standard daemon pattern.

c
#include <unistd.h>

int main() {
    pid_t child = fork();
    if (child > 0) _exit(0);  // Parent exits
    if (child < 0) return 1;

    // First child: create new session
    setsid();

    // Second fork (prevent reacquiring terminal)
    pid_t grandchild = fork();
    if (grandchild > 0) _exit(0);

    // Daemon process
    chdir("/");
    umask(0);
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    while (1) {
        sleep(60);
        // Do daemon work
    }
    return 0;
}

posix_spawn Alternative

posix_spawn is a more efficient alternative to fork+exec on systems without MMU (embedded) or with large memory footprints (fork copies page tables). It combines process creation and exec in one call, with file actions (redirect, close) applied atomically. Use posix_spawn when you do not need to modify the child state between fork and exec. It is POSIX-standard and available on Linux, macOS, and most Unix systems.

c
#include <spawn.h>
#include <sys/wait.h>

extern char **environ;

int spawn_child(const char *cmd, char *const argv[]) {
    pid_t pid;
    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);

    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    int ret = posix_spawnp(&pid, cmd, &actions, &attr, argv, environ);

    posix_spawn_file_actions_destroy(&actions);
    posix_spawnattr_destroy(&attr);

    if (ret != 0) return -1;

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

Zombie Prevention

Zombie processes occur when a child exits before the parent calls wait. Prevent them by: (1) handling SIGCHLD with waitpid in a loop (WNOHANG avoids blocking), (2) setting SIGCHLD to SIG_IGN (kernel auto-reaps), or (3) double-forking so the grandchild is orphaned and adopted by init (PID 1), which reaps it automatically. Always save and restore errno in signal handlers.

c
#include <sys/wait.h>
#include <signal.h>

// Reap zombies with SIGCHLD handler
void sigchld_handler(int sig) {
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // Reap all available zombies
    }
    errno = saved_errno;
}

// Setup: handle SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);

// Alternative: explicitly ignore (auto-reap)
// signal(SIGCHLD, SIG_IGN);

// Double-fork to prevent zombies
pid_t inter = fork();
if (inter == 0) {
    if (fork() == 0) {
        // Grandchild does the work
        execlp("sleep", "sleep", "10", NULL);
        _exit(1);
    }
    _exit(0);  // First child exits immediately
}
waitpid(inter, NULL, 0);

Resource Limits

setrlimit imposes resource limits on a process: CPU time (RLIMIT_CPU), virtual memory (RLIMIT_AS), file size (RLIMIT_FSIZE), open files (RLIMIT_NOFILE), stack size, and core dump size. The soft limit is enforced; the hard limit is the ceiling. Exceeding CPU time sends SIGXCPU; exceeding memory causes malloc to fail. Use limits in child processes to prevent resource exhaustion from bugs or attacks.

c
#include <sys/resource.h>

void set_limits() {
    struct rlimit lim;

    // Limit CPU time to 10 seconds
    lim.rlim_cur = 10; lim.rlim_max = 10;
    setrlimit(RLIMIT_CPU, &lim);

    // Limit memory to 256 MB
    lim.rlim_cur = 256*1024*1024;
    lim.rlim_max = 256*1024*1024;
    setrlimit(RLIMIT_AS, &lim);

    // Limit open files
    lim.rlim_cur = 32; lim.rlim_max = 64;
    setrlimit(RLIMIT_NOFILE, &lim);
}

void show_limits() {
    struct rlimit lim;
    getrlimit(RLIMIT_NOFILE, &lim);
    printf("Max files: soft=%lu, hard=%lu\n",
           lim.rlim_cur, lim.rlim_max);
}
21

Pipes & IPC Advanced

Anonymous Pipes

Anonymous pipes provide unidirectional communication between parent and child processes. Always close the unused end: the writer must close the read end, and vice versa. Closing the write end signals EOF to the reader (read returns 0). Pipes have a fixed buffer (typically 64KB); writes block when full. Pipes are for related processes only (parent-child). For unrelated processes, use named pipes (FIFOs) or sockets.

c
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // Child: read from pipe
        close(pipefd[1]);  // Close unused write end
        char buf[256];
        ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
        buf[n] = '\0';
        printf("Child received: %s", buf);
        close(pipefd[0]);
    } else {
        // Parent: write to pipe
        close(pipefd[0]);  // Close unused read end
        const char *msg = "Hello from parent!\n";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);  // EOF signal to reader
        wait(NULL);
    }
    return 0;
}

Named Pipes (FIFOs)

Named pipes (FIFOs) are special files that act as pipes between unrelated processes. mkfifo creates the file; open blocks until both a reader and writer are present. Use O_NONBLOCK for non-blocking opens. FIFOs persist in the filesystem until unlinked. They are useful for simple IPC between independent programs, but for complex communication, consider Unix domain sockets or message queues.

c
#include <sys/stat.h>
#include <fcntl.h>

// Create a named pipe (persists in filesystem)
mkfifo("/tmp/myfifo", 0666);

// Writer process
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);

// Reader process (can be unrelated to writer)
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);

// Non-blocking open
int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK);

// Clean up
unlink("/tmp/myfifo");

Shared Memory

Shared memory is the fastest IPC: processes map the same physical memory into their address spaces. shm_open creates a POSIX shared memory object; mmap maps it. Changes are immediately visible to all mappers. Use semaphores or mutexes (with PTHREAD_PROCESS_SHARED) for synchronization. Always unmap and unlink to avoid leaks. Shared memory is ideal for large data; the overhead is just the initial mapping.

c
#include <sys/mman.h>
#include <fcntl.h>

// Create shared memory object
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);  // Set size

// Map into process address space
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);
close(fd);  // Can close after mmap

// Write data (visible to other processes)
sprintf(shared, "Shared data at %p", (void*)shared);

// Synchronize (flush to backing store)
msync(shared, 4096, MS_SYNC);

// Unmap and clean up
munmap(shared, 4096);
shm_unlink("/my_shm");

Unix Domain Sockets

Unix domain sockets provide bidirectional, stream-oriented IPC on the same machine. They are faster than TCP (no network overhead) and support passing file descriptors between processes via SCM_RIGHTS. Use SOCK_STREAM for reliable streams, SOCK_DGRAM for datagrams. The socket path is a filesystem entry; unlink before bind to avoid address in use errors. Unix sockets are the basis of Docker, X11, and systemd communication.

c
#include <sys/socket.h>
#include <sys/un.h>

// Server
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
unlink("/tmp/mysocket");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);

int cfd = accept(sfd, NULL, NULL);
char buf[256];
read(cfd, buf, sizeof(buf));
write(cfd, "Response", 8);
close(cfd);

// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "Request", 7);
close(sock);

unlink("/tmp/mysocket");

Message Queues

POSIX message queues provide priority-ordered, message-based IPC. Each message has a priority; higher priority messages are received first. mq_send and mq_receive are atomic for single messages. Use O_NONBLOCK for non-blocking operation or mq_timedreceive for timeouts. Message queues persist until unlinked, unlike pipes which die with processes. They are ideal for task dispatching and event notification.

c
#include <mqueue.h>

// Create/open a message queue
struct mq_attr attr = {
    .mq_maxmsg = 10,
    .mq_msgsize = 256
};
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);

// Send a message (priority-based)
mq_send(mq, "Hello MQ", 8, 1);  // priority 1

// Receive a message (highest priority first)
char buf[256];
unsigned int prio;
ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
printf("Received (priority %u): %.*s\n", prio, (int)n, buf);

// Non-blocking receive
mqd_t mq_nb = mq_open("/my_queue", O_RDONLY | O_NONBLOCK);

// Timed receive
struct timespec ts = { .tv_sec = time(NULL) + 5 };
mq_timedreceive(mq, buf, sizeof(buf), &prio, &ts);

mq_close(mq);
mq_unlink("/my_queue");
22

Makefile & Build Advanced

Automatic Variables & Patterns

Automatic variables make Makefiles concise and maintainable. Pattern rules (%.o: %.c) define how to build any file matching a pattern. -MM generates dependency files (.d) that track header dependencies, so editing a header triggers recompilation of dependent .c files. The -include directive silently includes dependency files if they exist. This is the foundation of robust C/C++ build systems.

c
# Automatic variables:
# $@ = target name
# $< = first prerequisite
# $^ = all prerequisites
# $? = prerequisites newer than target

# Pattern rule: compiles any .c to .o
%.o: %.c %.h
	$(CC) $(CFLAGS) -c $< -o $@

OBJS = main.o utils.o parser.o

program: $(OBJS)
	$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)

# Automatic dependency generation
%.d: %.c
	@$(CC) -MM $< > $@

-include $(OBJS:.o=.d)

clean:
	rm -f $(OBJS) program

Variables & Conditionals

Use := for immediate evaluation (faster, predictable) and = for lazy evaluation (allows forward references). ?= sets a variable only if unset, allowing user overrides from the command line. Conditionals (ifeq, ifdef) enable debug/release builds. The Q trick silences command echo unless VERBOSE is set. MAKECMDGOALS contains the targets from the command line.

c
# = recursive (evaluated when used)
# := simple (evaluated when defined)
# ?= conditional (only if not already set)

CC := gcc
CFLAGS ?= -Wall -Wextra -O2
DEBUG := $(filter debug,$(MAKECMDGOALS))

ifeq ($(DEBUG),debug)
    CFLAGS += -g -DDEBUG -O0
else
    CFLAGS += -DNDEBUG
endif

ifdef VERBOSE
    Q =
else
    Q = @
endif

build:
	$(Q)$(CC) $(CFLAGS) -c main.c

Functions & Text Processing

Make functions enable text transformation: patsubst for pattern replacement, filter for selecting files, wildcard for globbing, foreach for iteration. The shell function executes commands at parse time—useful for embedding version info. Substitution references ($(VAR:.c=.o)) are a concise alternative to patsubst for simple suffix changes.

c
SRCS = main.c utils.c parser.c

# Substitution
OBJS = $(patsubst %.c,%.o,$(SRCS))  # main.o utils.o parser.o
# Or shorthand:
OBJS = $(SRCS:.c=.o)

# Filter files by extension
C_SRCS = $(filter %.c,$(wildcard *.c))

# Add prefix
FLAGS = $(addprefix -I,$(INCLUDE_DIRS))

# Foreach
DIRS = src lib include
CREATE = $(foreach dir,$(DIRS),mkdir -p $(dir);)

# Shell function
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"

Subdirectories & Recursive Make

Recursive make (subdirectory Makefiles) is traditional but can be slow and error-prone with parallel builds. The non-recursive approach (single Makefile with vpath) is preferred for correctness and speed. If using recursive make, pass variables explicitly and use MAKECMDGOALS to propagate targets. For large projects, consider CMake or Meson instead of raw Make for better dependency tracking and IDE support.

c
SUBDIRS = lib src tests

.PHONY: all $(SUBDIRS) clean test

all: $(SUBDIRS)

# Pass variables to sub-makes
$(SUBDIRS):
	$(MAKE) -C $@ $(MAKECMDGOALS)

# Parallel build: make -j4

clean: $(SUBDIRS)
	rm -f *.o program

# Non-recursive alternative (single Makefile)
vpath %.c src:lib
vpath %.h include

CFLAGS += -Iinclude -Ilib

program: main.o lib/utils.o
	$(CC) $^ -o $@

CMake Integration

CMake generates Makefiles (or Ninja, VS, Xcode projects) from a declarative CMakeLists.txt. Modern CMake uses target-based commands (target_include_directories, target_link_libraries) instead of global variables. Generator expressions ($<$<CONFIG:Debug>:...) enable per-configuration flags. CMake is the de facto standard for C/C++ projects, with better IDE integration and cross-platform support than raw Makefiles.

c
# CMakeLists.txt (modern CMake 3.15+)
cmake_minimum_required(VERSION 3.15)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(myapp src/main.c src/utils.c)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra)

# Per-configuration flags
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-g -O0 -DDEBUG>
    $<$<CONFIG:Release>:-O2 -DNDEBUG>
)

# Find and link library
find_package(MATH REQUIRED)
target_link_libraries(myapp PRIVATE m)

install(TARGETS myapp DESTINATION bin)
23

Debugging with GDB

Starting & Breakpoints

Compile with -g to embed debug symbols and -O0 to disable optimizations (otherwise variables may be optimized away). break sets breakpoints at functions, lines, or conditions. watch (data breakpoints) trigger when a variable changes—powerful for finding memory corruption. Conditional breakpoints (break func if cond) only fire when the condition is true, useful for loops. tbreak is a one-shot breakpoint.

c
# Compile with debug symbols
gcc -g -O0 -o program program.c

# Start GDB
gdb ./program
gdb --args ./program arg1 arg2

# Breakpoints
(gdb) break main           # Function
(gdb) break 42             # Line 42
(gdb) break file.c:50      # Specific file
(gdb) break func if x > 10 # Conditional
(gdb) tbreak main          # Temporary (removed after hit)
(gdb) watch x              # Break when x changes

# Manage breakpoints
(gdb) info breakpoints
(gdb) delete 2             # Delete breakpoint #2
(gdb) disable 1            # Temporarily disable
(gdb) enable 1

Stepping & Inspection

next steps over function calls; step enters them. finish runs to the end of the current function. print formats: /x (hex), /c (char), /s (string), /t (binary). The @ operator prints array slices: arr@5 shows 5 elements. display auto-prints variables at each stop. backtrace shows the call stack; frame N switches context to inspect that frame.

c
(gdb) run                  # Start program
(gdb) continue             # Continue to next breakpoint
(gdb) next                 # Step over (no function entry)
(gdb) step                 # Step into (enters functions)
(gdb) finish               # Run until function returns
(gdb) until 50             # Run until line 50

# Inspect variables
(gdb) print x              # Print variable
(gdb) print *ptr           # Dereference pointer
(gdb) print arr[0]@5       # First 5 elements
(gdb) print/x x            # Print in hex
(gdb) display x            # Auto-print at each stop
(gdb) info locals          # All local variables

# Backtrace
(gdb) backtrace            # Call stack
(gdb) frame 2              # Switch to frame #2
(gdb) up / down            # Navigate stack

Examining Memory

The examine (x) command inspects raw memory. The format specifies count, display format, and unit size. x/10i $pc disassembles 10 instructions from the program counter. x/s treats memory as a null-terminated string. info proc mappings shows the virtual memory layout (text, data, heap, stack, shared libraries). This is essential for debugging buffer overflows and memory corruption.

c
# x/[count][format][size] address
(gdb) x/10x &arr           # 10 words in hex
(gdb) x/20cb &str          # 20 bytes as chars
(gdb) x/4xw ptr            # 4 words in hex
(gdb) x/s str              # String

# Format: x(hex) d(decimal) u(unsigned) o(octal)
#         t(binary) f(float) a(addr) i(instr) c(char) s(string)
# Size: b(byte) h(half/2) w(word/4) g(giant/8)

# Disassemble
(gdb) disassemble main
(gdb) disassemble /r main  # With raw bytes

# Memory map
(gdb) info proc mappings   # Process memory layout

Core Dumps

Core dumps capture the process state at crash time for post-mortem debugging. Enable them with ulimit -c unlimited. Load the core file with gdb program core. The backtrace shows where the crash occurred; info locals shows variable values. For multithreaded programs, thread apply all bt shows all thread states—essential for deadlock analysis.

c
# Enable core dumps
ulimit -c unlimited
echo "core.%p" > /proc/sys/kernel/core_pattern

# Run program until it crashes
./program                  # Produces core.12345

# Analyze core dump
gdb ./program core.12345

(gdb) bt                   # Backtrace at crash
(gdb) bt full              # With local variables
(gdb) frame 0              # Top frame (crash location)
(gdb) info locals          # Variables at crash
(gdb) print *ptr           # What was the pointer?

# Multithreaded
(gdb) info threads         # List all threads
(gdb) thread 3             # Switch to thread 3
(gdb) thread apply all bt  # Backtraces for ALL threads

GDB Scripts & Automation

.gdbinit automates common settings on startup. define creates custom commands for repetitive tasks. commands attaches actions to breakpoints (e.g., log a variable and continue). GDB supports Python scripting for complex analysis: automate test runs, visualize data structures, or extract statistics. Python scripts can access GDB internals via the gdb module. Use scripts to standardize debugging workflows across a team.

c
# .gdbinit file (loaded on startup)
set pagination off
set print pretty on
set print element 0        # No limit on string display

# Define custom commands
define print_array
    set $i = 0
    while $i < $arg0
        printf "[%d] = %d\n", $i, $arg1[$i]
        set $i = $i + 1
    end
end
# Usage: print_array 10 my_array

# Commands attached to breakpoints
break main
commands 1
  silent
  printf "x = %d\n", x
  continue
end

# Python scripting (GDB 7+)
python
import gdb
gdb.execute("break main")
gdb.execute("run")
val = gdb.parse_and_eval("x")
print(f"x = {int(val)}")
end
24

Memory Alignment & Bitfields

Struct Alignment & Padding

The compiler inserts padding so that each member is naturally aligned (typically to its size: char=1, short=2, int=4, double=8). Reordering members from largest to smallest minimizes padding. Use offsetof to inspect layout. On 64-bit systems, pointers need 8-byte alignment. Excessive padding wastes memory and hurts cache performance. Always order struct members by decreasing size.

c
#include <stddef.h>

// Without alignment consideration (padded)
struct Bad {
    char a;     // 1 byte + 7 padding
    double b;   // 8 bytes
    char c;     // 1 byte + 7 padding
};  // sizeof = 24

// Reordered for efficiency
struct Good {
    double b;   // 8 bytes
    char a;     // 1 byte
    char c;     // 1 byte + 6 padding
};  // sizeof = 16

printf("Bad: a=%zu b=%zu c=%zu total=%zu\n",
    offsetof(struct Bad, a),
    offsetof(struct Bad, b),
    offsetof(struct Bad, c),
    sizeof(struct Bad));

Controlling Alignment

C11 alignas specifies minimum alignment for types or variables—useful for SIMD (16/32-byte alignment) and DMA. Packed structs (__attribute__((packed)) or #pragma pack) remove all padding, saving space but potentially slowing access (unaligned memory access may fault on some architectures). Use packed for network protocols and file formats where exact layout matters. Never pack structs that need fast access.

c
#include <stdalign.h>

// C11 explicit alignment
struct alignas(16) Aligned16 {
    int data[4];  // 16 bytes, 16-byte aligned
};

// Check alignment
printf("Alignment of int: %zu\n", alignof(int));     // 4
printf("Alignment of struct: %zu\n",
       alignof(struct Aligned16));  // 16

// Packed struct (no padding) - GCC/Clang
struct __attribute__((packed)) Packed {
    char a;
    int b;    // No padding after a
    char c;
};  // sizeof = 6

// MSVC packed
#pragma pack(push, 1)
struct PackedMSVC { char a; int b; char c; };
#pragma pack(pop)

Flexible Array Members

Flexible array members (C99) allow a struct to have a variable-length array as its last member. Allocate with malloc(sizeof(struct) + desired_length). The array shares the single allocation, so one free releases everything. This is more efficient and cleaner than a separate pointer + malloc. Common in dynamic arrays, strings, and network packet headers. sizeof(struct) excludes the flexible array.

c
#include <stdlib.h>

// C99 flexible array member (last member with no size)
struct Buffer {
    size_t size;
    char data[];  // Flexible array
};

// Allocate with space for data
size_t data_len = 1024;
struct Buffer *buf = malloc(sizeof(struct Buffer) + data_len);
buf->size = data_len;
memset(buf->data, 0, data_len);

// Use data
strcpy(buf->data, "Hello");

// Single free (no separate allocation for data)
free(buf);

Union Type Puning

Unions overlay members in the same memory, enabling type punning (reinterpreting bits as a different type). Reading a union member other than the last written one is allowed in C (implementation-defined). Union type punning is legal under strict aliasing, unlike pointer casting. Anonymous unions (C11) expose members directly without a member name. Use unions for tagged variants and accessing IEEE 754 float internals.

c
#include <stdio.h>

// Union: members share the same memory
union Data {
    int i;
    float f;
    char bytes[4];
};

int main() {
    union Data d;
    d.i = 0x41424344;

    printf("As int: %d\n", d.i);
    printf("As float: %f\n", d.f);
    printf("As bytes: %02x %02x %02x %02x\n",
           d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);

    // Safe type punning (legal in C)
    union { float f; int i; } u;
    u.f = 3.14f;
    printf("Float bits: 0x%08x\n", u.i);

    // Anonymous unions (C11)
    struct Variant {
        int type;
        union { int i; float f; char *s; };
    };
}

Memory Layout & Endianness

Endianness determines byte order in memory: little-endian (x86, ARM default) stores LSB first; big-endian stores MSB first. When writing portable binary formats, serialize with explicit shifts instead of memcpy. Use dump_hex to inspect raw memory during debugging. Network protocols use big-endian (network byte order); use htonl/ntohl for portable code. Always test on both endianness when writing portable binary I/O.

c
#include <stdint.h>

void dump_hex(void *ptr, size_t len) {
    unsigned char *bytes = ptr;
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", bytes[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
}

int main() {
    uint32_t val = 0x12345678;
    printf("Value: 0x%08x\n", val);
    printf("Bytes: ");
    dump_hex(&val, sizeof(val));

    // Little-endian: 78 56 34 12 (LSB first)
    // Big-endian:    12 34 56 78 (MSB first)

    // Serialize to big-endian (network order)
    unsigned char be[4];
    be[0] = (val >> 24) & 0xFF;
    be[1] = (val >> 16) & 0xFF;
    be[2] = (val >> 8)  & 0xFF;
    be[3] = val & 0xFF;

    // Deserialize from big-endian
    uint32_t recovered = (be[0]<<24)|(be[1]<<16)|(be[2]<<8)|be[3];
    printf("Recovered: 0x%08x\n", recovered);
    return 0;
}
25

Variadic Functions Advanced

va_list Basics

Variadic functions use va_list to access variable arguments. va_start initializes the list with the last named parameter. va_arg retrieves the next argument with the specified type. va_end cleans up. The caller must communicate the count and types (e.g., printf uses format specifiers). Variadic functions lack type safety—mismatched types cause undefined behavior.

c
#include <stdarg.h>

// Variadic function: variable number of arguments
int sum(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize with last named param

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  // Get next argument
    }

    va_end(args);  // Cleanup
    return total;
}

// Usage
int result = sum(3, 10, 20, 30);  // 60
int result2 = sum(5, 1, 2, 3, 4, 5);  // 15

Sentinel-Terminated Variadics

A sentinel value (often NULL) marks the end of the argument list, eliminating the need for a count parameter. This is common in C APIs like execl. GCC __attribute__((sentinel)) warns if the last argument is not NULL. Always document the expected sentinel. The downside is that the sentinel cannot appear as a valid data value.

c
#include <stdarg.h>

// Sentinel value marks the end
void log_messages(const char *first, ...) {
    va_list args;
    va_start(args, first);

    const char *msg = first;
    while (msg != NULL) {  // NULL is the sentinel
        printf("%s\n", msg);
        msg = va_arg(args, const char *);
    }

    va_end(args);
}

// Usage: NULL terminates the list
log_messages("Error 1", "Error 2", "Error 3", NULL);

// GCC attribute to enforce sentinel
void log_messages(const char *first, ...)
    __attribute__((sentinel));

vfprintf & Format Strings

vprintf/vfprintf/vsnprintf accept a va_list instead of ..., enabling custom printf-like functions. Always use vsnprintf (bounded) instead of vsprintf to prevent buffer overflows. Forward the va_list directly. This pattern is used in logging libraries, error reporting, and custom formatters. The format string vulnerability (user-controlled format) is a security risk—never pass user input as the format.

c
#include <stdarg.h>
#include <stdio.h>

// Custom printf-like function
void logf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    // Add timestamp prefix
    printf("[LOG] ");
    vprintf(format, args);  // Pass va_list to vprintf
    printf("\n");

    va_end(args);
}

// Write to string with vsnprintf
void format_msg(char *buf, size_t size, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, size, fmt, args);  // Safe: bounded
    va_end(args);
}

// Usage
logf("User %s logged in (id=%d)", username, id);
char msg[256];
format_msg(msg, sizeof(msg), "Error %d: %s", code, text);

Function Pointers & Callbacks

Function pointers enable callbacks and polymorphism in C. The syntax return_type (*name)(params) declares a pointer to a function. qsort uses a comparison callback for generic sorting. Arrays of function pointers implement dispatch tables (alternative to switch). Always ensure the callback signature matches exactly. Function pointers are the basis of event handlers, plugins, and the strategy pattern in C.

c
#include <stdlib.h>

// Function pointer type
typedef int (*compare_fn)(const void *, const void *);

// Comparison function for qsort
int cmp_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer
    qsort(arr, n, sizeof(int), cmp_int);

    // Array of function pointers
    double (*ops[])(double, double) = {
        add, subtract, multiply, divide
    };
    double result = ops[2](10.0, 3.0);  // multiply

    // Function pointer as parameter
    void apply(int *arr, size_t n, int (*fn)(int)) {
        for (size_t i = 0; i < n; i++) arr[i] = fn(arr[i]);
    }
    apply(arr, n, square);
    return 0;
}

Variadic Macros

Variadic macros (__VA_ARGS__) accept variable arguments, useful for logging and debugging. __VA_OPT__ (C2x) handles the zero-argument case by conditionally including the comma. The ##__VA_ARGS__ GCC extension removes the preceding comma when no arguments are passed. Debug macros that compile to nothing in release builds eliminate overhead without code changes. Always guard format strings to prevent format string attacks.

c
// C99 variadic macros
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

// Usage
LOG("Value: %d", x);
LOG("User %s, age %d", name, age);

// GCC __VA_OPT__ (C99/C2x): handle zero arguments
#define LOG2(fmt, ...)     printf("[LOG] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)

// Count arguments (GCC extension)
#define COUNT(...) NARG_(__VA_ARGS__, 5, 4, 3, 2, 1)
#define NARG_(_1, _2, _3, _4, _5, N, ...) N

// Debug macro that compiles out in release
#ifdef NDEBUG
    #define DEBUG(fmt, ...) ((void)0)
#else
    #define DEBUG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#endif

// ## removes comma if no variadic args (GCC extension)
DEBUG("just a message");
DEBUG("value = %d", x);

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.