From "wtf is a variable" to "I work at NASA now." Every concept explained like you're human, not a textbook.
// every C file needs this. stdio.h = standard input/output. // without it you literally can't print shit to the screen #include <stdio.h> // int main() is THE entry point. program starts here, period. // return 0 means "hey OS, everything went fine, chill" int main() { // printf = "print formatted". \n is a newline (enter key basically) printf("Hello, World!\n"); return 0; }
gcc hello.c -o hello then run: ./hello — that's literally it.| Type | Size | What it holds |
|---|---|---|
| int | 4 bytes | Whole numbers. No decimals, don't even try. |
| float | 4 bytes | Decimals. ~7 digits of precision. Gets sketchy fast. |
| double | 8 bytes | Better decimals. Use this over float unless you hate memory. |
| char | 1 byte | Single character. Like 'A' or '$'. Yes it's tiny. |
| long | 8 bytes | Big-ass integers. When int just isn't enough. |
| short | 2 bytes | Tiny int. You'll rarely use this but it exists. |
| unsigned int | 4 bytes | Only positive ints. Double the max value tho. |
| void | 0 | Nothing. "This function returns nothing, shut up." |
// format: type name = value; // think of it like labeling a box before you put shit in it int age = 17; float gpa = 3.7; char grade = 'A'; // single quotes for char, don't mess this up double bigNum = 9999999.99; // you can declare without assigning (it'll have garbage value lol) int x; // x = some random bullshit number. be careful. x = 42; // now x is 42. finally. // const = you CAN'T change it later. compiler will yell at you. const float PI = 3.14159;
// format specifiers: how you shove variables into strings int score = 100; float temp = 98.6; char letter = 'B'; printf("Score: %d\n", score); // %d = integer printf("Temp: %.1f\n", temp); // %.1f = float, 1 decimal place printf("Grade: %c\n", letter); // %c = char printf("Name: %s\n", "Chad"); // %s = string // scanf reads user input. THE & IS NOT OPTIONAL. // & means "address of". forget it = segfault = your program dies int num; scanf("%d", &num); // reads an int the user types
& before your variable in scanf or it WILL crash. No exceptions.// arithmetic — basic ass math int a = 10, b = 3; printf("%d\n", a + b); // 13 printf("%d\n", a - b); // 7 printf("%d\n", a * b); // 30 printf("%d\n", a / b); // 3 — INT DIVISION. no decimal. R.I.P. the .33 printf("%d\n", a % b); // 1 — modulo = remainder. super useful btw // shorthand — because typing is exhausting a++; // a = a + 1 (same as a += 1) a--; // a = a - 1 a += 5; // a = a + 5 a *= 2; // a = a * 2 // comparison — returns 1 (true) or 0 (false) a == b // equal? (NOT = which is assignment, don't mix these up) a != b // not equal? a > b // greater than? a <= b // less than or equal? // logical — combining conditions a > 0 && b > 0 // AND — both must be true a > 0 || b > 0 // OR — at least one must be true !(a > 0) // NOT — flips it
int grade = 75; // if the condition in () is true, run the {} block if (grade >= 90) { printf("You're a genius or you cheated\n"); } else if (grade >= 70) { printf("Decent. Could be worse.\n"); } else if (grade >= 60) { printf("Barely passing lmao\n"); } else { printf("Bro... study more\n"); } // ternary operator — if/else on one line (flex move) // condition ? value_if_true : value_if_false int max = (grade > 50) ? grade : 50; printf("Max of grade/50: %d\n", max);
// FOR loop — when you know how many times to loop // for (start; keep going while; do this each time) for (int i = 0; i < 5; i++) { printf("Loop #%d\n", i); // prints 0 1 2 3 4 (NOT 5, < not <=) } // WHILE loop — when you don't know how many times int count = 0; while (count < 3) { printf("Still going: %d\n", count); count++; // DON'T FORGET THIS. infinite loop = freeze = sadness } // DO-WHILE — runs at least once, THEN checks condition do { printf("I ran at least once no matter what\n"); count++; } while (count < 1); // break = GTFO the loop immediately // continue = skip this iteration, keep looping for (int i = 0; i < 10; i++) { if (i == 5) break; // stops at 5 if (i % 2 == 0) continue; // skips even numbers printf("%d ", i); // prints: 1 3 (evens skipped, stops at 5) }
int day = 3; switch (day) { // checks day against each case case 1: printf("Monday. Ugh.\n"); break; // ALWAYS break or it falls through to next case case 2: printf("Tuesday. Still rough.\n"); break; case 3: printf("Wednesday. Hump day lol.\n"); break; default: // like the "else" — catches everything else printf("Some other day idk\n"); } // fall-through trick — when two cases do the same thing switch (day) { case 6: // no break here, falls through to case 7 case 7: printf("WEEKEND BABY LET'S GO\n"); break; }
break is one of the most classic C bugs ever. Don't be that person.// define a function BEFORE main() or declare it as a prototype // returnType functionName(param1, param2) { ... } // this function takes two ints and returns their sum int add(int a, int b) { return a + b; // sends the value back to whoever called it } // void functions return nothing — just DO stuff void greet(char name[]) { printf("Hey %s, what's up!\n", name); } // function prototype — declare it up top so you can define it anywhere double power(double base, int exp); // just the signature, no body int main() { int result = add(5, 3); // result = 8 greet("Alex"); printf("2^10 = %.0f\n", power(2.0, 10)); return 0; } // defined down here because we prototyped it above double power(double base, int exp) { double result = 1.0; for (int i = 0; i < exp; i++) result *= base; return result; }
// array = a fixed-size list. all same type. indexed from 0. int scores[5] = {95, 87, 76, 92, 83}; // access by index. first element = [0]. last = [4] NOT [5]. // going out of bounds = undefined behavior = random chaos. BE CAREFUL. printf("%d\n", scores[0]); // 95 printf("%d\n", scores[4]); // 83 // loop through array — this is the standard move int len = 5; for (int i = 0; i < len; i++) { printf("Score %d: %d\n", i, scores[i]); } // 2D array — like a grid or table int grid[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; printf("%d\n", grid[1][2]); // row 1, col 2 = 6 // get array size at compile time (only works in same scope it's declared) int size = sizeof(scores) / sizeof(scores[0]); // 20/4 = 5
#include <string.h> // need this for string functions // strings are char arrays ending with '\0' (null terminator) // C literally does NOT have a String type. it's always this. char name[20] = "Alex"; // stored as: A, l, e, x, \0 + garbage char city[] = "NYC"; // compiler figures out the size (4 bytes: N,Y,C,\0) // useful string functions from string.h strlen(name); // length of string (NOT counting \0) strcpy(name, "Jordan"); // copy "Jordan" into name. DON'T overflow the buffer! strcat(name, " Smith"); // append " Smith" to name. again, watch size. strcmp(name, "Alex"); // compare. 0 = equal, neg = less, pos = greater // NEVER use == to compare strings. it compares addresses, not content. // that's one of those bugs that'll haunt you for hours lmao if (strcmp(name, "Alex") == 0) { printf("Same name!\n"); }
== on strings. Use strcmp(). Seriously. This trips up everyone.// global variables — accessible from ANY function in the file // generally avoid using these. it gets messy fast. int globalCount = 0; void increment() { globalCount++; // can see globalCount because it's global // local variable — ONLY lives inside this function int localVar = 10; } // localVar is GONE after this }. it doesn't exist anymore. // static local — keeps its value between function calls void counter() { static int count = 0; // initialized ONCE, persists between calls count++; printf("Called %d times\n", count); }
// every variable lives at some memory address. a pointer STORES that address. // think: pointer is a sticky note with someone's home address on it int x = 42; int *p; // p is a pointer to an int. the * means "pointer to" p = &x; // & means "give me the address of x". now p points to x. printf("%d\n", x); // 42 — the actual value printf("%p\n", p); // some address like 0x7fff... — memory location printf("%d\n", *p); // 42 — *p = "go to that address and get the value" = dereference // changing x THROUGH the pointer *p = 100; // x is now 100. wild right? we changed x without touching x directly. printf("%d\n", x); // 100 ← yep, changed. // NULL pointer — pointer that points to nothing. safe default. int *ptr = NULL; if (ptr != NULL) { // ALWAYS check before dereferencing or you segfault printf("%d\n", *ptr); }
// arrays and pointers are SUPER related in C // the array name IS a pointer to the first element int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; // p points to arr[0] printf("%d\n", *p); // 10 — arr[0] printf("%d\n", *(p+1)); // 20 — arr[1]. p+1 moves forward by sizeof(int) printf("%d\n", *(p+4)); // 50 — arr[4] // arr[i] and *(arr+i) are literally identical. C translates one to the other. // this is why arrays start at 0 — it's an OFFSET from the start address p++; // moves p forward by sizeof(int) bytes. now points to arr[1] printf("%d\n", *p); // 20 // pointer to pointer — yes this is a thing and yes it's annoying int **pp = &p; // pp points to p which points to x. pointer inception. printf("%d\n", **pp); // dereference twice to get the actual value
// struct = a custom data type made of other types // like if you want to store info about a person in one place struct Person { char name[50]; int age; float gpa; }; // or use typedef so you don't have to type "struct" every time typedef struct { char name[50]; int age; float gpa; } Person; Person p1; // now no need to write "struct Person" strcpy(p1.name, "Maya"); // dot notation to access fields p1.age = 17; p1.gpa = 3.9; // or initialize all at once Person p2 = {"Jordan", 18, 3.5}; // struct pointer — use -> instead of dot when you have a pointer to a struct Person *ptr = &p1; printf("%s is %d years old\n", ptr->name, ptr->age); // ptr->name is the same as (*ptr).name. -> is just cleaner.
#include <stdlib.h> // need this for malloc, calloc, realloc, free // malloc — allocate N bytes of memory. returns a void pointer (cast it) // this is memory on the HEAP, not the stack. it persists until you free it. int *arr = (int*) malloc(5 * sizeof(int)); // space for 5 ints // ALWAYS check if malloc returned NULL (means system is out of memory) if (arr == NULL) { printf("malloc failed. system is cooked.\n"); return 1; } // use it like a normal array for (int i = 0; i < 5; i++) arr[i] = i * 10; // calloc — like malloc but ZEROES out the memory. safer. int *arr2 = (int*) calloc(5, sizeof(int)); // 5 ints, all set to 0 // realloc — resize existing allocation (like array.push in JS but manual) arr = (int*) realloc(arr, 10 * sizeof(int)); // now space for 10 ints // FREE IT when you're done. EVERY malloc needs a free. // not freeing = memory leak = program slowly eats your RAM. BAD. free(arr); free(arr2); arr = NULL; // set to NULL after freeing. prevents use-after-free bugs.
malloc/calloc/realloc must have a matching free(). No exceptions. Memory leaks are silent killers.#include <stdio.h> // fopen opens a file. modes: "r"=read, "w"=write (creates/clears), // "a"=append, "rb"/"wb" = binary read/write FILE *f = fopen("data.txt", "w"); if (f == NULL) { // check this. file might not exist or no permission. perror("Couldn't open file"); // perror prints system error message return 1; } fprintf(f, "Hello File!\n"); // printf but to a file fprintf(f, "Score: %d\n", 100); fclose(f); // ALWAYS close the file when done. flushes buffer. // reading from a file f = fopen("data.txt", "r"); char line[100]; while (fgets(line, sizeof(line), f) != NULL) { // reads line by line printf("%s", line); // fgets includes the \n so don't add another } fclose(f);
// enum = named constants. way better than magic numbers like 0, 1, 2 // instead of if (status == 2) you write if (status == RUNNING) — clear! typedef enum { IDLE, // = 0 automatically RUNNING, // = 1 STOPPED, // = 2 ERROR // = 3 } Status; Status s = RUNNING; switch (s) { case IDLE: printf("Chillin'\n"); break; case RUNNING: printf("Go go go!\n"); break; case ERROR: printf("Something broke lol\n"); break; default: break; } // typedef — give any type a new name (usually used to simplify ugly types) typedef unsigned long long u64; // now u64 instead of "unsigned long long" u64 bigNumber = 1000000000000ULL;
// preprocessor directives start with #. they run before compilation. // #define — text substitution. no type checking. no semicolon. #define MAX_SIZE 100 #define PI 3.14159 #define SQUARE(x) ((x) * (x)) // function-like macro. ALWAYS wrap in () or bugs happen // SQUARE(3+1) without parens would be 3+1*3+1=7 not 16. wrap it. // include guards — prevent the same header from being included twice // put this at the TOP of every .h file you write #ifndef MY_HEADER_H #define MY_HEADER_H // ... all your header content ... #endif // conditional compilation — include code only in certain builds #define DEBUG 1 #ifdef DEBUG printf("[DEBUG] value = %d\n", someVar); // only in debug builds #endif // predefined macros — free stuff the compiler gives you printf("Line %d in file %s\n", __LINE__, __FILE__); printf("Compiled: %s %s\n", __DATE__, __TIME__);
// bits are the lowest level. 0s and 1s. this shit is FAST. // used in embedded systems, flags, compression, encryption, etc. unsigned int a = 0b1010; // = 10 in decimal. 0b prefix = binary literal unsigned int b = 0b1100; // = 12 in decimal a & b // AND: 1010 & 1100 = 1000 (8). bit on only if BOTH are 1 a | b // OR: 1010 | 1100 = 1110 (14). bit on if EITHER is 1 a ^ b // XOR: 1010 ^ 1100 = 0110 (6). bit on if DIFFERENT ~a // NOT: flip every bit. ~1010 = ...11110101 (depends on int size) a << 2 // left shift: 1010 << 2 = 101000 (40). multiply by 2^n a >> 1 // right shift: 1010 >> 1 = 0101 (5). divide by 2^n // real use: bit flags — pack multiple true/false values into one int #define FLAG_READ (1 << 0) // = 0001 #define FLAG_WRITE (1 << 1) // = 0010 #define FLAG_EXEC (1 << 2) // = 0100 int perms = FLAG_READ | FLAG_WRITE; // set read + write if (perms & FLAG_READ) printf("Can read\n"); // check if read is set perms |= FLAG_EXEC; // set exec flag perms &= ~FLAG_WRITE; // clear write flag
// functions have addresses too. you can store those addresses in pointers. // this enables callbacks, dispatch tables, plugin systems — powerful stuff. int add(int a, int b) { return a + b; } int mul(int a, int b) { return a * b; } // syntax: returnType (*pointerName)(paramTypes) int (*op)(int, int); // op is a pointer to a function taking 2 ints, returning int op = add; // point to add function printf("%d\n", op(3, 4)); // 7 op = mul; // now point to mul printf("%d\n", op(3, 4)); // 12 // use typedef to make it readable (the raw syntax is ugly as sin) typedef int (*MathOp)(int, int); MathOp ops[] = {add, mul}; // array of function pointers = dispatch table! printf("%d\n", ops[0](5, 6)); // 11 printf("%d\n", ops[1](5, 6)); // 30 // passing function as argument (callback pattern) void apply(int *arr, int n, int (*fn)(int)) { for (int i = 0; i < n; i++) arr[i] = fn(arr[i]); }
// recursion = function calls itself. needs a BASE CASE or it loops forever. // each call uses stack space. deep recursion = stack overflow. be careful. // classic example: factorial. 5! = 5*4*3*2*1 int factorial(int n) { if (n <= 1) return 1; // base case — stop here or we recurse forever return n * factorial(n - 1); // recursive case — calls itself with smaller n } // fibonacci — each number is sum of the two before it int fib(int n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); // this is simple but SLOW for large n — exponential time complexity // for serious use, use iteration or memoization instead } printf("%d\n", factorial(5)); // 120 printf("%d\n", fib(10)); // 55
// linked list = each node holds data AND a pointer to the next node // unlike arrays, size is dynamic. inserting/deleting is O(1) at the head. typedef struct Node { int data; struct Node *next; // pointer to the next node in the chain } Node; // create a new node on the heap Node* newNode(int val) { Node* n = (Node*) malloc(sizeof(Node)); n->data = val; n->next = NULL; // tail of the list points to nothing return n; } // traverse the list and print every node void printList(Node* head) { Node* cur = head; while (cur != NULL) { // keep going until we hit the end printf("%d -> ", cur->data); cur = cur->next; // move to next node } printf("NULL\n"); } // build a small list: 1 -> 2 -> 3 -> NULL Node* head = newNode(1); head->next = newNode(2); head->next->next = newNode(3); printList(head);
/* ── math_utils.h ─────────────────────────────── */ // header file = declarations only. tells OTHER files what exists. #ifndef MATH_UTILS_H // include guard — prevents double-include #define MATH_UTILS_H int add(int a, int b); // function prototype (declaration) int subtract(int a, int b); #endif /* ── math_utils.c ─────────────────────────────── */ // source file = actual implementations #include "math_utils.h" // "" for your own files, <> for system headers int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } /* ── main.c ───────────────────────────────────── */ #include <stdio.h> #include "math_utils.h" int main() { printf("%d\n", add(3, 4)); // 7 printf("%d\n", subtract(9, 5)); // 4 return 0; } // compile: gcc main.c math_utils.c -o myapp // or use a Makefile to automate all this
/* A C program's memory is divided into segments: HIGH ADDRESS ┌──────────────────────────────────────────┐ │ STACK — local variables, function args │ ← grows downward │ function call frames │ │ (fast, limited, automatic cleanup) │ ├──────────────────────────────────────────┤ │ ...free space in between... │ ├──────────────────────────────────────────┤ │ HEAP — malloc/calloc allocations │ ← grows upward │ (large, manual management, slow alloc) │ ├──────────────────────────────────────────┤ │ BSS — uninitialized global/static │ (zeroed out at start) ├──────────────────────────────────────────┤ │ DATA — initialized global/static vars │ ├──────────────────────────────────────────┤ │ TEXT — your actual compiled code │ (read-only) LOW ADDRESS └──────────────────────────────────────────┘ Stack overflow = when the stack grows into the heap. RIP program. Use heap (malloc) for large or dynamically-sized data. Use stack for small, short-lived local variables. */ int global = 5; // DATA segment int uninit; // BSS segment (gets zeroed) int main() { int local = 10; // STACK int* h = malloc(4); // HEAP (the pointer itself is on stack though!) free(h); }
// "Undefined Behavior" (UB) = C standard literally doesn't define what happens // it might work, crash, corrupt data, or make demons fly out of your nose // compilers can assume UB never happens and optimize AGGRESSIVELY around it // signed integer overflow (wraps in most cases but NOT guaranteed) int x = INT_MAX; x + 1; // UB. could be anything. // accessing array out of bounds int arr[5]; arr[10] = 42; // writing to random memory. corrupts stuff silently. // dereferencing NULL or freed pointer int* p = NULL; *p = 5; // segfault. program dead. // use-after-free free(p); *p = 5; // UB. you freed it! memory belongs to OS now. // double-free free(p); free(p); // UB. could corrupt malloc's internal structures. // data races (in multithreaded code) // USE TOOLS: -fsanitize=address,undefined when compiling to catch this stuff // gcc -fsanitize=address,undefined -g your_file.c -o out
-fsanitize=address,undefined) to find UB before it finds you.// volatile — tells compiler "don't optimize this. it can change externally." // used for hardware registers, shared memory, signal handlers volatile int sensorVal; // compiler won't cache this in a register volatile int* reg = (int*)0xDEADBEEF; // memory-mapped hardware register // without volatile, the compiler might optimize this loop away thinking // the value never changes (because it doesn't change in THIS code) while (sensorVal == 0) {} // waits for hardware to set sensorVal != 0 // restrict — pointer is the ONLY way to access this memory // enables compiler optimizations (no aliasing assumptions needed) // common in high-perf code like memcpy implementations void fastCopy(int restrict *dst, const int restrict *src, int n) { for (int i = 0; i < n; i++) dst[i] = src[i]; } // inline — suggests compiler paste the function body in place of the call // eliminates function call overhead. good for tiny hot functions. static inline int clamp(int val, int lo, int hi) { return val < lo ? lo : (val > hi ? hi : val); }
#include <setjmp.h> // setjmp/longjmp = non-local goto. like try/catch but cursed. // setjmp saves the CPU state. longjmp teleports back to that state. // used in interpreters, error recovery, and deeply nested escape patterns. jmp_buf env; // stores the CPU state (registers, stack pointer, etc) void riskyFunction() { printf("About to do something dumb...\n"); longjmp(env, 1); // teleport back to where setjmp was called, return value 1 printf("This line never executes\n"); } int main() { int val = setjmp(env); // returns 0 first time. after longjmp, returns whatever you passed. if (val == 0) { printf("Normal execution\n"); riskyFunction(); } else { printf("Recovered! longjmp returned %d\n", val); // prints this } return 0; } // WARNING: destructors, VLAs, and local vars modified between setjmp/longjmp // can have undefined behavior. this is powerful but genuinely spooky to use.
longjmp skips destructors and cleanup code. If you do use it, make sure you know exactly what state you're jumping back to.// BIT FIELDS — pack multiple small values into one int // saves memory. used in protocols, hardware, OS kernels struct Flags { unsigned int isActive : 1; // only uses 1 bit unsigned int isAdmin : 1; // 1 bit unsigned int priority : 4; // 4 bits (values 0-15) unsigned int padding : 2; // explicit padding to byte-align }; // whole thing fits in 1 byte instead of 12! // UNION — multiple fields sharing the SAME memory // only ONE field is valid at a time. size = size of largest member union Data { int i; // 4 bytes float f; // 4 bytes — same 4 bytes as i! char c[4]; // 4 bytes — same 4 bytes! overlapping storage }; // writing to .f and then reading .i = type punning. spooky but used in serialization // FLEXIBLE ARRAY MEMBER (C99+) — struct with variable-length tail typedef struct { int count; int data[]; // flexible array — size determined at allocation time } Buffer; Buffer* makeBuffer(int n) { return malloc(sizeof(Buffer) + n * sizeof(int)); // allocate header + n ints }
// C has no templates. but void* lets you write type-agnostic functions. // qsort from stdlib.h uses this — it sorts ANYTHING #include <stdlib.h> // comparison function: returns neg, 0, or pos (like strcmp) int compareInts(const void* a, const void* b) { return (*(int*)a) - (*(int*)b); // cast void* to int*, then dereference. feels gross but it works. } int arr[] = {5, 2, 8, 1, 9}; qsort(arr, 5, sizeof(int), compareInts); // sorts ascending // now arr = {1, 2, 5, 8, 9} // X-Macro trick — generate repetitive code from a list // define the list once, use it in multiple contexts #define COLORS \ X(RED, 0) \ X(GREEN, 1) \ X(BLUE, 2) // generate the enum from the list typedef enum { #define X(name, val) name = val, COLORS #undef X } Color; // generate string table from the SAME list const char* colorNames[] = { #define X(name, val) [val] = #name, COLORS #undef X };
/* ══════════════════════════════════════════════════════════════ RULE 1 — No complex flow constructs (goto, setjmp, longjmp) Simple control flow = auditable code. goto is literally banned. Every function should have a single, obvious execution path. ══════════════════════════════════════════════════════════════ */ // BANNED — don't even think about it goto cleanup; // Use structured cleanup instead int process(int n) { int *buf = malloc(n * sizeof(int)); if (buf == NULL) return -1; // early return = structured, readable // ... do stuff ... free(buf); return 0; }
/* ══════════════════════════════════════════════════════════════ RULE 2 — All loops must have a fixed upper bound An infinite loop is a potential system hang. Unacceptable. If the verifier can't prove the loop terminates, it's banned. ══════════════════════════════════════════════════════════════ */ #define MAX_ITER 1000 // explicit upper bound. always. // BANNED — while(1) with no guaranteed exit while (1) { /* could run forever */ } // Bounded loop with safety counter int iter = 0; while (condition && iter < MAX_ITER) { // ... do work ... iter++; } // even if condition never flips, we exit after MAX_ITER. guaranteed. if (iter >= MAX_ITER) { logError("Max iterations exceeded — something is very wrong"); }
/* ══════════════════════════════════════════════════════════════ RULE 3 — No dynamic memory allocation after initialization malloc at runtime = unpredictable timing, fragmentation, OOM. In hard real-time systems, you allocate everything up front. ══════════════════════════════════════════════════════════════ */ // BANNED in flight software — malloc during mission void handle_event() { int* buf = malloc(100); // NOPE. timing is nondeterministic. } // Pre-allocated static pool — allocate ONCE at startup #define POOL_SIZE 1024 static uint8_t memPool[POOL_SIZE]; // fixed-size memory arena static size_t poolOffset = 0; void* poolAlloc(size_t size) { if (poolOffset + size > POOL_SIZE) return NULL; // out of pool void* ptr = memPool + poolOffset; poolOffset += size; return ptr; }
/* ══════════════════════════════════════════════════════════════ RULE 4 — No function longer than 60 lines (1 screen) If you can't see the whole function at once, it's too complex. Break it up. Each function = one well-defined task. Period. ══════════════════════════════════════════════════════════════ */ // A 300-line function with nested ifs 5 deep = instant reject at code review // Small, focused functions. each does ONE thing and does it well. static bool validateInput(int val) { return val >= 0 && val < 100; } static int processVal(int val) { return val * 2 + 1; } static void outputResult(int res) { printf("%d\n", res); } void doWork(int input) { if (!validateInput(input)) return; int result = processVal(input); outputResult(result); }
/* ══════════════════════════════════════════════════════════════ RULE 5 — Minimum 2 assertions per function assert() checks invariants. if they fire = bug caught early. Assertions are the "sanity checks that you thought were obvious." ══════════════════════════════════════════════════════════════ */ #include <assert.h> #include <stdint.h> int32_t divide(int32_t num, int32_t den) { assert(den != 0); // precondition: can't divide by zero assert(num != INT32_MIN || den != -1); // INT32_MIN / -1 overflows int32_t result = num / den; // postcondition: verify result is in expected range assert(result * den == num || 1); // sanity check return result; } // for production flight software, use custom assert that logs and recovers: #define ASSERT(cond, msg) \ do { if (!(cond)) { log_critical(msg); safe_shutdown(); } } while(0)
/* ══════════════════════════════════════════════════════════════ RULE 6 — Declare variables at smallest possible scope Keep variables close to where they're used. Less surface area for bugs. Easier to reason about. Easier to audit. ══════════════════════════════════════════════════════════════ */ // Old C89 style — all vars at top of function (don't do this) void old_style() { int i, j, sum, count, tmp; // what are these even for? who knows anymore. // ... 100 lines later ... sum = 0; } // C99 style — declare where you need it void new_style() { for (int i = 0; i < 10; i++) { // i only exists inside this for loop int doubled = i * 2; // doubled only exists here printf("%d\n", doubled); } }
/* ══════════════════════════════════════════════════════════════ RULE 7 — Check return values of ALL non-void functions If a function can fail, assume it WILL fail eventually. Ignoring return values = ignoring errors = mystery crashes later. ══════════════════════════════════════════════════════════════ */ // BANNED — ignoring return values fclose(file); // fclose can fail (flush error). you don't care? malloc(1000); // allocated memory and threw the pointer away lol scanf("%d", &x); // what if scanf failed? x has garbage value. // Check everything. everything. if (fclose(file) != 0) { perror("fclose failed"); // handle appropriately } int* buf = malloc(1000); if (buf == NULL) { return ERR_NOMEM; } int n = scanf("%d", &x); if (n != 1) { return ERR_BADINPUT; } // scanf returns # items read // use GCC's __attribute__((warn_unused_result)) to enforce this in your APIs __attribute__((warn_unused_result)) int criticalOp(void) { return -1; } // compiler warns if caller ignores return
/* ══════════════════════════════════════════════════════════════ RULE 8 — Limit preprocessor use. No macros for functions. Macros have no type checking, no scope, expand unexpectedly. Use inline functions or const variables instead where possible. ══════════════════════════════════════════════════════════════ */ // Macro function — dangerous, no type check, double-evaluation #define MAX(a,b) ((a) > (b) ? (a) : (b)) MAX(x++, y) // x++ gets evaluated TWICE. classic macro footgun. // inline function — type-safe, debuggable, no double-eval static inline int max_int(int a, int b) { return a > b ? a : b; } // const instead of #define for constants static const int MAX_BUFFER = 4096; // has type, has scope, shows in debugger #define MAX_BUFFER_BAD 4096 // invisible in debugger, no type check
/* ══════════════════════════════════════════════════════════════ RULE 9 — Limit pointer use. No function pointers. No >1 level of pointer dereferencing. Pointers are error-prone. Deeply nested pointer chains = auditing nightmare. ══════════════════════════════════════════════════════════════ */ // too many levels of indirection int*** triplePtr; // what the hell even is this. who hurt you. // function pointers in flight code (makes static analysis hard) void (*handler)(int); // banned — static analyzer can't trace this // max one level in regular code int* ptr = &x; // fine int** pptr; // only when absolutely necessary (e.g., passing pointer to pointer) // for struct members, a single level is fine Person* p = &person; p->age = 17; // one dereference. clear. auditable.
/* ══════════════════════════════════════════════════════════════ RULE 10 — Compile with all warnings enabled. Zero warnings. Warnings are the compiler saying "this looks sketchy." In safety-critical code, warnings are ERRORS. All of them. ══════════════════════════════════════════════════════════════ */ // The full compile flags you should use for any serious C project: // gcc -std=c11 — use C11 standard // -Wall -Wextra -Wpedantic — enable all the warnings // -Werror — treat warnings as errors (zero tolerance) // -Wshadow — warn about variable shadowing // -Wconversion — warn about implicit type conversions // -Wformat=2 — extra format string checks // -fstack-protector-strong — detect stack smashing at runtime // -D_FORTIFY_SOURCE=2 — harden string/memory operations // -fsanitize=address,undefined — catch memory bugs + UB during testing // -fanalyzer — GCC's static analysis pass // // Short version: gcc -std=c11 -Wall -Wextra -Werror -Wpedantic -g file.c -o out // // Also run these tools regularly: // - valgrind ./out — memory leak detection // - clang-tidy — static analysis // - cppcheck — another static analyzer (catches different stuff) // - splint — annotation-based analysis (used at JPL)
| Specifier | Type | Example |
|---|---|---|
| %d / %i | int (signed decimal) | printf("%d", 42) → "42" |
| %u | unsigned int | printf("%u", 42u) |
| %ld | long int | printf("%ld", 1234567L) |
| %lld | long long int | printf("%lld", big) |
| %f | float/double | printf("%.2f", 3.14) → "3.14" |
| %e | scientific notation | printf("%e", 12345.0) → "1.23e+04" |
| %c | char | printf("%c", 'A') → "A" |
| %s | string (char*) | printf("%s", "yo") |
| %p | pointer address | printf("%p", ptr) |
| %x / %X | hex (lower/upper) | printf("%x", 255) → "ff" |
| %o | octal | printf("%o", 8) → "10" |
| %zu | size_t | printf("%zu", sizeof(x)) |
| %% | literal % | printf("100%%") |
| Escape | Meaning |
|---|---|
| \n | Newline (enter key) |
| \t | Tab |
| \r | Carriage return |
| \\ | Literal backslash |
| \" | Literal double quote inside string |
| \' | Literal single quote |
| \0 | Null character (string terminator) |
| \a | Bell (plays beep sound lol) |
| Header | What it gives you |
|---|---|
| <stdio.h> | printf, scanf, FILE, fopen, fclose |
| <stdlib.h> | malloc, free, rand, exit, atoi |
| <string.h> | strlen, strcpy, strcmp, memcpy |
| <math.h> | sqrt, pow, sin, cos, fabs |
| <stdbool.h> | bool, true, false (C99+) |
| <stdint.h> | int8_t, uint32_t, int64_t etc. |
| <limits.h> | INT_MAX, INT_MIN, CHAR_MAX |
| <assert.h> | assert() |
| <errno.h> | errno, EINVAL, ENOMEM etc. |
| <time.h> | time(), clock(), struct tm |
| <pthread.h> | threads, mutexes (POSIX) |