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.
#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.
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.
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.
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-alignedPreprocessor & 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.
#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
#endifStrings & 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.
#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); // HelloConcatenation & 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).
#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.
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=20strchr, 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.
#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).
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);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.
#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); // 4294967295Floating 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.
#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.
#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.
#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.0Type 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.
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");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>.
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.
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.
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.
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 onceBreak, 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.
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");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.
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.
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) == 120Function 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.
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.
#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) == 60static & 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.
// 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;
}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).
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]); // bananaPointers
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.
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.
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.
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.
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");
}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.
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.
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.
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).xUnions
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.
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.
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); // 1Memory 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.
#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 pointercalloc & 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.
#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.
// 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 freedMemory 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.
// 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 likelyDynamic 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.
#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);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.
#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.
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.
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.
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.
#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)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.
#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); // 7Conditional 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.
#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
#endifInclude 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).
// 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.
#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.
#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); // 42Bit 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.
#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).
#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.
#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.
#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.
#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;
}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.
#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.
#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.
#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.
#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 childrenSelf-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.
#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;
}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.
#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.
#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 envZombie & 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'.
#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.
#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.
#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 queuesPipes & 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).
#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.
#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.
#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.
#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.
#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 inputMakefile & 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.
# 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.
# 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 directoriesDependencies & 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.
# 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 automaticallyMulti-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.
# 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 changesCMake 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.
# 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 projectsFunction 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.
#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).
#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.
#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.
#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.
#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 consistentVariable 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.
#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).
#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.
#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.
#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.
#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;
}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.
// 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.
#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.
// 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.
#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");
#endifBitwise 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.
// 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;
}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.
#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.
#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.
#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.
#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.
#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);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.
#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.
#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.
#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.
#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.
#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);
}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.
#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.
#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.
#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.
#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.
#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");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.
# 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) programVariables & 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.
# = 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.cFunctions & 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.
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.
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.
# 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)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.
# 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 1Stepping & 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.
(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 stackExamining 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.
# 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 layoutCore 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.
# 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 threadsGDB 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.
# .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)}")
endMemory 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.
#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.
#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.
#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.
#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.
#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;
}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.
#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); // 15Sentinel-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.
#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.
#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.
#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.
// 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);Related C snippets
Copy-paste ready code for common tasks.
Pointer Basics
Declare pointers, dereference, and walk an array with pointer arithmetic.
Memory Management
Allocate, resize, and free heap memory with malloc, realloc, and free.
String Operations
Use string.h helpers for length, copy, concat, compare, and tokenize.
File I/O
Open, read, write, and close files using the stdio FILE API.
Structs
Group related fields with typedef and pass by pointer for mutation.
Function Pointers
Store function addresses for callbacks and dispatch tables.
Preprocessor Macros
Define object-like and function-like macros with conditional compilation.
Bit Operations
Set, clear, toggle, and test bits with bitwise operators and flags.
Was this helpful?