From cd961958956c414e98c43dba111c9d410f4414ef Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 14:45:43 +0530 Subject: [PATCH 01/15] Revise introduction in README for clarity and emphasis on learning approach --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 874615b..7d14c8d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # CNoBS -**C without the BS.** No 400-page books. No "hello world" for 3 chapters. Just code that runs, breaks, and teaches you why. +> **C, No BS.** That's it. That's the name. + +**C without the bullshit.** No 400-page books. No "hello world" for 3 chapters. No theory dumps. Code that runs, breaks, and teaches you why — in 5 minutes or it's a bad lesson. > You have 20 minutes between classes. You learn by breaking things. You quit if the lesson doesn't pay off in 5. This repo is for you. From 1ccc3be48eaefa294805b5a04dbc44f1de79d28b Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:01 +0530 Subject: [PATCH 02/15] chore: restructure repo into topic-based dirs + extend .gitignore --- .gitignore | 10 +++++++++- challenges/{ => tier-1-foundational}/01-fix-the-loop.c | 0 .../01-swap-puzzle.c} | 0 .../01-types-and-output.c} | 0 .../01-address-and-dereference.c} | 0 .../03-arrays-as-pointers.c} | 0 .../{04-structs.c => 05-structs/01-basic-structs.c} | 0 7 files changed, 9 insertions(+), 1 deletion(-) rename challenges/{ => tier-1-foundational}/01-fix-the-loop.c (100%) rename challenges/{02-pointer-puzzle.c => tier-2-pointers/01-swap-puzzle.c} (100%) rename lessons/{01-output-first.c => 00-basics/01-types-and-output.c} (100%) rename lessons/{02-pointers-visual.c => 02-pointers/01-address-and-dereference.c} (100%) rename lessons/{03-arrays-memory.c => 02-pointers/03-arrays-as-pointers.c} (100%) rename lessons/{04-structs.c => 05-structs/01-basic-structs.c} (100%) diff --git a/.gitignore b/.gitignore index d960d3a..3e0a213 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,12 @@ a.out *.o *.obj -# Phonebook project data +# Test runner artifacts +tests/.build/ + +# Project local data projects/phonebook/book.txt +projects/guess/best_score.txt # Editor / OS .vscode/ @@ -19,3 +23,7 @@ Thumbs.db *.dSYM/ *.pdb *.ilk + +# Solutions hidden until requested +challenges/solutions/*.c +!challenges/solutions/README.md diff --git a/challenges/01-fix-the-loop.c b/challenges/tier-1-foundational/01-fix-the-loop.c similarity index 100% rename from challenges/01-fix-the-loop.c rename to challenges/tier-1-foundational/01-fix-the-loop.c diff --git a/challenges/02-pointer-puzzle.c b/challenges/tier-2-pointers/01-swap-puzzle.c similarity index 100% rename from challenges/02-pointer-puzzle.c rename to challenges/tier-2-pointers/01-swap-puzzle.c diff --git a/lessons/01-output-first.c b/lessons/00-basics/01-types-and-output.c similarity index 100% rename from lessons/01-output-first.c rename to lessons/00-basics/01-types-and-output.c diff --git a/lessons/02-pointers-visual.c b/lessons/02-pointers/01-address-and-dereference.c similarity index 100% rename from lessons/02-pointers-visual.c rename to lessons/02-pointers/01-address-and-dereference.c diff --git a/lessons/03-arrays-memory.c b/lessons/02-pointers/03-arrays-as-pointers.c similarity index 100% rename from lessons/03-arrays-memory.c rename to lessons/02-pointers/03-arrays-as-pointers.c diff --git a/lessons/04-structs.c b/lessons/05-structs/01-basic-structs.c similarity index 100% rename from lessons/04-structs.c rename to lessons/05-structs/01-basic-structs.c From 807d76e569d7adab754b3c587013987b3c412eff Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:09 +0530 Subject: [PATCH 03/15] feat(lessons): add 19 lessons across 8 topics with topic READMEs --- lessons/00-basics/02-operators.c | 42 +++++++++++++ lessons/00-basics/03-control-flow.c | 52 ++++++++++++++++ lessons/00-basics/README.md | 9 +++ lessons/01-functions/01-simple-functions.c | 43 +++++++++++++ lessons/01-functions/02-scope-and-stack.c | 63 ++++++++++++++++++++ lessons/01-functions/README.md | 8 +++ lessons/02-pointers/02-pointer-arithmetic.c | 47 +++++++++++++++ lessons/02-pointers/04-function-pointers.c | 47 +++++++++++++++ lessons/02-pointers/05-double-pointers.c | 55 +++++++++++++++++ lessons/02-pointers/06-common-pointer-bugs.c | 57 ++++++++++++++++++ lessons/02-pointers/README.md | 14 +++++ lessons/03-memory/01-stack-vs-heap.c | 58 ++++++++++++++++++ lessons/03-memory/02-malloc-free.c | 49 +++++++++++++++ lessons/03-memory/README.md | 10 ++++ lessons/04-strings/01-char-arrays.c | 48 +++++++++++++++ lessons/04-strings/02-string-functions.c | 47 +++++++++++++++ lessons/04-strings/03-dangerous-strcpy.c | 47 +++++++++++++++ lessons/04-strings/README.md | 9 +++ lessons/05-structs/02-nested-structs.c | 55 +++++++++++++++++ lessons/05-structs/README.md | 6 ++ lessons/06-file-io/01-fopen-fread.c | 53 ++++++++++++++++ lessons/06-file-io/02-fwrite-append.c | 38 ++++++++++++ lessons/06-file-io/README.md | 6 ++ lessons/07-advanced/01-typedef-enums.c | 49 +++++++++++++++ lessons/07-advanced/02-unions.c | 41 +++++++++++++ lessons/07-advanced/03-bit-manipulation.c | 52 ++++++++++++++++ lessons/07-advanced/README.md | 7 +++ 27 files changed, 1012 insertions(+) create mode 100644 lessons/00-basics/02-operators.c create mode 100644 lessons/00-basics/03-control-flow.c create mode 100644 lessons/00-basics/README.md create mode 100644 lessons/01-functions/01-simple-functions.c create mode 100644 lessons/01-functions/02-scope-and-stack.c create mode 100644 lessons/01-functions/README.md create mode 100644 lessons/02-pointers/02-pointer-arithmetic.c create mode 100644 lessons/02-pointers/04-function-pointers.c create mode 100644 lessons/02-pointers/05-double-pointers.c create mode 100644 lessons/02-pointers/06-common-pointer-bugs.c create mode 100644 lessons/02-pointers/README.md create mode 100644 lessons/03-memory/01-stack-vs-heap.c create mode 100644 lessons/03-memory/02-malloc-free.c create mode 100644 lessons/03-memory/README.md create mode 100644 lessons/04-strings/01-char-arrays.c create mode 100644 lessons/04-strings/02-string-functions.c create mode 100644 lessons/04-strings/03-dangerous-strcpy.c create mode 100644 lessons/04-strings/README.md create mode 100644 lessons/05-structs/02-nested-structs.c create mode 100644 lessons/05-structs/README.md create mode 100644 lessons/06-file-io/01-fopen-fread.c create mode 100644 lessons/06-file-io/02-fwrite-append.c create mode 100644 lessons/06-file-io/README.md create mode 100644 lessons/07-advanced/01-typedef-enums.c create mode 100644 lessons/07-advanced/02-unions.c create mode 100644 lessons/07-advanced/03-bit-manipulation.c create mode 100644 lessons/07-advanced/README.md diff --git a/lessons/00-basics/02-operators.c b/lessons/00-basics/02-operators.c new file mode 100644 index 0000000..a9c8008 --- /dev/null +++ b/lessons/00-basics/02-operators.c @@ -0,0 +1,42 @@ +/* + * Lesson: Operators | Time: 10 min | Difficulty: easy + * Why this matters: Every program does math + logic. Bitwise looks scary, + * it's not. You'll see it in flags, masks, protocols. + * + * Run: gcc 02-operators.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * 7 + 2 = 9 + * 7 / 2 = 3 (int div truncates) + * 7 % 2 = 1 + * (5 > 3) && (2 < 4) = 1 + * !0 = 1 + * 5 & 3 = 1 + * 5 | 3 = 7 + * 5 ^ 3 = 6 + * 1 << 3 = 8 + * ------------------------------------------ + */ + +#include + +int main(void) { + printf("7 + 2 = %d\n", 7 + 2); + printf("7 / 2 = %d (int div truncates)\n", 7 / 2); + printf("7 %% 2 = %d\n", 7 % 2); + printf("(5 > 3) && (2 < 4) = %d\n", (5 > 3) && (2 < 4)); + printf("!0 = %d\n", !0); + printf("5 & 3 = %d\n", 5 & 3); + printf("5 | 3 = %d\n", 5 | 3); + printf("5 ^ 3 = %d\n", 5 ^ 3); + printf("1 << 3 = %d\n", 1 << 3); + return 0; +} + +/* + * BREAK IT: + * 1. Try `7 / 2.0`. Different result. Why? (Promotion to double.) + * 2. Predict: `1 << 31` on a 32-bit int. Overflow. Sign bit. Welcome to UB. + * 3. `5 & 1` tells you if 5 is odd. Use it instead of `% 2`. Faster, ancient trick. + */ diff --git a/lessons/00-basics/03-control-flow.c b/lessons/00-basics/03-control-flow.c new file mode 100644 index 0000000..b4d94b8 --- /dev/null +++ b/lessons/00-basics/03-control-flow.c @@ -0,0 +1,52 @@ +/* + * Lesson: Control Flow | Time: 10 min | Difficulty: easy + * Why this matters: if/else/switch/while/for. That's 95% of code branching. + * + * Run: gcc 03-control-flow.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * grade: B + * day 3 = Wed + * evens: 0 2 4 6 8 + * sum 1..5 = 15 + * ------------------------------------------ + */ + +#include + +int main(void) { + int score = 82; + char grade; + if (score >= 90) grade = 'A'; + else if (score >= 80) grade = 'B'; + else if (score >= 70) grade = 'C'; + else grade = 'F'; + printf("grade: %c\n", grade); + + int day = 3; + printf("day %d = ", day); + switch (day) { + case 1: printf("Mon\n"); break; + case 2: printf("Tue\n"); break; + case 3: printf("Wed\n"); break; + default: printf("?\n"); + } + + printf("evens:"); + for (int i = 0; i < 10; i += 2) printf(" %d", i); + printf("\n"); + + int n = 1, sum = 0; + while (n <= 5) { sum += n; n++; } + printf("sum 1..5 = %d\n", sum); + + return 0; +} + +/* + * BREAK IT: + * 1. Remove `break;` from a switch case. Fallthrough. Sometimes intentional. + * 2. Change `while (n <= 5)` to `while (n < 5)`. Off-by-one. + * 3. Use `do { } while (...)` to force one iteration even if condition false. + */ diff --git a/lessons/00-basics/README.md b/lessons/00-basics/README.md new file mode 100644 index 0000000..c728a1c --- /dev/null +++ b/lessons/00-basics/README.md @@ -0,0 +1,9 @@ +# 00 — Basics + +Types, output, operators, control flow. Building blocks. No memory tricks yet. + +1. [01-types-and-output.c](01-types-and-output.c) — printf, int, basic loop +2. [02-operators.c](02-operators.c) — arithmetic, logical, bitwise +3. [03-control-flow.c](03-control-flow.c) — if/else, switch, while, for + +Done? Go to [../01-functions/](../01-functions/). diff --git a/lessons/01-functions/01-simple-functions.c b/lessons/01-functions/01-simple-functions.c new file mode 100644 index 0000000..1c716eb --- /dev/null +++ b/lessons/01-functions/01-simple-functions.c @@ -0,0 +1,43 @@ +/* + * Lesson: Simple Functions | Time: 10 min | Difficulty: easy + * Why this matters: Functions = reusable code blocks with names. Same as + * every other language, except return type up front. + * + * Run: gcc 01-simple-functions.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * square(5) = 25 + * max(7, 12) = 12 + * factorial(5) = 120 + * ------------------------------------------ + */ + +#include + +int square(int x) { return x * x; } + +int max(int a, int b) { return a > b ? a : b; } + +int factorial(int n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +} + +int main(void) { + printf("square(5) = %d\n", square(5)); + printf("max(7, 12) = %d\n", max(7, 12)); + printf("factorial(5) = %d\n", factorial(5)); + return 0; +} + +/* + * Why it works: + * `int square(int x)` = returns int, takes one int. + * Recursion: function calls itself. Base case stops it. + * + * BREAK IT: + * 1. Call `factorial(-1)`. Infinite recursion. Stack overflow. Segfault. + * 2. Forget the return statement in square. Compiler warning. Garbage result. + * 3. Declare `square` AFTER main without a prototype. Pre-C99 broke. Modern: warn. + */ diff --git a/lessons/01-functions/02-scope-and-stack.c b/lessons/01-functions/02-scope-and-stack.c new file mode 100644 index 0000000..4554871 --- /dev/null +++ b/lessons/01-functions/02-scope-and-stack.c @@ -0,0 +1,63 @@ +/* + * Lesson: Scope and Stack | Time: 12 min | Difficulty: medium + * Why this matters: Local vars die when the function returns. Returning a + * pointer to a local var = bug that bites you 6 months later. + * + * Run: gcc 02-scope-and-stack.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * outer x = 10 + * inner x = 20 + * outer x still = 10 + * counter call 1 = 1 + * counter call 2 = 2 + * counter call 3 = 3 + * ------------------------------------------ + * + * STACK during nested call: + * + * +-------------+ + * | main: x=10 | <-- outer frame + * +-------------+ + * | inner: x=20 | <-- new frame, shadows outer x + * +-------------+ <-- top of stack + * + * When inner returns, its frame is popped. Its `x` is gone. + */ + +#include + +void inner(void) { + int x = 20; + printf("inner x = %d\n", x); +} + +int counter(void) { + static int n = 0; + n++; + return n; +} + +int main(void) { + int x = 10; + printf("outer x = %d\n", x); + inner(); + printf("outer x still = %d\n", x); + + printf("counter call 1 = %d\n", counter()); + printf("counter call 2 = %d\n", counter()); + printf("counter call 3 = %d\n", counter()); + return 0; +} + +/* + * Why it works: + * Each function call = new stack frame. Locals live there. Pop on return. + * `static` inside a function = lives forever, value persists across calls. + * + * BREAK IT: + * 1. Write `int* bad(void) { int n = 7; return &n; }` and use it. UB, garbage, + * maybe segfault. Don't return addresses of locals. + * 2. Remove `static` from counter's n. Always prints 1. Why? + */ diff --git a/lessons/01-functions/README.md b/lessons/01-functions/README.md new file mode 100644 index 0000000..0c6cec8 --- /dev/null +++ b/lessons/01-functions/README.md @@ -0,0 +1,8 @@ +# 01 — Functions + +Define, call, return. Recursion. Scope and the stack. + +1. [01-simple-functions.c](01-simple-functions.c) — params, return, recursion +2. [02-scope-and-stack.c](02-scope-and-stack.c) — locals die on return, `static` survives + +Next: [../02-pointers/](../02-pointers/) — the hard part. diff --git a/lessons/02-pointers/02-pointer-arithmetic.c b/lessons/02-pointers/02-pointer-arithmetic.c new file mode 100644 index 0000000..eec38eb --- /dev/null +++ b/lessons/02-pointers/02-pointer-arithmetic.c @@ -0,0 +1,47 @@ +/* + * Lesson: Pointer Arithmetic | Time: 12 min | Difficulty: medium + * Why this matters: `p + 1` doesn't add 1 byte. It adds sizeof(*p) bytes. + * Once you see this, arrays + buffers click. + * + * Run: gcc 02-pointer-arithmetic.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * nums[0] via *p = 10 + * nums[1] via *(p+1) = 20 + * nums[2] via *(p+2) = 30 + * diff in elements = 3 + * ------------------------------------------ + * + * MEMORY (int = 4 bytes here): + * + * addr: 0x100 0x104 0x108 0x10c + * +----+ +----+ +----+ +----+ + * | 10 | | 20 | | 30 | | 40 | + * +----+ +----+ +----+ +----+ + * ^ ^ + * p p+3 + * + * p+1 = 0x104, NOT 0x101. Compiler scales by sizeof(int). + */ + +#include + +int main(void) { + int nums[4] = {10, 20, 30, 40}; + int *p = nums; + int *end = nums + 3; + + printf("nums[0] via *p = %d\n", *p); + printf("nums[1] via *(p+1) = %d\n", *(p + 1)); + printf("nums[2] via *(p+2) = %d\n", *(p + 2)); + printf("diff in elements = %ld\n", (long)(end - p)); + return 0; +} + +/* + * BREAK IT: + * 1. Print `(void*)p` and `(void*)(p+1)`. Diff is 4, not 1. + * 2. Subtract two pointers from different arrays. UB. + * 3. `char *cp = (char*)p; cp+1` moves 1 byte. Cast changes the step. + */ diff --git a/lessons/02-pointers/04-function-pointers.c b/lessons/02-pointers/04-function-pointers.c new file mode 100644 index 0000000..7fb4aff --- /dev/null +++ b/lessons/02-pointers/04-function-pointers.c @@ -0,0 +1,47 @@ +/* + * Lesson: Function Pointers | Time: 12 min | Difficulty: medium + * Why this matters: Callbacks. qsort uses them. Every event loop uses them. + * Syntax looks like alphabet soup. It's not. + * + * Run: gcc 04-function-pointers.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * op(3,4) via add = 7 + * op(3,4) via mul = 12 + * apply twice = 16 + * ------------------------------------------ + */ + +#include + +int add(int a, int b) { return a + b; } +int mul(int a, int b) { return a * b; } + +int apply(int (*op)(int, int), int x, int y) { + return op(x, y); +} + +int main(void) { + int (*f)(int, int) = add; + printf("op(3,4) via add = %d\n", apply(f, 3, 4)); + + f = mul; + printf("op(3,4) via mul = %d\n", apply(f, 3, 4)); + + printf("apply twice = %d\n", apply(mul, apply(add, 1, 3), 4)); + return 0; +} + +/* + * Read the type: + * int (*f)(int, int) + * ^ ^------^ + * | params + * pointer to function returning int + * + * BREAK IT: + * 1. Drop the parens: `int *f(int, int)` = function returning int*. Different! + * 2. Build an array of function pointers: `int (*ops[2])(int,int) = {add, mul};` + * 3. Pass `apply` to itself. C lets you. + */ diff --git a/lessons/02-pointers/05-double-pointers.c b/lessons/02-pointers/05-double-pointers.c new file mode 100644 index 0000000..ba0cb7f --- /dev/null +++ b/lessons/02-pointers/05-double-pointers.c @@ -0,0 +1,55 @@ +/* + * Lesson: Double Pointers | Time: 12 min | Difficulty: medium + * Why this matters: When a function needs to REASSIGN a caller's pointer + * (e.g., allocate memory and hand it back), you need `T**`. + * + * Run: gcc 05-double-pointers.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * before: p = (null) + * after: *p = 42 + * argv[0] starts with: ./ + * ------------------------------------------ + * + * MEMORY (after alloc_int sets *pp): + * + * main: + * p ---------+ + * v + * +----+ + * | 42 | <-- heap + * +----+ + * + * alloc_int(&p): pp points to p, *pp = the heap block. + */ + +#include +#include + +void alloc_int(int **pp, int val) { + *pp = malloc(sizeof(int)); + **pp = val; +} + +int main(int argc, char **argv) { + int *p = NULL; + printf("before: p = %s\n", p ? "set" : "(null)"); + alloc_int(&p, 42); + printf("after: *p = %d\n", *p); + free(p); + + (void)argc; + printf("argv[0] starts with: %c%c\n", argv[0][0], argv[0][1]); + return 0; +} + +/* + * Why argv is char**: + * It's an array of strings. Each string = char*. Array-of = T**. + * + * BREAK IT: + * 1. Pass `p` (not `&p`) to alloc_int. Caller's p never updates. Why? + * 2. Forget the `free(p)`. Leak. Valgrind would yell. + * 3. Triple pointers exist (T***). Rare. Usually a sign of bad design. + */ diff --git a/lessons/02-pointers/06-common-pointer-bugs.c b/lessons/02-pointers/06-common-pointer-bugs.c new file mode 100644 index 0000000..01e6e09 --- /dev/null +++ b/lessons/02-pointers/06-common-pointer-bugs.c @@ -0,0 +1,57 @@ +/* + * Lesson: Common Pointer Bugs | Time: 15 min | Difficulty: medium + * Why this matters: Null deref, uninitialized, use-after-free. The three + * horsemen of C segfaults. See them once, recognize them forever. + * + * Run: gcc 06-common-pointer-bugs.c && ./a.out + * + * This file SHOWS the bugs as comments. The runnable code does the SAFE version. + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * safe: value = 7 + * freed pointer set to NULL (defensive) + * ------------------------------------------ + */ + +#include +#include + +int main(void) { + /* BUG 1: null deref + * int *p = NULL; + * *p = 5; // segfault + * Fix: check before deref. + */ + + /* BUG 2: uninitialized pointer (wild) + * int *p; // garbage address + * *p = 5; // writes to who knows where + * Fix: always init to NULL or a real address. + */ + + int *p = malloc(sizeof(int)); + if (!p) return 1; + *p = 7; + printf("safe: value = %d\n", *p); + + /* BUG 3: use-after-free + * free(p); + * *p = 9; // UB. heap corruption. + * Fix: set p = NULL after free. + */ + free(p); + p = NULL; + printf("freed pointer set to NULL (defensive)\n"); + + return 0; +} + +/* + * Mental checklist before touching a pointer: + * 1. Is it NULL? -> don't deref. + * 2. Has it been initialized? -> if not, init now. + * 3. Has it been freed? -> if yes, set to NULL. + * + * See ../../mnemonics/POINTERS_101.txt + */ diff --git a/lessons/02-pointers/README.md b/lessons/02-pointers/README.md new file mode 100644 index 0000000..debce00 --- /dev/null +++ b/lessons/02-pointers/README.md @@ -0,0 +1,14 @@ +# 02 — Pointers + +The hard part. Take it slow. Each lesson has a memory diagram. + +1. [01-address-and-dereference.c](01-address-and-dereference.c) — `&` and `*` +2. [02-pointer-arithmetic.c](02-pointer-arithmetic.c) — `p+1` adds `sizeof(*p)` bytes +3. [03-arrays-as-pointers.c](03-arrays-as-pointers.c) — array name = first element address +4. [04-function-pointers.c](04-function-pointers.c) — callbacks +5. [05-double-pointers.c](05-double-pointers.c) — `T**`, why argv exists +6. [06-common-pointer-bugs.c](06-common-pointer-bugs.c) — null / wild / use-after-free + +One-page memory aid: [../../mnemonics/POINTERS_101.txt](../../mnemonics/POINTERS_101.txt) + +Next: [../03-memory/](../03-memory/) — malloc and the heap. diff --git a/lessons/03-memory/01-stack-vs-heap.c b/lessons/03-memory/01-stack-vs-heap.c new file mode 100644 index 0000000..26d942a --- /dev/null +++ b/lessons/03-memory/01-stack-vs-heap.c @@ -0,0 +1,58 @@ +/* + * Lesson: Stack vs Heap | Time: 12 min | Difficulty: medium + * Why this matters: Two places memory lives. Stack = fast, auto, small. + * Heap = manual, big, persists until freed. + * + * Run: gcc 01-stack-vs-heap.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * stack var x = 5 + * heap var *p = 99 + * stack address near top of memory (high) + * heap address near program data (low-ish) + * ------------------------------------------ + * + * BIG PICTURE: + * + * high addr +----------------+ + * | STACK | grows down, auto, fast + * | | | + * | v | + * | | + * | ^ | + * | | | + * | HEAP | malloc/free, manual + * +----------------+ + * | data + code | + * low addr +----------------+ + */ + +#include +#include + +int main(void) { + int x = 5; + int *p = malloc(sizeof(int)); + if (!p) return 1; + *p = 99; + + printf("stack var x = %d\n", x); + printf("heap var *p = %d\n", *p); + printf("stack address near top of memory (high)\n"); + printf("heap address near program data (low-ish)\n"); + + free(p); + return 0; +} + +/* + * Rules: + * - Stack vars die at end of their scope. Don't return their address. + * - Heap vars live until you free() them. Don't forget. + * + * BREAK IT: + * 1. Allocate 1GB on stack: `char big[1<<30];` -> stack overflow. + * 2. Allocate 1GB on heap: usually fine. Heap is huge. + * 3. Forget free. Run in a loop. Watch memory climb. Welcome to leaks. + */ diff --git a/lessons/03-memory/02-malloc-free.c b/lessons/03-memory/02-malloc-free.c new file mode 100644 index 0000000..3654814 --- /dev/null +++ b/lessons/03-memory/02-malloc-free.c @@ -0,0 +1,49 @@ +/* + * Lesson: malloc and free | Time: 12 min | Difficulty: medium + * Why this matters: Every dynamic data structure (lists, trees, hash tables) + * lives on the heap. malloc gets it, free returns it. + * + * Run: gcc 02-malloc-free.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * nums[0]=0 nums[1]=1 nums[2]=4 nums[3]=9 nums[4]=16 + * sum = 30 + * ------------------------------------------ + */ + +#include +#include + +int main(void) { + int n = 5; + int *nums = malloc(n * sizeof(int)); + if (!nums) return 1; + + for (int i = 0; i < n; i++) nums[i] = i * i; + + printf("nums[0]=%d nums[1]=%d nums[2]=%d nums[3]=%d nums[4]=%d\n", + nums[0], nums[1], nums[2], nums[3], nums[4]); + + int sum = 0; + for (int i = 0; i < n; i++) sum += nums[i]; + printf("sum = %d\n", sum); + + free(nums); + return 0; +} + +/* + * Rules of malloc: + * 1. Always check for NULL (out of memory). + * 2. Always pair with exactly ONE free. + * 3. Don't use after free. Don't double free. + * 4. malloc returns void*. Implicit cast to your type. + * + * See ../../mnemonics/MALLOC_RULES.txt + * + * BREAK IT: + * 1. Write to nums[10]. Out of bounds. Heap corruption. Might crash later. + * 2. `free(nums); free(nums);` Double free. Glibc detects, aborts. + * 3. Replace malloc with calloc(n, sizeof(int)). All zeros. Costs a memset. + */ diff --git a/lessons/03-memory/README.md b/lessons/03-memory/README.md new file mode 100644 index 0000000..1b7db50 --- /dev/null +++ b/lessons/03-memory/README.md @@ -0,0 +1,10 @@ +# 03 — Memory + +Where data lives. Manual management. The thing GC languages hide. + +1. [01-stack-vs-heap.c](01-stack-vs-heap.c) — two regions, different rules +2. [02-malloc-free.c](02-malloc-free.c) — allocate, use, return + +Mnemonics: [../../mnemonics/MALLOC_RULES.txt](../../mnemonics/MALLOC_RULES.txt) · [../../mnemonics/STACK_VS_HEAP.txt](../../mnemonics/STACK_VS_HEAP.txt) + +Deep dive: [../../docs/HOW_MALLOC_WORKS.md](../../docs/HOW_MALLOC_WORKS.md) diff --git a/lessons/04-strings/01-char-arrays.c b/lessons/04-strings/01-char-arrays.c new file mode 100644 index 0000000..ae8b770 --- /dev/null +++ b/lessons/04-strings/01-char-arrays.c @@ -0,0 +1,48 @@ +/* + * Lesson: char arrays = C strings | Time: 10 min | Difficulty: easy + * Why this matters: C has no "string" type. A string = char array ending in '\0'. + * That's the whole truth. + * + * Run: gcc 01-char-arrays.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * name = Alice + * length = 5 + * first char = A + * ------------------------------------------ + * + * MEMORY: + * + * char name[] = "Alice"; + * + * index: 0 1 2 3 4 5 + * +----+----+----+----+----+----+ + * | A | l | i | c | e | \0 | + * +----+----+----+----+----+----+ + * + * The \0 is invisible but stored. It's what marks "end of string." + */ + +#include + +int my_strlen(const char *s) { + int n = 0; + while (s[n] != '\0') n++; + return n; +} + +int main(void) { + char name[] = "Alice"; + printf("name = %s\n", name); + printf("length = %d\n", my_strlen(name)); + printf("first char = %c\n", name[0]); + return 0; +} + +/* + * BREAK IT: + * 1. `char name[5] = "Alice";` no room for \0. printf walks past. Garbage. + * 2. `name[5] = '!';` overwrite the \0. Now strlen never stops. Bug. + * 3. Use `sizeof(name)` vs `strlen(name)`. They differ by 1. + */ diff --git a/lessons/04-strings/02-string-functions.c b/lessons/04-strings/02-string-functions.c new file mode 100644 index 0000000..dbe7225 --- /dev/null +++ b/lessons/04-strings/02-string-functions.c @@ -0,0 +1,47 @@ +/* + * Lesson: string.h functions | Time: 10 min | Difficulty: easy + * Why this matters: strlen, strcpy, strcmp, strcat. Know these four, read 90% of C. + * + * Run: gcc 02-string-functions.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * len = 5 + * copy = hello + * cmp(hi, hi) = 0 + * cmp(a, b) = -1 + * cat = hello world + * ------------------------------------------ + */ + +#include +#include + +int main(void) { + char src[] = "hello"; + char dst[32]; + + printf("len = %zu\n", strlen(src)); + + strcpy(dst, src); + printf("copy = %s\n", dst); + + printf("cmp(hi, hi) = %d\n", strcmp("hi", "hi")); + printf("cmp(a, b) = %d\n", strcmp("a", "b") < 0 ? -1 : 1); + + strcat(dst, " world"); + printf("cat = %s\n", dst); + return 0; +} + +/* + * strcmp returns: + * 0 = equal + * < 0 = first is "less" (lex order) + * > 0 = first is "greater" + * + * BREAK IT: + * 1. `char dst[5]; strcpy(dst, "hello");` buffer overflow. \0 doesn't fit. + * 2. strcat onto a string with no room. Overflow. Use strncat. + * 3. strcmp returns int, NOT bool. `if (strcmp(a,b))` is "if NOT equal". + */ diff --git a/lessons/04-strings/03-dangerous-strcpy.c b/lessons/04-strings/03-dangerous-strcpy.c new file mode 100644 index 0000000..b5a4a17 --- /dev/null +++ b/lessons/04-strings/03-dangerous-strcpy.c @@ -0,0 +1,47 @@ +/* + * Lesson: Why strcpy Is Dangerous | Time: 10 min | Difficulty: medium + * Why this matters: strcpy doesn't check destination size. Morris worm (1988) + * used this. Heartbleed adjacent. Use bounded versions. + * + * Run: gcc 03-dangerous-strcpy.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * safe copy: hello + * truncated: hello wor + * ------------------------------------------ + */ + +#include +#include + +int main(void) { + char buf[16]; + + strcpy(buf, "hello"); + printf("safe copy: %s\n", buf); + + /* DANGER (commented out): + * char tiny[4]; + * strcpy(tiny, "this is too long"); // smashes the stack + */ + + char small[10]; + strncpy(small, "hello world", sizeof(small) - 1); + small[sizeof(small) - 1] = '\0'; + printf("truncated: %s\n", small); + return 0; +} + +/* + * Rules: + * - Never strcpy untrusted input into a fixed buffer. + * - Prefer strncpy + manual \0 termination, or snprintf, or strlcpy. + * - snprintf(buf, sizeof buf, "%s", src) is the modern safe choice. + * + * See ../../mnemonics/STRING_DANGERS.txt + * + * BREAK IT: + * 1. Uncomment the DANGER block. Compile with `-fsanitize=address`. Watch it die. + * 2. Forget the manual \0 after strncpy. Garbage tail. + */ diff --git a/lessons/04-strings/README.md b/lessons/04-strings/README.md new file mode 100644 index 0000000..ebeaa7d --- /dev/null +++ b/lessons/04-strings/README.md @@ -0,0 +1,9 @@ +# 04 — Strings + +Char arrays with `\0`. No string type. No length field. Walk to find the end. + +1. [01-char-arrays.c](01-char-arrays.c) — the null terminator +2. [02-string-functions.c](02-string-functions.c) — strlen, strcpy, strcmp, strcat +3. [03-dangerous-strcpy.c](03-dangerous-strcpy.c) — buffer overflow + safe alternatives + +Mnemonic: [../../mnemonics/STRING_DANGERS.txt](../../mnemonics/STRING_DANGERS.txt) diff --git a/lessons/05-structs/02-nested-structs.c b/lessons/05-structs/02-nested-structs.c new file mode 100644 index 0000000..9cba91d --- /dev/null +++ b/lessons/05-structs/02-nested-structs.c @@ -0,0 +1,55 @@ +/* + * Lesson: Nested Structs | Time: 12 min | Difficulty: medium + * Why this matters: Real data has shape. A Person has an Address. An Address + * has a City. Compose structs. + * + * Run: gcc 02-nested-structs.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * Alice lives in Paris + * via pointer: Alice / Paris + * roster[0] = Bob + * roster[1] = Carol + * ------------------------------------------ + */ + +#include +#include + +struct Address { + char city[32]; +}; + +struct Person { + char name[32]; + struct Address addr; +}; + +int main(void) { + struct Person a; + strcpy(a.name, "Alice"); + strcpy(a.addr.city, "Paris"); + printf("%s lives in %s\n", a.name, a.addr.city); + + struct Person *p = &a; + printf("via pointer: %s / %s\n", p->name, p->addr.city); + + struct Person roster[2]; + strcpy(roster[0].name, "Bob"); + strcpy(roster[1].name, "Carol"); + printf("roster[0] = %s\n", roster[0].name); + printf("roster[1] = %s\n", roster[1].name); + return 0; +} + +/* + * Two ways to access: + * a.field // struct value + * p->field // struct pointer (shorthand for (*p).field) + * + * BREAK IT: + * 1. Add `struct Person *manager;` field. Now you have a graph. + * 2. `sizeof(struct Person)` may include padding. Compiler aligns fields. + * 3. Use `typedef struct { ... } Person;` to skip the `struct` keyword. + */ diff --git a/lessons/05-structs/README.md b/lessons/05-structs/README.md new file mode 100644 index 0000000..332fc52 --- /dev/null +++ b/lessons/05-structs/README.md @@ -0,0 +1,6 @@ +# 05 — Structs + +Your own types. Composition. The foundation of every C data structure. + +1. [01-basic-structs.c](01-basic-structs.c) — define, init, pass by value +2. [02-nested-structs.c](02-nested-structs.c) — structs in structs, arrays of structs, `->` diff --git a/lessons/06-file-io/01-fopen-fread.c b/lessons/06-file-io/01-fopen-fread.c new file mode 100644 index 0000000..41e17f8 --- /dev/null +++ b/lessons/06-file-io/01-fopen-fread.c @@ -0,0 +1,53 @@ +/* + * Lesson: fopen + fgets | Time: 12 min | Difficulty: medium + * Why this matters: Read a file line by line. Building block for every + * config parser, log analyzer, CSV reader. + * + * Run: + * echo -e "line1\nline2\nline3" > sample.txt + * gcc 01-fopen-fread.c && ./a.out + * + * EXPECTED OUTPUT (when sample.txt exists with 3 lines): + * ------------------------------------------ + * line 1: line1 + * line 2: line2 + * line 3: line3 + * ------------------------------------------ + * + * If sample.txt missing: + * could not open sample.txt + */ + +#include + +int main(void) { + FILE *f = fopen("sample.txt", "r"); + if (!f) { + printf("could not open sample.txt\n"); + return 0; + } + + char buf[128]; + int n = 0; + while (fgets(buf, sizeof(buf), f)) { + n++; + size_t len = 0; + while (buf[len] && buf[len] != '\n') len++; + buf[len] = '\0'; + printf("line %d: %s\n", n, buf); + } + fclose(f); + return 0; +} + +/* + * Rules: + * - Always check fopen for NULL. + * - fgets reads up to size-1, keeps the trailing \n if it fits. + * - Always fclose. Leaked FDs are a real bug. + * + * BREAK IT: + * 1. Read with `fgets(buf, 4, f)` on a 10-char line. Splits across reads. + * 2. Open "r+" to read + write. Mode matters. + * 3. Use `feof(f)` as loop condition. Wrong. Use return of fgets. + */ diff --git a/lessons/06-file-io/02-fwrite-append.c b/lessons/06-file-io/02-fwrite-append.c new file mode 100644 index 0000000..479e6e7 --- /dev/null +++ b/lessons/06-file-io/02-fwrite-append.c @@ -0,0 +1,38 @@ +/* + * Lesson: fprintf + append mode | Time: 10 min | Difficulty: easy + * Why this matters: Log files. Append-only state. Configs. + * + * Run: gcc 02-fwrite-append.c && ./a.out + * + * EXPECTED OUTPUT (file `log.txt` gets 2 lines added each run): + * ------------------------------------------ + * wrote 2 lines to log.txt + * ------------------------------------------ + */ + +#include + +int main(void) { + FILE *f = fopen("log.txt", "a"); + if (!f) { printf("open failed\n"); return 1; } + + fprintf(f, "event: start\n"); + fprintf(f, "event: end\n"); + fclose(f); + + printf("wrote 2 lines to log.txt\n"); + return 0; +} + +/* + * Modes: + * "r" read, file must exist + * "w" write, truncates existing + * "a" append, creates if missing + * "r+" read/write + * + * BREAK IT: + * 1. Open with "w" instead of "a". Each run truncates. History gone. + * 2. Remove fclose. On crash, your last writes may not flush. Use fflush. + * 3. Write binary data with fwrite(buf, size, count, f). Same idea, no formatting. + */ diff --git a/lessons/06-file-io/README.md b/lessons/06-file-io/README.md new file mode 100644 index 0000000..d110ee7 --- /dev/null +++ b/lessons/06-file-io/README.md @@ -0,0 +1,6 @@ +# 06 — File I/O + +Read files. Write files. Append. The basics behind every log, config, CSV. + +1. [01-fopen-fread.c](01-fopen-fread.c) — fgets line by line +2. [02-fwrite-append.c](02-fwrite-append.c) — fprintf + append mode diff --git a/lessons/07-advanced/01-typedef-enums.c b/lessons/07-advanced/01-typedef-enums.c new file mode 100644 index 0000000..c12c4df --- /dev/null +++ b/lessons/07-advanced/01-typedef-enums.c @@ -0,0 +1,49 @@ +/* + * Lesson: typedef + enums | Time: 10 min | Difficulty: medium + * Why this matters: typedef gives types short names. enums give names to ints. + * Together: readable code instead of magic numbers. + * + * Run: gcc 01-typedef-enums.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * color = 2 (BLUE) + * status = RUNNING + * ------------------------------------------ + */ + +#include + +typedef enum { RED, GREEN, BLUE } Color; + +typedef struct { + int id; + Color tint; +} Pixel; + +typedef enum { IDLE, RUNNING, DONE } Status; + +const char* status_name(Status s) { + switch (s) { + case IDLE: return "IDLE"; + case RUNNING: return "RUNNING"; + case DONE: return "DONE"; + } + return "?"; +} + +int main(void) { + Pixel p = {1, BLUE}; + printf("color = %d (BLUE)\n", p.tint); + + Status s = RUNNING; + printf("status = %s\n", status_name(s)); + return 0; +} + +/* + * BREAK IT: + * 1. Assign 99 to a Color var. C lets you. enums are just int. + * 2. Force values: `enum { OK=200, NOT_FOUND=404 };` + * 3. Drop the typedef. Write `enum Color c;` everywhere. Annoying. That's why. + */ diff --git a/lessons/07-advanced/02-unions.c b/lessons/07-advanced/02-unions.c new file mode 100644 index 0000000..1040241 --- /dev/null +++ b/lessons/07-advanced/02-unions.c @@ -0,0 +1,41 @@ +/* + * Lesson: unions | Time: 10 min | Difficulty: medium + * Why this matters: Same memory, multiple interpretations. Tagged unions = + * sum types. Used in interpreters, protocols, network packets. + * + * Run: gcc 02-unions.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * as int = 1065353216 + * as float = 1.000000 + * ------------------------------------------ + */ + +#include + +union Bits { + int i; + float f; +}; + +int main(void) { + union Bits b; + b.f = 1.0f; + printf("as int = %d\n", b.i); + printf("as float = %f\n", b.f); + return 0; +} + +/* + * Memory: + * sizeof(union Bits) = max(sizeof(int), sizeof(float)) = 4 + * .i and .f share the SAME 4 bytes. + * + * BREAK IT: + * 1. Set .i then read .f. You see the float's bit pattern. IEEE 754 in action. + * 2. Add a `double d;` field. sizeof grows to 8. + * 3. Tagged union pattern: + * struct Value { int tag; union { int i; float f; } u; }; + * Switch on tag, read matching field. + */ diff --git a/lessons/07-advanced/03-bit-manipulation.c b/lessons/07-advanced/03-bit-manipulation.c new file mode 100644 index 0000000..defad19 --- /dev/null +++ b/lessons/07-advanced/03-bit-manipulation.c @@ -0,0 +1,52 @@ +/* + * Lesson: Bit Manipulation | Time: 12 min | Difficulty: medium + * Why this matters: Flags packed into one int. Fast multiply/divide by 2. + * Hardware registers. Network protocols. + * + * Run: gcc 03-bit-manipulation.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * flags = 0x5 (READ | EXEC) + * has READ? 1 + * has WRITE? 0 + * after add WRITE: 0x7 + * after drop READ: 0x6 + * 12 * 8 via shift = 96 + * ------------------------------------------ + */ + +#include + +#define READ (1 << 0) +#define WRITE (1 << 1) +#define EXEC (1 << 2) + +int main(void) { + int flags = READ | EXEC; + printf("flags = 0x%x (READ | EXEC)\n", flags); + printf("has READ? %d\n", (flags & READ) ? 1 : 0); + printf("has WRITE? %d\n", (flags & WRITE) ? 1 : 0); + + flags |= WRITE; + printf("after add WRITE: 0x%x\n", flags); + + flags &= ~READ; + printf("after drop READ: 0x%x\n", flags); + + printf("12 * 8 via shift = %d\n", 12 << 3); + return 0; +} + +/* + * Bit ops cheat: + * set: x |= MASK + * clear: x &= ~MASK + * toggle: x ^= MASK + * test: x & MASK + * + * BREAK IT: + * 1. Use 1U << 31. Set high bit. Signed shift = UB. Use unsigned. + * 2. Print flags in binary by walking bits with `(flags >> i) & 1`. + * 3. Combine flags in scanf-style %x parsing. + */ diff --git a/lessons/07-advanced/README.md b/lessons/07-advanced/README.md new file mode 100644 index 0000000..6864a0b --- /dev/null +++ b/lessons/07-advanced/README.md @@ -0,0 +1,7 @@ +# 07 — Advanced + +Bonus topics. Not required to read source. But you'll see them. + +1. [01-typedef-enums.c](01-typedef-enums.c) — kill magic numbers +2. [02-unions.c](02-unions.c) — same memory, multiple views +3. [03-bit-manipulation.c](03-bit-manipulation.c) — flags, masks, shifts From 4dcd0dcb31a96d6b55965781014923381114de64 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:15 +0530 Subject: [PATCH 04/15] feat(mnemonics): add ASCII memory aids for pointers, malloc, strings, stack/heap --- mnemonics/MALLOC_RULES.txt | 30 +++++++++++++++++++++++++++++ mnemonics/POINTERS_101.txt | 34 +++++++++++++++++++++++++++++++++ mnemonics/STACK_VS_HEAP.txt | 37 ++++++++++++++++++++++++++++++++++++ mnemonics/STRING_DANGERS.txt | 27 ++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 mnemonics/MALLOC_RULES.txt create mode 100644 mnemonics/POINTERS_101.txt create mode 100644 mnemonics/STACK_VS_HEAP.txt create mode 100644 mnemonics/STRING_DANGERS.txt diff --git a/mnemonics/MALLOC_RULES.txt b/mnemonics/MALLOC_RULES.txt new file mode 100644 index 0000000..4d81b42 --- /dev/null +++ b/mnemonics/MALLOC_RULES.txt @@ -0,0 +1,30 @@ ++-----------------------------------------------------------+ +| MALLOC RULES: Don't Leak, Don't Crash | ++-----------------------------------------------------------+ +| | +| Every malloc must have exactly ONE free. | +| | +| int *p = malloc(n * sizeof(int)); | +| if (!p) { /* out of memory */ return -1; } | +| ... use p ... | +| free(p); | +| p = NULL; // defensive: prevents use-after-free| +| | ++-----------------------------------------------------------+ +| THE FOUR SINS | +| 1. Forget to check NULL -> crash on out-of-memory | +| 2. Forget to free -> memory leak | +| 3. Free twice -> heap corruption | +| 4. Use after free -> UB, hard-to-find bug | ++-----------------------------------------------------------+ +| CHEAT | +| malloc(n) -> n bytes, uninitialized | +| calloc(n, size) -> n*size bytes, zeroed | +| realloc(p, n) -> resize block (may move it!) | +| free(p) -> return block to heap | ++-----------------------------------------------------------+ +| OWNERSHIP RULE | +| Whoever malloc'd it owns it. Document who frees. | +| If a function returns a malloc'd pointer, say so. | +| /* caller must free() the result */ | ++-----------------------------------------------------------+ diff --git a/mnemonics/POINTERS_101.txt b/mnemonics/POINTERS_101.txt new file mode 100644 index 0000000..3e0a588 --- /dev/null +++ b/mnemonics/POINTERS_101.txt @@ -0,0 +1,34 @@ ++-----------------------------------------------------------+ +| POINTERS 101: Remember This One Thing | ++-----------------------------------------------------------+ +| | +| Memory is a street. Variables live in houses. | +| | +| int x = 42; house at address 0x1000 | +| inside: 42 | +| | +| int *p = &x; pointer p holds an address | +| 0x1000 (x's house address) | +| | +| *p "go to that house, read value" | +| returns 42 | +| | +| p++ "move to next house" | +| now points to 0x1004 (next int) | +| | ++-----------------------------------------------------------+ +| GOLDEN RULE | +| & = "address of" | +| * = "value at this address" | +| They are opposites. Know one, know both. | ++-----------------------------------------------------------+ +| THREE BUGS THAT BITE | +| 1. NULL deref: *NULL -> crash | +| 2. Wild pointer: int *p; *p = 5; -> crash | +| 3. Use-after-free: free(p); *p = 9; -> UB | +| | +| Checklist before deref: | +| - Is it NULL? | +| - Has it been initialized? | +| - Has it been freed? | ++-----------------------------------------------------------+ diff --git a/mnemonics/STACK_VS_HEAP.txt b/mnemonics/STACK_VS_HEAP.txt new file mode 100644 index 0000000..386b0ac --- /dev/null +++ b/mnemonics/STACK_VS_HEAP.txt @@ -0,0 +1,37 @@ ++-----------------------------------------------------------+ +| STACK vs HEAP: Two Worlds | ++-----------------------------------------------------------+ +| | +| high addr +-------------------+ | +| | STACK | grows DOWN | +| | | | auto, fast | +| | v | small (~1-8 MB) | +| | | | +| | | | +| | ^ | | +| | | | | +| | HEAP | grows UP | +| | | manual (malloc/free) | +| | | huge (GBs) | +| +-------------------+ | +| | data + code + BSS | | +| low addr +-------------------+ | +| | ++-----------------------------------------------------------+ +| STACK | +| - Local variables | +| - Function args | +| - Return addresses | +| - Auto-cleanup when function returns | +| - DON'T return a pointer to a local. It's gone. | ++-----------------------------------------------------------+ +| HEAP | +| - malloc / calloc / realloc | +| - Lives until you free() | +| - Survives function returns | +| - YOU manage the lifetime | ++-----------------------------------------------------------+ +| QUICK PICK | +| Need it after function returns? -> heap | +| Size known + small + short-lived? -> stack | ++-----------------------------------------------------------+ diff --git a/mnemonics/STRING_DANGERS.txt b/mnemonics/STRING_DANGERS.txt new file mode 100644 index 0000000..b0be8be --- /dev/null +++ b/mnemonics/STRING_DANGERS.txt @@ -0,0 +1,27 @@ ++-----------------------------------------------------------+ +| STRING DANGERS: C Strings Will Bite You | ++-----------------------------------------------------------+ +| | +| C string = char array ending in '\0'. No length field. | +| strlen walks until it finds '\0'. Overwrite the '\0', | +| strlen never stops. | +| | ++-----------------------------------------------------------+ +| BANNED IN MODERN CODE | +| strcpy -> no size check, buffer overflow | +| strcat -> no size check, buffer overflow | +| gets -> deleted from C standard. dangerous. | +| sprintf -> no size check on output | ++-----------------------------------------------------------+ +| USE THESE INSTEAD | +| snprintf(buf, sizeof buf, "%s", src); // bounded fmt | +| strncpy + manual buf[n-1] = '\0'; // bounded copy | +| strlcpy / strlcat (BSD, some libcs) | +| fgets(buf, sizeof buf, stdin); // bounded input | ++-----------------------------------------------------------+ +| TWO NUMBERS TO KEEP STRAIGHT | +| sizeof(buf) = capacity (including '\0' slot) | +| strlen(buf) = length so far (excluding '\0') | +| | +| They differ by AT LEAST 1. | ++-----------------------------------------------------------+ From a405f8969bcc7c55bc3a04d5fb8a1f445acfa175 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:15 +0530 Subject: [PATCH 05/15] feat(patterns): add bubble-sort, file-read-lines, malloc-cleanup snippets --- patterns/README.md | 12 +++++++++++ patterns/array-sort.c | 41 ++++++++++++++++++++++++++++++++++++++ patterns/file-read-lines.c | 30 ++++++++++++++++++++++++++++ patterns/malloc-cleanup.c | 38 +++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 patterns/README.md create mode 100644 patterns/array-sort.c create mode 100644 patterns/file-read-lines.c create mode 100644 patterns/malloc-cleanup.c diff --git a/patterns/README.md b/patterns/README.md new file mode 100644 index 0000000..3e22da8 --- /dev/null +++ b/patterns/README.md @@ -0,0 +1,12 @@ +# Patterns + +Real snippets people actually write. Copy. Adapt. Ship. + +| File | When to use | +|------|-------------| +| [string-search.c](string-search.c) | Find a substring (no `strstr`) — strings lesson follow-up | +| [array-sort.c](array-sort.c) | Sort small array — when `qsort` overkill | +| [file-read-lines.c](file-read-lines.c) | Count/process lines in a text file | +| [malloc-cleanup.c](malloc-cleanup.c) | Multiple allocs + clean error path (`goto cleanup`) | + +Production code: prefer `strstr`, `qsort`, and your platform's safer string fns. These show the mechanics. diff --git a/patterns/array-sort.c b/patterns/array-sort.c new file mode 100644 index 0000000..0d5082f --- /dev/null +++ b/patterns/array-sort.c @@ -0,0 +1,41 @@ +/* + * Pattern: Bubble sort an int array (manual, no qsort) + * Why this matters: Simplest sort. O(n^2). Use qsort in real code. Write this + * once to see what "swap and compare" looks like. + * + * Run: gcc array-sort.c && ./a.out + * + * EXPECTED OUTPUT: + * before: 5 2 9 1 7 + * after: 1 2 5 7 9 + */ + +#include + +static void print_arr(int *a, int n) { + for (int i = 0; i < n; i++) printf("%d ", a[i]); + printf("\n"); +} + +void bubble_sort(int *a, int n) { + for (int i = 0; i < n - 1; i++) { + for (int j = 0; j < n - 1 - i; j++) { + if (a[j] > a[j+1]) { + int t = a[j]; a[j] = a[j+1]; a[j+1] = t; + } + } + } +} + +int main(void) { + int a[] = {5, 2, 9, 1, 7}; + int n = sizeof(a) / sizeof(a[0]); + printf("before: "); + print_arr(a, n); + bubble_sort(a, n); + printf("after: "); + print_arr(a, n); + return 0; +} + +/* Real world: qsort(a, n, sizeof(int), cmp_int) from . */ diff --git a/patterns/file-read-lines.c b/patterns/file-read-lines.c new file mode 100644 index 0000000..1f8b12c --- /dev/null +++ b/patterns/file-read-lines.c @@ -0,0 +1,30 @@ +/* + * Pattern: Read a file line by line, count non-empty lines + * Why this matters: Every log parser, every CSV reader. Same shape. + * + * Run: + * echo -e "alpha\n\nbeta\ngamma" > data.txt + * gcc file-read-lines.c && ./a.out + * + * EXPECTED OUTPUT (when data.txt has 4 lines, 1 empty): + * non-empty lines: 3 + * + * If data.txt missing: + * could not open data.txt + */ + +#include + +int main(void) { + FILE *f = fopen("data.txt", "r"); + if (!f) { printf("could not open data.txt\n"); return 0; } + + char buf[256]; + int count = 0; + while (fgets(buf, sizeof(buf), f)) { + if (buf[0] != '\n' && buf[0] != '\0') count++; + } + fclose(f); + printf("non-empty lines: %d\n", count); + return 0; +} diff --git a/patterns/malloc-cleanup.c b/patterns/malloc-cleanup.c new file mode 100644 index 0000000..8e21d00 --- /dev/null +++ b/patterns/malloc-cleanup.c @@ -0,0 +1,38 @@ +/* + * Pattern: Allocate, use, clean up. The single-exit cleanup pattern. + * Why this matters: Real C code uses `goto cleanup` for error paths. Don't + * recoil. It's the cleanest way to free multiple allocations on failure. + * + * Run: gcc malloc-cleanup.c && ./a.out + * + * EXPECTED OUTPUT: + * sum = 45 + */ + +#include +#include + +int main(void) { + int rc = 0; + int *a = NULL; + int *b = NULL; + + a = malloc(10 * sizeof(int)); + if (!a) { rc = 1; goto cleanup; } + + b = malloc(10 * sizeof(int)); + if (!b) { rc = 1; goto cleanup; } + + for (int i = 0; i < 10; i++) { a[i] = i; b[i] = i; } + + int sum = 0; + for (int i = 0; i < 10; i++) sum += a[i]; + printf("sum = %d\n", sum); + +cleanup: + free(a); + free(b); + return rc; +} + +/* free(NULL) is a no-op. That's why init to NULL up top is safe. */ From 8aabc0275c973088bd7512d07343ca5626978b4b Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:22 +0530 Subject: [PATCH 06/15] feat(challenges): tier challenges into foundational/pointers/integration + hide solutions --- challenges/README.md | 24 +++++++++ challenges/solutions/README.md | 12 +++++ .../tier-1-foundational/02-predict-output.c | 37 ++++++++++++++ challenges/tier-2-pointers/02-segfault-hunt.c | 51 +++++++++++++++++++ .../tier-3-integration/01-build-and-sort.c | 45 ++++++++++++++++ 5 files changed, 169 insertions(+) create mode 100644 challenges/README.md create mode 100644 challenges/solutions/README.md create mode 100644 challenges/tier-1-foundational/02-predict-output.c create mode 100644 challenges/tier-2-pointers/02-segfault-hunt.c create mode 100644 challenges/tier-3-integration/01-build-and-sort.c diff --git a/challenges/README.md b/challenges/README.md new file mode 100644 index 0000000..abb044f --- /dev/null +++ b/challenges/README.md @@ -0,0 +1,24 @@ +# Challenges + +Tiered break-it labs. Pick the tier matching what you've done. + +## Tier 1 — Foundational +Loops, output, simple branching. Match the expected output. + +- [01-fix-the-loop.c](tier-1-foundational/01-fix-the-loop.c) +- [02-predict-output.c](tier-1-foundational/02-predict-output.c) + +## Tier 2 — Pointers +Pointer puzzles, segfault hunts. Read carefully. + +- [01-swap-puzzle.c](tier-2-pointers/01-swap-puzzle.c) +- [02-segfault-hunt.c](tier-2-pointers/02-segfault-hunt.c) + +## Tier 3 — Integration +Combine 3+ concepts. Mini real-world shape. + +- [01-build-and-sort.c](tier-3-integration/01-build-and-sort.c) + +## Solutions + +Hidden until you ask. See [solutions/README.md](solutions/README.md). diff --git a/challenges/solutions/README.md b/challenges/solutions/README.md new file mode 100644 index 0000000..296c691 --- /dev/null +++ b/challenges/solutions/README.md @@ -0,0 +1,12 @@ +# Solutions + +Solutions are hidden by `.gitignore` until you ask the maintainer. + +Why: solving the challenge is the lesson. Reading the solution first short-circuits learning. + +Want a solution? Open an issue with: +- Which challenge +- What you tried +- What output you saw vs. expected + +Maintainer drops the file via PR after a real attempt is shared. diff --git a/challenges/tier-1-foundational/02-predict-output.c b/challenges/tier-1-foundational/02-predict-output.c new file mode 100644 index 0000000..c0d4718 --- /dev/null +++ b/challenges/tier-1-foundational/02-predict-output.c @@ -0,0 +1,37 @@ +/* + * Challenge: Predict the Output | Time: 3 min + * Don't compile yet. Read. Predict each printf line. + * Then compile and check. Re-trace by hand where you were wrong. + * + * Run: gcc 02-predict-output.c && ./a.out + * + * EXPECTED OUTPUT (look only after you've guessed): + * a=3 b=2 + * x=6 y=5 + * z=7 + */ + +#include + +int main(void) { + int a = 1, b = 2; + a = a + b; + printf("a=%d b=%d\n", a, b); + + int x = 5, y; + y = x++; /* post-increment: y = 5, then x = 6 */ + printf("x=%d y=%d\n", x, y); + + int z = 5; + z += 3; + z -= 1; + printf("z=%d\n", z); + return 0; +} + +/* + * The lesson: + * post-increment returns OLD value, then bumps. + * pre-increment bumps first, returns NEW value. + * `y = x++` vs `y = ++x` differ by one. + */ diff --git a/challenges/tier-2-pointers/02-segfault-hunt.c b/challenges/tier-2-pointers/02-segfault-hunt.c new file mode 100644 index 0000000..cb492e4 --- /dev/null +++ b/challenges/tier-2-pointers/02-segfault-hunt.c @@ -0,0 +1,51 @@ +/* + * Challenge: Segfault Hunt | Time: 5 min + * Three pointer functions. One of them crashes. Read all three. + * Predict which crashes BEFORE compiling. Then verify. + * + * Run: gcc 02-segfault-hunt.c && ./a.out + * + * EXPECTED OUTPUT (after you fix the bad one): + * ok_a: 5 + * ok_b: 10 + * ok_c: 7 + */ + +#include +#include + +void ok_a(void) { + int x = 5; + int *p = &x; + printf("ok_a: %d\n", *p); +} + +void ok_b(void) { + int *p = malloc(sizeof(int)); + if (!p) return; + *p = 10; + printf("ok_b: %d\n", *p); + free(p); +} + +void ok_c(void) { + /* BUG: uncomment to see segfault: + * int *p; + * *p = 7; + * printf("ok_c: %d\n", *p); + */ + int x = 7; + int *p = &x; + printf("ok_c: %d\n", *p); +} + +int main(void) { + ok_a(); + ok_b(); + ok_c(); + return 0; +} + +/* + * Which one is the wild pointer? Hint: look for a `*p = ...` where p is never assigned. + */ diff --git a/challenges/tier-3-integration/01-build-and-sort.c b/challenges/tier-3-integration/01-build-and-sort.c new file mode 100644 index 0000000..c9854bf --- /dev/null +++ b/challenges/tier-3-integration/01-build-and-sort.c @@ -0,0 +1,45 @@ +/* + * Challenge: Build And Sort | Integration | Time: 15 min + * Read 5 ints from a hard-coded array, build a dynamically-allocated copy, + * sort it, print it, free it. + * + * Touches: arrays, malloc/free, function pointers (qsort), printf loop. + * + * Run: gcc 01-build-and-sort.c && ./a.out + * + * EXPECTED OUTPUT (when fixed): + * sorted: 1 3 5 7 9 + */ + +#include +#include + +static int cmp_int(const void *a, const void *b) { + int ia = *(const int*)a, ib = *(const int*)b; + return (ia > ib) - (ia < ib); +} + +int main(void) { + int src[] = {9, 1, 7, 3, 5}; + int n = sizeof(src) / sizeof(src[0]); + + int *copy = malloc(n * sizeof(int)); + if (!copy) return 1; + for (int i = 0; i < n; i++) copy[i] = src[i]; + + qsort(copy, n, sizeof(int), cmp_int); + + printf("sorted:"); + for (int i = 0; i < n; i++) printf(" %d", copy[i]); + printf("\n"); + + free(copy); + return 0; +} + +/* + * Extend: + * 1. Read the input from a file (`in.txt`, one int per line). + * 2. Sort descending (flip cmp). + * 3. Print median. + */ From 8a36e5179f2f8166dd713d5521226be4064c5fc0 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:22 +0530 Subject: [PATCH 07/15] feat(practice): add 15 tiered practice problems with skeletons --- practice/01-easy-warmup/README.md | 10 +++++ practice/01-easy-warmup/count-vowels.c | 29 +++++++++++++ practice/01-easy-warmup/find-max.c | 29 +++++++++++++ practice/01-easy-warmup/print-triangle.c | 30 ++++++++++++++ practice/01-easy-warmup/reverse-string.c | 39 ++++++++++++++++++ practice/02-medium/README.md | 10 +++++ practice/02-medium/binary-search.c | 30 ++++++++++++++ practice/02-medium/bubble-sort.c | 27 ++++++++++++ practice/02-medium/linked-list-insert.c | 44 ++++++++++++++++++++ practice/02-medium/read-csv.c | 36 ++++++++++++++++ practice/02-medium/reverse-list.c | 43 ++++++++++++++++++++ practice/02-medium/simple-hash.c | 52 ++++++++++++++++++++++++ practice/03-hard/README.md | 11 +++++ practice/03-hard/bst.c | 45 ++++++++++++++++++++ practice/03-hard/file-merge-sort.c | 51 +++++++++++++++++++++++ practice/03-hard/mini-calc.c | 36 ++++++++++++++++ practice/03-hard/mini-interpreter.c | 42 +++++++++++++++++++ practice/03-hard/queue-ring.c | 49 ++++++++++++++++++++++ 18 files changed, 613 insertions(+) create mode 100644 practice/01-easy-warmup/README.md create mode 100644 practice/01-easy-warmup/count-vowels.c create mode 100644 practice/01-easy-warmup/find-max.c create mode 100644 practice/01-easy-warmup/print-triangle.c create mode 100644 practice/01-easy-warmup/reverse-string.c create mode 100644 practice/02-medium/README.md create mode 100644 practice/02-medium/binary-search.c create mode 100644 practice/02-medium/bubble-sort.c create mode 100644 practice/02-medium/linked-list-insert.c create mode 100644 practice/02-medium/read-csv.c create mode 100644 practice/02-medium/reverse-list.c create mode 100644 practice/02-medium/simple-hash.c create mode 100644 practice/03-hard/README.md create mode 100644 practice/03-hard/bst.c create mode 100644 practice/03-hard/file-merge-sort.c create mode 100644 practice/03-hard/mini-calc.c create mode 100644 practice/03-hard/mini-interpreter.c create mode 100644 practice/03-hard/queue-ring.c diff --git a/practice/01-easy-warmup/README.md b/practice/01-easy-warmup/README.md new file mode 100644 index 0000000..349d1aa --- /dev/null +++ b/practice/01-easy-warmup/README.md @@ -0,0 +1,10 @@ +# Tier 1 — Easy Warmup + +Pick one. Fill in the TODO. Run. Match the expected output. + +- [reverse-string.c](reverse-string.c) +- [count-vowels.c](count-vowels.c) +- [find-max.c](find-max.c) +- [print-triangle.c](print-triangle.c) + +Each is < 30 lines after you fill it in. If you struggle, re-read the relevant lesson. diff --git a/practice/01-easy-warmup/count-vowels.c b/practice/01-easy-warmup/count-vowels.c new file mode 100644 index 0000000..1fa529c --- /dev/null +++ b/practice/01-easy-warmup/count-vowels.c @@ -0,0 +1,29 @@ +/* + * Practice: Count vowels in a string. | Difficulty: easy + * + * Spec: + * "hello world" -> 3 + * "rhythm" -> 0 + * + * Run: gcc count-vowels.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 3 + * 0 + * 4 + */ + +#include + +int count_vowels(const char *s) { + /* TODO: walk s, return count of a/e/i/o/u (case-insensitive). */ + (void)s; + return 0; +} + +int main(void) { + printf("%d\n", count_vowels("hello world")); + printf("%d\n", count_vowels("rhythm")); + printf("%d\n", count_vowels("AEIOUxyz")); + return 0; +} diff --git a/practice/01-easy-warmup/find-max.c b/practice/01-easy-warmup/find-max.c new file mode 100644 index 0000000..48c81a3 --- /dev/null +++ b/practice/01-easy-warmup/find-max.c @@ -0,0 +1,29 @@ +/* + * Practice: Find max in int array. | Difficulty: easy + * + * Spec: + * {3,1,4,1,5,9,2,6} -> 9 + * + * Run: gcc find-max.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 9 + * -1 + */ + +#include + +int find_max(const int *a, int n) { + /* TODO: assume n >= 1. Return the largest. */ + (void)a; (void)n; + return 0; +} + +int main(void) { + int a[] = {3, 1, 4, 1, 5, 9, 2, 6}; + printf("%d\n", find_max(a, sizeof(a)/sizeof(a[0]))); + + int b[] = {-5, -3, -1, -9}; + printf("%d\n", find_max(b, sizeof(b)/sizeof(b[0]))); + return 0; +} diff --git a/practice/01-easy-warmup/print-triangle.c b/practice/01-easy-warmup/print-triangle.c new file mode 100644 index 0000000..6527ec4 --- /dev/null +++ b/practice/01-easy-warmup/print-triangle.c @@ -0,0 +1,30 @@ +/* + * Practice: Print a left-aligned triangle of stars. | Difficulty: easy + * + * Spec: + * n = 4 -> + * * + * ** + * *** + * **** + * + * Run: gcc print-triangle.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved, n=4): + * * + * ** + * *** + * **** + */ + +#include + +void triangle(int n) { + /* TODO: nested loops. Outer = rows, inner = stars per row. */ + (void)n; +} + +int main(void) { + triangle(4); + return 0; +} diff --git a/practice/01-easy-warmup/reverse-string.c b/practice/01-easy-warmup/reverse-string.c new file mode 100644 index 0000000..b601651 --- /dev/null +++ b/practice/01-easy-warmup/reverse-string.c @@ -0,0 +1,39 @@ +/* + * Practice: Reverse a string in place. | Difficulty: easy + * + * Spec: + * Input: "hello" + * Output: "olleh" + * + * Modify the chars in place. Don't malloc a new string. + * + * Run: gcc reverse-string.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * olleh + * a + * (empty) + */ + +#include +#include + +void reverse(char *s) { + /* TODO: walk from both ends, swap until middle. */ + (void)s; +} + +int main(void) { + char a[] = "hello"; + reverse(a); + printf("%s\n", a); + + char b[] = "a"; + reverse(b); + printf("%s\n", b); + + char c[] = ""; + reverse(c); + printf("%s\n", c[0] ? c : "(empty)"); + return 0; +} diff --git a/practice/02-medium/README.md b/practice/02-medium/README.md new file mode 100644 index 0000000..8a32ee4 --- /dev/null +++ b/practice/02-medium/README.md @@ -0,0 +1,10 @@ +# Tier 2 — Medium + +Data structures + file I/O. Each takes 30-60 min. + +- [bubble-sort.c](bubble-sort.c) +- [linked-list-insert.c](linked-list-insert.c) +- [reverse-list.c](reverse-list.c) +- [binary-search.c](binary-search.c) +- [read-csv.c](read-csv.c) +- [simple-hash.c](simple-hash.c) diff --git a/practice/02-medium/binary-search.c b/practice/02-medium/binary-search.c new file mode 100644 index 0000000..af1c9f4 --- /dev/null +++ b/practice/02-medium/binary-search.c @@ -0,0 +1,30 @@ +/* + * Practice: Binary search in sorted int array. | Difficulty: medium + * + * Spec: + * Return index of target, or -1 if not found. + * + * Run: gcc binary-search.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 4 + * -1 + * 0 + */ + +#include + +int bsearch_int(const int *a, int n, int target) { + /* TODO: classic lo/hi. while (lo <= hi). Return mid on match. */ + (void)a; (void)n; (void)target; + return -1; +} + +int main(void) { + int a[] = {1, 3, 5, 7, 9, 11, 13}; + int n = sizeof(a) / sizeof(a[0]); + printf("%d\n", bsearch_int(a, n, 9)); + printf("%d\n", bsearch_int(a, n, 4)); + printf("%d\n", bsearch_int(a, n, 1)); + return 0; +} diff --git a/practice/02-medium/bubble-sort.c b/practice/02-medium/bubble-sort.c new file mode 100644 index 0000000..4f291cd --- /dev/null +++ b/practice/02-medium/bubble-sort.c @@ -0,0 +1,27 @@ +/* + * Practice: Implement bubble sort. | Difficulty: medium + * + * Spec: + * sort an int array ascending, in place. + * + * Run: gcc bubble-sort.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 1 1 2 3 4 5 5 6 9 + */ + +#include + +void bubble_sort(int *a, int n) { + /* TODO: nested loop. Swap adjacent out-of-order pairs. */ + (void)a; (void)n; +} + +int main(void) { + int a[] = {3, 1, 4, 1, 5, 9, 2, 6, 5}; + int n = sizeof(a) / sizeof(a[0]); + bubble_sort(a, n); + for (int i = 0; i < n; i++) printf("%d ", a[i]); + printf("\n"); + return 0; +} diff --git a/practice/02-medium/linked-list-insert.c b/practice/02-medium/linked-list-insert.c new file mode 100644 index 0000000..e631f39 --- /dev/null +++ b/practice/02-medium/linked-list-insert.c @@ -0,0 +1,44 @@ +/* + * Practice: Singly linked list, insert at head + print. | Difficulty: medium + * + * Spec: + * Insert 1, 2, 3 at head. Print: 3 -> 2 -> 1 -> NULL + * + * Run: gcc linked-list-insert.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 3 -> 2 -> 1 -> NULL + */ + +#include +#include + +typedef struct Node { + int val; + struct Node *next; +} Node; + +Node* push(Node *head, int v) { + /* TODO: malloc a Node, set val=v, next=head. Return new head. */ + (void)v; + return head; +} + +void print_list(Node *head) { + /* TODO: walk next pointers, print "v -> ", end with "NULL\n". */ + (void)head; +} + +void free_list(Node *head) { + while (head) { Node *n = head->next; free(head); head = n; } +} + +int main(void) { + Node *h = NULL; + h = push(h, 1); + h = push(h, 2); + h = push(h, 3); + print_list(h); + free_list(h); + return 0; +} diff --git a/practice/02-medium/read-csv.c b/practice/02-medium/read-csv.c new file mode 100644 index 0000000..02809bc --- /dev/null +++ b/practice/02-medium/read-csv.c @@ -0,0 +1,36 @@ +/* + * Practice: Read a CSV file, sum the second column. | Difficulty: medium + * + * Setup: + * Create `nums.csv`: + * alice,10 + * bob,20 + * carol,30 + * + * Spec: + * Sum column 2. Print 60. + * + * Run: gcc read-csv.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved + file exists): + * sum = 60 + */ + +#include +#include +#include + +int main(void) { + FILE *f = fopen("nums.csv", "r"); + if (!f) { printf("nums.csv missing\n"); return 0; } + + char buf[256]; + int sum = 0; + while (fgets(buf, sizeof(buf), f)) { + /* TODO: find the ',' with strchr. atoi the rest. Add to sum. */ + (void)buf; + } + fclose(f); + printf("sum = %d\n", sum); + return 0; +} diff --git a/practice/02-medium/reverse-list.c b/practice/02-medium/reverse-list.c new file mode 100644 index 0000000..72872fd --- /dev/null +++ b/practice/02-medium/reverse-list.c @@ -0,0 +1,43 @@ +/* + * Practice: Reverse a singly linked list in place. | Difficulty: medium + * + * Spec: + * 1 -> 2 -> 3 becomes 3 -> 2 -> 1 + * + * Run: gcc reverse-list.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * before: 1 -> 2 -> 3 -> NULL + * after: 3 -> 2 -> 1 -> NULL + */ + +#include +#include + +typedef struct Node { int v; struct Node *next; } Node; + +static Node* push(Node *h, int v) { + Node *n = malloc(sizeof(*n)); + n->v = v; n->next = h; return n; +} + +static void print_list(const char *label, Node *h) { + printf("%s: ", label); + while (h) { printf("%d -> ", h->v); h = h->next; } + printf("NULL\n"); +} + +Node* reverse(Node *head) { + /* TODO: three pointers (prev, cur, next). Walk + flip. */ + return head; +} + +int main(void) { + Node *h = NULL; + h = push(h, 3); h = push(h, 2); h = push(h, 1); + print_list("before", h); + h = reverse(h); + print_list("after ", h); + while (h) { Node *n = h->next; free(h); h = n; } + return 0; +} diff --git a/practice/02-medium/simple-hash.c b/practice/02-medium/simple-hash.c new file mode 100644 index 0000000..3424b37 --- /dev/null +++ b/practice/02-medium/simple-hash.c @@ -0,0 +1,52 @@ +/* + * Practice: Toy hash table (fixed-size, string -> int, open addressing). | medium + * + * Spec: + * put("apple", 1); put("banana", 2); get("apple") -> 1 + * + * Run: gcc simple-hash.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * apple=1 + * banana=2 + * missing=-1 + */ + +#include +#include + +#define SLOTS 16 + +typedef struct { + char key[32]; + int val; + int used; +} Slot; + +static Slot table[SLOTS]; + +static unsigned hash(const char *s) { + unsigned h = 5381; + while (*s) h = h * 33 + (unsigned char)*s++; + return h; +} + +void put(const char *k, int v) { + /* TODO: linear probe from hash(k) % SLOTS. Find empty or matching key. */ + (void)k; (void)v; +} + +int get(const char *k) { + /* TODO: linear probe. Return val if found, -1 if not. */ + (void)k; + return -1; +} + +int main(void) { + put("apple", 1); + put("banana", 2); + printf("apple=%d\n", get("apple")); + printf("banana=%d\n", get("banana")); + printf("missing=%d\n", get("missing")); + return 0; +} diff --git a/practice/03-hard/README.md b/practice/03-hard/README.md new file mode 100644 index 0000000..cc4b1bc --- /dev/null +++ b/practice/03-hard/README.md @@ -0,0 +1,11 @@ +# Tier 3 — Ceiling + +Intermediate-level problems. Multi-hour. Combines lessons from across the repo. + +- [bst.c](bst.c) — binary search tree +- [mini-calc.c](mini-calc.c) — parse + evaluate "A op B" +- [mini-interpreter.c](mini-interpreter.c) — tiny command executor +- [file-merge-sort.c](file-merge-sort.c) — sort numbers from file +- [queue-ring.c](queue-ring.c) — fixed-capacity FIFO + +Got these? See [../../projects/capstone/](../../projects/capstone/). diff --git a/practice/03-hard/bst.c b/practice/03-hard/bst.c new file mode 100644 index 0000000..6aa5983 --- /dev/null +++ b/practice/03-hard/bst.c @@ -0,0 +1,45 @@ +/* + * Practice: Binary Search Tree (insert + inorder). | Difficulty: hard + * + * Spec: + * Insert 5, 3, 8, 1, 4. Inorder print: 1 3 4 5 8 + * + * Run: gcc bst.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 1 3 4 5 8 + */ + +#include +#include + +typedef struct Node { + int v; + struct Node *l, *r; +} Node; + +Node* insert(Node *root, int v) { + /* TODO: if root NULL, malloc + return. Else recurse left or right. */ + (void)v; + return root; +} + +void inorder(Node *root) { + /* TODO: left, print, right. Recursion. */ + (void)root; +} + +void free_tree(Node *root) { + if (!root) return; + free_tree(root->l); free_tree(root->r); free(root); +} + +int main(void) { + Node *t = NULL; + int vals[] = {5, 3, 8, 1, 4}; + for (int i = 0; i < 5; i++) t = insert(t, vals[i]); + inorder(t); + printf("\n"); + free_tree(t); + return 0; +} diff --git a/practice/03-hard/file-merge-sort.c b/practice/03-hard/file-merge-sort.c new file mode 100644 index 0000000..1397aa9 --- /dev/null +++ b/practice/03-hard/file-merge-sort.c @@ -0,0 +1,51 @@ +/* + * Practice: Read numbers from file, sort, write to output. | Difficulty: hard + * + * Setup: + * in.txt: + * 5 + * 1 + * 9 + * 3 + * + * Spec: + * Read in.txt, sort ascending, write to out.txt: + * 1 + * 3 + * 5 + * 9 + * + * Run: gcc file-merge-sort.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved + file exists): + * sorted 4 numbers + */ + +#include +#include + +static int cmp_int(const void *a, const void *b) { + int ia = *(const int*)a, ib = *(const int*)b; + return (ia > ib) - (ia < ib); +} + +int main(void) { + FILE *fin = fopen("in.txt", "r"); + if (!fin) { printf("in.txt missing\n"); return 0; } + + int buf[1024]; + int n = 0; + while (n < 1024 && fscanf(fin, "%d", &buf[n]) == 1) n++; + fclose(fin); + + /* TODO: qsort(buf, n, sizeof(int), cmp_int); */ + (void)cmp_int; + + FILE *fout = fopen("out.txt", "w"); + if (!fout) { printf("out.txt write failed\n"); return 1; } + for (int i = 0; i < n; i++) fprintf(fout, "%d\n", buf[i]); + fclose(fout); + + printf("sorted %d numbers\n", n); + return 0; +} diff --git a/practice/03-hard/mini-calc.c b/practice/03-hard/mini-calc.c new file mode 100644 index 0000000..20f06ca --- /dev/null +++ b/practice/03-hard/mini-calc.c @@ -0,0 +1,36 @@ +/* + * Practice: Mini calculator — parse and evaluate "A op B". | Difficulty: hard + * + * Spec: + * Input string "3 + 4" -> prints 7 + * Supports + - * /. Single op. Integers. + * + * Run: gcc mini-calc.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 7 + * 12 + * 2 + */ + +#include +#include +#include + +int eval(const char *s) { + /* TODO: + * 1. skip spaces, parse int A + * 2. skip spaces, read op char + * 3. skip spaces, parse int B + * 4. apply op, return result. + */ + (void)s; + return 0; +} + +int main(void) { + printf("%d\n", eval("3 + 4")); + printf("%d\n", eval("4 * 3")); + printf("%d\n", eval("10 / 5")); + return 0; +} diff --git a/practice/03-hard/mini-interpreter.c b/practice/03-hard/mini-interpreter.c new file mode 100644 index 0000000..1278f8e --- /dev/null +++ b/practice/03-hard/mini-interpreter.c @@ -0,0 +1,42 @@ +/* + * Practice: Tiny command interpreter. | Difficulty: hard + * + * Spec: + * set x 10 + * set y 20 + * add x y -> prints 30 + * print x -> prints 10 + * + * Run: gcc mini-interpreter.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 30 + * 10 + */ + +#include +#include + +#define VARS 26 + +static int env[VARS]; + +static int idx(char name) { return name - 'a'; } + +void exec_line(const char *line) { + /* TODO: tokenize. Dispatch on first word: + * set + * add (print sum) + * print + */ + (void)line; +} + +int main(void) { + exec_line("set x 10"); + exec_line("set y 20"); + exec_line("add x y"); + exec_line("print x"); + (void)idx; (void)env; + return 0; +} diff --git a/practice/03-hard/queue-ring.c b/practice/03-hard/queue-ring.c new file mode 100644 index 0000000..5c18c7d --- /dev/null +++ b/practice/03-hard/queue-ring.c @@ -0,0 +1,49 @@ +/* + * Practice: Ring buffer queue (fixed capacity). | Difficulty: hard + * + * Spec: + * enqueue 1,2,3. dequeue -> 1. dequeue -> 2. enqueue 4. dequeue -> 3. + * + * Run: gcc queue-ring.c && ./a.out + * + * EXPECTED TEST OUTPUT (when solved): + * 1 + * 2 + * 3 + * 4 + */ + +#include + +#define CAP 8 + +typedef struct { + int data[CAP]; + int head, tail, size; +} Ring; + +void rinit(Ring *r) { r->head = r->tail = r->size = 0; } + +int enq(Ring *r, int v) { + /* TODO: if full (size == CAP), return -1. Else write + advance tail mod CAP. */ + (void)r; (void)v; + return 0; +} + +int deq(Ring *r, int *out) { + /* TODO: if empty, return -1. Else read + advance head mod CAP. */ + (void)r; (void)out; + return 0; +} + +int main(void) { + Ring r; rinit(&r); + enq(&r, 1); enq(&r, 2); enq(&r, 3); + int v; + deq(&r, &v); printf("%d\n", v); + deq(&r, &v); printf("%d\n", v); + enq(&r, 4); + deq(&r, &v); printf("%d\n", v); + deq(&r, &v); printf("%d\n", v); + return 0; +} From 8f2a855bacb5ef5b02e902fe74f4f35e01debe00 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:30 +0530 Subject: [PATCH 08/15] feat(projects): add capstone expression evaluator + tests + READMEs for guess/phonebook --- projects/capstone/README.md | 55 ++++++++++++ projects/capstone/capstone.c | 129 ++++++++++++++++++++++++++++ projects/capstone/capstone_test.c | 109 +++++++++++++++++++++++ projects/guess/README.md | 25 ++++++ projects/guess/guess_test.c | 35 ++++++++ projects/phonebook/README.md | 26 ++++++ projects/phonebook/phonebook_test.c | 44 ++++++++++ 7 files changed, 423 insertions(+) create mode 100644 projects/capstone/README.md create mode 100644 projects/capstone/capstone.c create mode 100644 projects/capstone/capstone_test.c create mode 100644 projects/guess/README.md create mode 100644 projects/guess/guess_test.c create mode 100644 projects/phonebook/README.md create mode 100644 projects/phonebook/phonebook_test.c diff --git a/projects/capstone/README.md b/projects/capstone/README.md new file mode 100644 index 0000000..708f823 --- /dev/null +++ b/projects/capstone/README.md @@ -0,0 +1,55 @@ +# Capstone — Expression Evaluator + +Final project. Parse + evaluate integer expressions with +, -, *, /, parentheses, unary minus. + +``` +3 + 5 * (2 - 8) / 2 -> -12 +``` + +## Why + +Combines everything: + +- **strings** — walk the input +- **pointers** — cursor over chars, parser state +- **recursion** — grammar is recursive (expr -> term -> factor -> expr) +- **structs** — parser state +- **file I/O** — `--file` mode reads expressions line by line +- **error handling** — bad input returns non-zero, no crash + +Recursive-descent parsers exist in every interpreter, compiler, and config loader. Once you've written one, you've seen the shape. + +## Run + +```bash +gcc capstone.c -o capstone +./capstone "3 + 5 * 2" +./capstone --file exprs.txt +``` + +## Test + +```bash +gcc capstone_test.c -o capstone_test +./capstone_test +``` + +Expected: `capstone_test: 10 passed, 0 failed.` + +## Grammar + +``` +expr = term (('+' | '-') term)* +term = factor (('*' | '/') factor)* +factor = number | '(' expr ')' | '-' factor +``` + +Left-associative. Standard precedence: `*` and `/` bind tighter than `+` and `-`. + +## Extend it + +- Add `^` (power). Right-associative — careful. +- Add variables: `x = 5; x + 3`. +- Switch to floats. +- Add functions: `sqrt(2)`, `max(a, b)`. +- Compile to a stack-based VM instead of direct eval. diff --git a/projects/capstone/capstone.c b/projects/capstone/capstone.c new file mode 100644 index 0000000..304a0e3 --- /dev/null +++ b/projects/capstone/capstone.c @@ -0,0 +1,129 @@ +/* + * Capstone: Expression Evaluator + * Parses + evaluates integer expressions: 3 + 5 * (2 - 8) / 2 + * + * Combines: strings (parse), recursion (grammar), pointers (cursor), + * file I/O (read expressions from a file), error handling. + * + * Grammar (recursive descent, left-associative): + * expr = term (('+'|'-') term)* + * term = factor (('*'|'/') factor)* + * factor = number | '(' expr ')' | '-' factor + * + * Run: + * gcc capstone.c -o capstone + * ./capstone "3 + 5 * 2" + * ./capstone --file exprs.txt + * + * EXAMPLE: + * $ ./capstone "3 + 5 * (2 - 8) / 2" + * -12 + */ + +#include +#include +#include +#include + +typedef struct { + const char *src; + int pos; + int err; +} Parser; + +static void skip_ws(Parser *p) { + while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t') p->pos++; +} + +static int parse_expr(Parser *p); + +static int parse_factor(Parser *p) { + skip_ws(p); + char c = p->src[p->pos]; + if (c == '(') { + p->pos++; + int v = parse_expr(p); + skip_ws(p); + if (p->src[p->pos] != ')') { p->err = 1; return 0; } + p->pos++; + return v; + } + if (c == '-') { p->pos++; return -parse_factor(p); } + if (!isdigit((unsigned char)c)) { p->err = 1; return 0; } + int v = 0; + while (isdigit((unsigned char)p->src[p->pos])) { + v = v * 10 + (p->src[p->pos] - '0'); + p->pos++; + } + return v; +} + +static int parse_term(Parser *p) { + int v = parse_factor(p); + for (;;) { + skip_ws(p); + char c = p->src[p->pos]; + if (c != '*' && c != '/') return v; + p->pos++; + int r = parse_factor(p); + if (c == '*') v *= r; + else { + if (r == 0) { p->err = 1; return 0; } + v /= r; + } + } +} + +static int parse_expr(Parser *p) { + int v = parse_term(p); + for (;;) { + skip_ws(p); + char c = p->src[p->pos]; + if (c != '+' && c != '-') return v; + p->pos++; + int r = parse_term(p); + if (c == '+') v += r; + else v -= r; + } +} + +int eval(const char *expr, int *out) { + Parser p = { expr, 0, 0 }; + int v = parse_expr(&p); + skip_ws(&p); + if (p.err || p.src[p.pos] != '\0') return -1; + *out = v; + return 0; +} + +static int eval_file(const char *path) { + FILE *f = fopen(path, "r"); + if (!f) { fprintf(stderr, "could not open %s\n", path); return 1; } + char buf[256]; + while (fgets(buf, sizeof(buf), f)) { + size_t n = strlen(buf); + while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r')) buf[--n] = '\0'; + if (n == 0) continue; + int result; + if (eval(buf, &result) == 0) printf("%s = %d\n", buf, result); + else printf("%s = ERROR\n", buf); + } + fclose(f); + return 0; +} + +int main(int argc, char **argv) { + if (argc == 3 && strcmp(argv[1], "--file") == 0) return eval_file(argv[2]); + if (argc != 2) { + fprintf(stderr, "usage: %s \"\" or %s --file \n", + argv[0], argv[0]); + return 2; + } + int result; + if (eval(argv[1], &result) != 0) { + fprintf(stderr, "parse error\n"); + return 1; + } + printf("%d\n", result); + return 0; +} diff --git a/projects/capstone/capstone_test.c b/projects/capstone/capstone_test.c new file mode 100644 index 0000000..6a7bc28 --- /dev/null +++ b/projects/capstone/capstone_test.c @@ -0,0 +1,109 @@ +/* + * Capstone tests. Compile + run, no external framework. + * + * Build: + * gcc capstone_test.c -o capstone_test + * ./capstone_test + * + * EXPECTED OUTPUT: + * capstone_test: 10 passed, 0 failed. + * + * This file vendors the parser via a copy of the public functions + * so we can test without linking. In a real project we'd extract a header. + */ + +#include +#include +#include +#include + +typedef struct { const char *src; int pos; int err; } Parser; + +static void skip_ws(Parser *p) { + while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t') p->pos++; +} +static int parse_expr(Parser *p); +static int parse_factor(Parser *p) { + skip_ws(p); + char c = p->src[p->pos]; + if (c == '(') { p->pos++; int v = parse_expr(p); skip_ws(p); + if (p->src[p->pos] != ')') { p->err = 1; return 0; } p->pos++; return v; } + if (c == '-') { p->pos++; return -parse_factor(p); } + if (!isdigit((unsigned char)c)) { p->err = 1; return 0; } + int v = 0; + while (isdigit((unsigned char)p->src[p->pos])) { v = v*10 + (p->src[p->pos]-'0'); p->pos++; } + return v; +} +static int parse_term(Parser *p) { + int v = parse_factor(p); + for (;;) { + skip_ws(p); + char c = p->src[p->pos]; + if (c != '*' && c != '/') return v; + p->pos++; + int r = parse_factor(p); + if (c == '*') v *= r; + else { if (r == 0) { p->err = 1; return 0; } v /= r; } + } +} +static int parse_expr(Parser *p) { + int v = parse_term(p); + for (;;) { + skip_ws(p); + char c = p->src[p->pos]; + if (c != '+' && c != '-') return v; + p->pos++; + int r = parse_term(p); + if (c == '+') v += r; else v -= r; + } +} +static int eval(const char *expr, int *out) { + Parser p = { expr, 0, 0 }; + int v = parse_expr(&p); + skip_ws(&p); + if (p.err || p.src[p.pos] != '\0') return -1; + *out = v; + return 0; +} + +static int passed = 0, failed = 0; + +static void check(const char *expr, int want) { + int got; + if (eval(expr, &got) != 0) { + printf(" FAIL: %s -> parse error (want %d)\n", expr, want); + failed++; + return; + } + if (got != want) { + printf(" FAIL: %s -> %d (want %d)\n", expr, got, want); + failed++; + } else { + passed++; + } +} +static void check_err(const char *expr) { + int got; + if (eval(expr, &got) == 0) { + printf(" FAIL: %s -> %d (wanted parse error)\n", expr, got); + failed++; + } else { + passed++; + } +} + +int main(void) { + check("1+2", 3); + check("3 + 5 * 2", 13); + check("(3 + 5) * 2", 16); + check("3 + 5 * (2 - 8) / 2", -12); + check("-7 + 3", -4); + check("100 / 10 / 2", 5); + check("2*3+4*5", 26); + check(" 42 ", 42); + check_err("1 +"); + check_err("(1 + 2"); + + printf("capstone_test: %d passed, %d failed.\n", passed, failed); + return failed == 0 ? 0 : 1; +} diff --git a/projects/guess/README.md b/projects/guess/README.md new file mode 100644 index 0000000..dc714d3 --- /dev/null +++ b/projects/guess/README.md @@ -0,0 +1,25 @@ +# Guess The Number + +Beginner project. Random secret 1-100. You guess. Higher/lower hints. + +```bash +gcc guess.c -o guess +./guess +``` + +Covers: `rand` + `srand`, `scanf`, while loop, simple branching. + +## Test + +```bash +gcc guess_test.c -o guess_test +./guess_test +``` + +`guess_test.c` smoke-tests the comparison logic without needing stdin or randomness. + +## Modify ideas + +- Make range 1-1000. +- Limit to 7 tries, give up with `the number was %d`. +- Track best score across runs in a file (see phonebook for file I/O). diff --git a/projects/guess/guess_test.c b/projects/guess/guess_test.c new file mode 100644 index 0000000..90a16fd --- /dev/null +++ b/projects/guess/guess_test.c @@ -0,0 +1,35 @@ +/* + * guess: smoke test for the comparison logic. + * Doesn't drive stdin or randomness; tests the rule directly. + * + * Build: gcc guess_test.c -o guess_test && ./guess_test + * + * EXPECTED OUTPUT: + * guess_test: 4 passed, 0 failed. + */ + +#include + +static const char* hint(int guess, int secret) { + if (guess < secret) return "low"; + if (guess > secret) return "high"; + return "match"; +} + +int main(void) { + int passed = 0, failed = 0; + struct { int g, s; const char *want; } cases[] = { + {25, 50, "low"}, + {75, 50, "high"}, + {50, 50, "match"}, + {1, 100, "low"}, + }; + for (int i = 0; i < 4; i++) { + const char *got = hint(cases[i].g, cases[i].s); + if (got[0] == cases[i].want[0]) passed++; + else { failed++; printf(" FAIL: g=%d s=%d got=%s want=%s\n", + cases[i].g, cases[i].s, got, cases[i].want); } + } + printf("guess_test: %d passed, %d failed.\n", passed, failed); + return failed == 0 ? 0 : 1; +} diff --git a/projects/phonebook/README.md b/projects/phonebook/README.md new file mode 100644 index 0000000..559695e --- /dev/null +++ b/projects/phonebook/README.md @@ -0,0 +1,26 @@ +# Phonebook + +Intermediate project. Add contacts, list, search. Persists to `book.txt` between runs. + +```bash +gcc phonebook.c -o phonebook +./phonebook +``` + +Covers: structs, fixed-size arrays, file I/O (load + save), case-insensitive substring search, menu loop. + +## Test + +```bash +gcc phonebook_test.c -o phonebook_test +./phonebook_test +``` + +`phonebook_test.c` tests the search helpers (case-insensitive prefix + contains) directly. + +## Modify ideas + +- Add a `delete` option (shift array down, save). +- Sort alphabetically before printing. +- Support names with spaces (use `fgets` instead of `scanf %s`). +- Swap linear search for a hash table — see `practice/02-medium/simple-hash.c`. diff --git a/projects/phonebook/phonebook_test.c b/projects/phonebook/phonebook_test.c new file mode 100644 index 0000000..d36783b --- /dev/null +++ b/projects/phonebook/phonebook_test.c @@ -0,0 +1,44 @@ +/* + * phonebook: test the search helpers (no file/stdin). + * + * Build: gcc phonebook_test.c -o phonebook_test && ./phonebook_test + * + * EXPECTED OUTPUT: + * phonebook_test: 6 passed, 0 failed. + */ + +#include + +static int starts_with_ci(const char *s, const char *prefix) { + for (int i = 0; prefix[i]; i++) { + char a = s[i], b = prefix[i]; + if (a >= 'A' && a <= 'Z') a += 32; + if (b >= 'A' && b <= 'Z') b += 32; + if (a != b) return 0; + } + return 1; +} + +static int contains_ci(const char *s, const char *needle) { + for (int i = 0; s[i]; i++) if (starts_with_ci(s + i, needle)) return 1; + return 0; +} + +static int passed, failed; +static void expect(int got, int want, const char *label) { + if (got == want) passed++; + else { failed++; printf(" FAIL: %s got=%d want=%d\n", label, got, want); } +} + +int main(void) { + expect(starts_with_ci("Alice", "ali"), 1, "Alice starts with ali"); + expect(starts_with_ci("Alice", "BOB"), 0, "Alice not BOB"); + expect(starts_with_ci("Alice", "ALICE"), 1, "Alice case-insensitive full"); + + expect(contains_ci("Alice", "lic"), 1, "Alice contains lic"); + expect(contains_ci("Alice", "xyz"), 0, "Alice no xyz"); + expect(contains_ci("Bob Smith", "smi"), 1, "Bob Smith contains smi"); + + printf("phonebook_test: %d passed, %d failed.\n", passed, failed); + return failed == 0 ? 0 : 1; +} From fa3d193a0a5cbadd854799c30d58f0968a16755c Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:31 +0530 Subject: [PATCH 09/15] feat(infra): Makefile + compile_check + test_lessons + golden outputs --- Makefile | 23 +++++++ scripts/compile_check.sh | 27 ++++++++ .../00-basics_01-types-and-output.txt | 4 ++ .../00-basics_02-operators.txt | 9 +++ .../00-basics_03-control-flow.txt | 4 ++ .../01-functions_01-simple-functions.txt | 3 + .../01-functions_02-scope-and-stack.txt | 6 ++ .../02-pointers_02-pointer-arithmetic.txt | 4 ++ .../02-pointers_03-arrays-as-pointers.txt | 4 ++ .../02-pointers_04-function-pointers.txt | 3 + .../02-pointers_06-common-pointer-bugs.txt | 2 + .../03-memory_02-malloc-free.txt | 2 + .../04-strings_01-char-arrays.txt | 3 + .../04-strings_02-string-functions.txt | 5 ++ .../04-strings_03-dangerous-strcpy.txt | 2 + .../05-structs_01-basic-structs.txt | 3 + .../05-structs_02-nested-structs.txt | 4 ++ .../07-advanced_01-typedef-enums.txt | 2 + .../07-advanced_02-unions.txt | 2 + .../07-advanced_03-bit-manipulation.txt | 6 ++ tests/expected_outputs/README.md | 15 +++++ tests/test_lessons.sh | 62 +++++++++++++++++++ 22 files changed, 195 insertions(+) create mode 100644 Makefile create mode 100644 scripts/compile_check.sh create mode 100644 tests/expected_outputs/00-basics_01-types-and-output.txt create mode 100644 tests/expected_outputs/00-basics_02-operators.txt create mode 100644 tests/expected_outputs/00-basics_03-control-flow.txt create mode 100644 tests/expected_outputs/01-functions_01-simple-functions.txt create mode 100644 tests/expected_outputs/01-functions_02-scope-and-stack.txt create mode 100644 tests/expected_outputs/02-pointers_02-pointer-arithmetic.txt create mode 100644 tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt create mode 100644 tests/expected_outputs/02-pointers_04-function-pointers.txt create mode 100644 tests/expected_outputs/02-pointers_06-common-pointer-bugs.txt create mode 100644 tests/expected_outputs/03-memory_02-malloc-free.txt create mode 100644 tests/expected_outputs/04-strings_01-char-arrays.txt create mode 100644 tests/expected_outputs/04-strings_02-string-functions.txt create mode 100644 tests/expected_outputs/04-strings_03-dangerous-strcpy.txt create mode 100644 tests/expected_outputs/05-structs_01-basic-structs.txt create mode 100644 tests/expected_outputs/05-structs_02-nested-structs.txt create mode 100644 tests/expected_outputs/07-advanced_01-typedef-enums.txt create mode 100644 tests/expected_outputs/07-advanced_02-unions.txt create mode 100644 tests/expected_outputs/07-advanced_03-bit-manipulation.txt create mode 100644 tests/expected_outputs/README.md create mode 100644 tests/test_lessons.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bf3db96 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +CC ?= gcc +CFLAGS ?= -Wall -Wextra -std=c11 -O0 -g + +.PHONY: all test compile-check clean help + +all: test + +help: + @echo "Targets:" + @echo " make compile-check Compile every .c file (no run). Fails on warning." + @echo " make test Compile + run + diff against expected_outputs." + @echo " make clean Remove built binaries." + +compile-check: + @bash scripts/compile_check.sh + +test: + @bash tests/test_lessons.sh + +clean: + @find . -type f \( -name "*.o" -o -name "a.out" -o -name "*.exe" \) -delete + @rm -rf tests/.build + @echo "clean." diff --git a/scripts/compile_check.sh b/scripts/compile_check.sh new file mode 100644 index 0000000..bc32f0e --- /dev/null +++ b/scripts/compile_check.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Compile every .c file in the repo. Fail on any error. +# Does NOT run binaries. + +set -u +CC="${CC:-gcc}" +CFLAGS="${CFLAGS:--Wall -Wextra -std=c11}" + +fail=0 +pass=0 +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +while IFS= read -r -d '' src; do + out="$tmpdir/$(basename "$src" .c).out" + if $CC $CFLAGS "$src" -o "$out" 2>"$tmpdir/err"; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + echo "FAIL: $src" + sed 's/^/ /' "$tmpdir/err" + fi +done < <(find lessons patterns practice projects challenges -name "*.c" -print0) + +echo "" +echo "compile-check: $pass passed, $fail failed." +[ "$fail" -eq 0 ] diff --git a/tests/expected_outputs/00-basics_01-types-and-output.txt b/tests/expected_outputs/00-basics_01-types-and-output.txt new file mode 100644 index 0000000..5f45a2d --- /dev/null +++ b/tests/expected_outputs/00-basics_01-types-and-output.txt @@ -0,0 +1,4 @@ +x = 5, y = 3 +sum = 8 +x is bigger +counting: 0 1 2 3 4 diff --git a/tests/expected_outputs/00-basics_02-operators.txt b/tests/expected_outputs/00-basics_02-operators.txt new file mode 100644 index 0000000..58be6b4 --- /dev/null +++ b/tests/expected_outputs/00-basics_02-operators.txt @@ -0,0 +1,9 @@ +7 + 2 = 9 +7 / 2 = 3 (int div truncates) +7 % 2 = 1 +(5 > 3) && (2 < 4) = 1 +!0 = 1 +5 & 3 = 1 +5 | 3 = 7 +5 ^ 3 = 6 +1 << 3 = 8 diff --git a/tests/expected_outputs/00-basics_03-control-flow.txt b/tests/expected_outputs/00-basics_03-control-flow.txt new file mode 100644 index 0000000..a591231 --- /dev/null +++ b/tests/expected_outputs/00-basics_03-control-flow.txt @@ -0,0 +1,4 @@ +grade: B +day 3 = Wed +evens: 0 2 4 6 8 +sum 1..5 = 15 diff --git a/tests/expected_outputs/01-functions_01-simple-functions.txt b/tests/expected_outputs/01-functions_01-simple-functions.txt new file mode 100644 index 0000000..9cae5be --- /dev/null +++ b/tests/expected_outputs/01-functions_01-simple-functions.txt @@ -0,0 +1,3 @@ +square(5) = 25 +max(7, 12) = 12 +factorial(5) = 120 diff --git a/tests/expected_outputs/01-functions_02-scope-and-stack.txt b/tests/expected_outputs/01-functions_02-scope-and-stack.txt new file mode 100644 index 0000000..55ac0e9 --- /dev/null +++ b/tests/expected_outputs/01-functions_02-scope-and-stack.txt @@ -0,0 +1,6 @@ +outer x = 10 +inner x = 20 +outer x still = 10 +counter call 1 = 1 +counter call 2 = 2 +counter call 3 = 3 diff --git a/tests/expected_outputs/02-pointers_02-pointer-arithmetic.txt b/tests/expected_outputs/02-pointers_02-pointer-arithmetic.txt new file mode 100644 index 0000000..c87bf58 --- /dev/null +++ b/tests/expected_outputs/02-pointers_02-pointer-arithmetic.txt @@ -0,0 +1,4 @@ +nums[0] via *p = 10 +nums[1] via *(p+1) = 20 +nums[2] via *(p+2) = 30 +diff in elements = 3 diff --git a/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt b/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt new file mode 100644 index 0000000..6d7d785 --- /dev/null +++ b/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt @@ -0,0 +1,4 @@ +nums[0]=10 nums[1]=20 nums[2]=30 nums[3]=40 +nums points to 10 +*(nums+2) = 30 +walking: 10 20 30 40 diff --git a/tests/expected_outputs/02-pointers_04-function-pointers.txt b/tests/expected_outputs/02-pointers_04-function-pointers.txt new file mode 100644 index 0000000..ab680b0 --- /dev/null +++ b/tests/expected_outputs/02-pointers_04-function-pointers.txt @@ -0,0 +1,3 @@ +op(3,4) via add = 7 +op(3,4) via mul = 12 +apply twice = 16 diff --git a/tests/expected_outputs/02-pointers_06-common-pointer-bugs.txt b/tests/expected_outputs/02-pointers_06-common-pointer-bugs.txt new file mode 100644 index 0000000..e5fa9a6 --- /dev/null +++ b/tests/expected_outputs/02-pointers_06-common-pointer-bugs.txt @@ -0,0 +1,2 @@ +safe: value = 7 +freed pointer set to NULL (defensive) diff --git a/tests/expected_outputs/03-memory_02-malloc-free.txt b/tests/expected_outputs/03-memory_02-malloc-free.txt new file mode 100644 index 0000000..b185f21 --- /dev/null +++ b/tests/expected_outputs/03-memory_02-malloc-free.txt @@ -0,0 +1,2 @@ +nums[0]=0 nums[1]=1 nums[2]=4 nums[3]=9 nums[4]=16 +sum = 30 diff --git a/tests/expected_outputs/04-strings_01-char-arrays.txt b/tests/expected_outputs/04-strings_01-char-arrays.txt new file mode 100644 index 0000000..c8e0126 --- /dev/null +++ b/tests/expected_outputs/04-strings_01-char-arrays.txt @@ -0,0 +1,3 @@ +name = Alice +length = 5 +first char = A diff --git a/tests/expected_outputs/04-strings_02-string-functions.txt b/tests/expected_outputs/04-strings_02-string-functions.txt new file mode 100644 index 0000000..0efb9e1 --- /dev/null +++ b/tests/expected_outputs/04-strings_02-string-functions.txt @@ -0,0 +1,5 @@ +len = 5 +copy = hello +cmp(hi, hi) = 0 +cmp(a, b) = -1 +cat = hello world diff --git a/tests/expected_outputs/04-strings_03-dangerous-strcpy.txt b/tests/expected_outputs/04-strings_03-dangerous-strcpy.txt new file mode 100644 index 0000000..9f07816 --- /dev/null +++ b/tests/expected_outputs/04-strings_03-dangerous-strcpy.txt @@ -0,0 +1,2 @@ +safe copy: hello +truncated: hello wor diff --git a/tests/expected_outputs/05-structs_01-basic-structs.txt b/tests/expected_outputs/05-structs_01-basic-structs.txt new file mode 100644 index 0000000..566add1 --- /dev/null +++ b/tests/expected_outputs/05-structs_01-basic-structs.txt @@ -0,0 +1,3 @@ +p1: (3, 4) +p2: (10, 20) +distance squared = 305 diff --git a/tests/expected_outputs/05-structs_02-nested-structs.txt b/tests/expected_outputs/05-structs_02-nested-structs.txt new file mode 100644 index 0000000..0a1ac49 --- /dev/null +++ b/tests/expected_outputs/05-structs_02-nested-structs.txt @@ -0,0 +1,4 @@ +Alice lives in Paris +via pointer: Alice / Paris +roster[0] = Bob +roster[1] = Carol diff --git a/tests/expected_outputs/07-advanced_01-typedef-enums.txt b/tests/expected_outputs/07-advanced_01-typedef-enums.txt new file mode 100644 index 0000000..dab6806 --- /dev/null +++ b/tests/expected_outputs/07-advanced_01-typedef-enums.txt @@ -0,0 +1,2 @@ +color = 2 (BLUE) +status = RUNNING diff --git a/tests/expected_outputs/07-advanced_02-unions.txt b/tests/expected_outputs/07-advanced_02-unions.txt new file mode 100644 index 0000000..b27fb91 --- /dev/null +++ b/tests/expected_outputs/07-advanced_02-unions.txt @@ -0,0 +1,2 @@ +as int = 1065353216 +as float = 1.000000 diff --git a/tests/expected_outputs/07-advanced_03-bit-manipulation.txt b/tests/expected_outputs/07-advanced_03-bit-manipulation.txt new file mode 100644 index 0000000..61b6974 --- /dev/null +++ b/tests/expected_outputs/07-advanced_03-bit-manipulation.txt @@ -0,0 +1,6 @@ +flags = 0x5 (READ | EXEC) +has READ? 1 +has WRITE? 0 +after add WRITE: 0x7 +after drop READ: 0x6 +12 * 8 via shift = 96 diff --git a/tests/expected_outputs/README.md b/tests/expected_outputs/README.md new file mode 100644 index 0000000..305ea96 --- /dev/null +++ b/tests/expected_outputs/README.md @@ -0,0 +1,15 @@ +# Expected Outputs + +Golden outputs. Test runner compares actual stdout to these. + +File naming: `_.txt` (slashes replaced with `_`, `.c` -> `.txt`). + +Excluded (non-deterministic or input-dependent): +- `02-pointers/01-address-and-dereference` — prints addresses +- `02-pointers/05-double-pointers` — prints argv path +- `03-memory/01-stack-vs-heap` — addresses +- `06-file-io/*` — depends on file presence +- `projects/guess/*` — random + stdin +- `projects/phonebook/*` — stdin + file + +Add new expected file when adding a deterministic lesson. diff --git a/tests/test_lessons.sh b/tests/test_lessons.sh new file mode 100644 index 0000000..c012685 --- /dev/null +++ b/tests/test_lessons.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Compile each lesson, run it, diff stdout against tests/expected_outputs/. +# Skips lessons without a matching expected file (non-deterministic ones). + +set -u +CC="${CC:-gcc}" +CFLAGS="${CFLAGS:--Wall -Wextra -std=c11}" +BUILD="tests/.build" +EXPECTED="tests/expected_outputs" +mkdir -p "$BUILD" + +pass=0 +fail=0 +skip=0 +failed_names=() + +shopt -s nullglob +for src in lessons/*/*.c; do + rel="${src#lessons/}" + # turn lessons/00-basics/01-foo.c -> 00-basics_01-foo + key="${rel%.c}" + key="${key//\//_}" + expected="$EXPECTED/${key}.txt" + + if [ ! -f "$expected" ]; then + skip=$((skip + 1)) + continue + fi + + bin="$BUILD/${key}.out" + if ! $CC $CFLAGS "$src" -o "$bin" 2>"$BUILD/${key}.cerr"; then + fail=$((fail + 1)) + failed_names+=("$key (compile)") + echo "FAIL compile: $src" + sed 's/^/ /' "$BUILD/${key}.cerr" + continue + fi + + actual="$BUILD/${key}.out.txt" + "$bin" >"$actual" 2>&1 || true + + if diff -u "$expected" "$actual" >"$BUILD/${key}.diff"; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + failed_names+=("$key (output)") + echo "FAIL output: $src" + sed 's/^/ /' "$BUILD/${key}.diff" + fi +done + +echo "" +echo "====================" +echo " passed: $pass" +echo " failed: $fail" +echo " skipped: $skip (no expected_outputs file)" +echo "====================" + +if [ "$fail" -ne 0 ]; then + echo "Failed: ${failed_names[*]}" + exit 1 +fi From 55c1a476780b107abb7dd86c83dc3cfade575e7a Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:37 +0530 Subject: [PATCH 10/15] feat(ci): GitHub Actions for compile-check + test, PR template with checklists --- .github/PULL_REQUEST_TEMPLATE.md | 30 +++++++++++++++++++++++++++++ .github/workflows/compile-check.yml | 17 ++++++++++++++++ .github/workflows/test.yml | 22 +++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/compile-check.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ec67083 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +## What changed + +- + +## Type + +- [ ] new lesson / practice / pattern +- [ ] fix (compile / output / typo) +- [ ] docs +- [ ] infra (Makefile / CI / tests) + +--- + +## Developer checklist + +- [ ] `make compile-check` passes locally +- [ ] `make test` passes locally +- [ ] New `.c` file follows lesson template (≤30 lines, output-first, BREAK IT) +- [ ] If deterministic: added matching `tests/expected_outputs/.txt` +- [ ] Linked from the topic `README.md` (if a new lesson) +- [ ] No secrets, no personal data, no large binaries + +## Reviewer checklist + +- [ ] CI green (compile-check + test) +- [ ] Tone matches repo (punchy, no fluff, no "learning journey") +- [ ] WHY comments only, no WHAT narration +- [ ] Memory diagram present where pointers/memory touched +- [ ] Golden output matches reality (not aspirational) +- [ ] No dead refs (links, file paths) diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml new file mode 100644 index 0000000..0cd8995 --- /dev/null +++ b/.github/workflows/compile-check.yml @@ -0,0 +1,17 @@ +name: compile-check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install gcc + run: sudo apt-get update && sudo apt-get install -y gcc + - name: Compile every .c file + run: make compile-check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a08c1b7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install gcc + run: sudo apt-get update && sudo apt-get install -y gcc + - name: Run lesson test suite + run: make test + - name: Fail loudly if any lesson broke + if: failure() + run: | + echo "::error::A lesson failed to compile or output drifted. PR rejected." + exit 1 From 8479e5637fdb5065818be8680a9e20a16b05e9d6 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:14:37 +0530 Subject: [PATCH 11/15] docs: rewrite README, refresh ONBOARDING, add CONTRIBUTING + docs/ deep dives --- CONTRIBUTING.md | 80 +++++++++++++++++ ONBOARDING.md | 48 ++++++---- README.md | 169 +++++++++++++++++++----------------- docs/HOW_MALLOC_WORKS.md | 49 +++++++++++ docs/SEGFAULT_DEBUGGING.md | 66 ++++++++++++++ docs/WHY_C_STILL_MATTERS.md | 47 ++++++++++ 6 files changed, 362 insertions(+), 97 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/HOW_MALLOC_WORKS.md create mode 100644 docs/SEGFAULT_DEBUGGING.md create mode 100644 docs/WHY_C_STILL_MATTERS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..258e62b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing to CNoBS + +Welcome. Two rules: don't break the build, follow the template. + +--- + +## Hard rules + +1. Every `.c` file MUST compile cleanly with `gcc -Wall -Wextra -std=c11`. +2. Every lesson MUST pass `make test`. +3. If you add a deterministic lesson, you MUST add a matching golden file in [tests/expected_outputs/](tests/expected_outputs/). +4. No PR merges if CI fails. Fix it locally first. + +--- + +## Lesson template + +```c +/* + * Lesson: [topic] | Time: ~N min | Difficulty: easy|medium + * Why this matters: [one sentence, no fluff] + * + * Run: gcc file.c && ./a.out + * + * EXPECTED OUTPUT: + * ------------------------------------------ + * [exact stdout] + * ------------------------------------------ + * + * MEMORY DIAGRAM (if pointers / memory relevant) + */ + +#include +/* code: 30 lines MAX */ + +/* + * Why it works: [one paragraph] + * + * BREAK IT: + * 1. [change X, predict, run] + * 2. ... + */ +``` + +Hard cap: 30 lines of actual code per lesson. If you need more, it belongs in `patterns/` or `projects/`. + +--- + +## Workflow + +1. Fork. +2. Branch: `git checkout -b lesson/my-topic`. +3. Write the lesson + the expected_outputs file. +4. Run locally: + ```bash + make compile-check + make test + ``` +5. Both green? Push. Open PR against `main`. +6. CI runs the same checks. Green = mergeable. + +--- + +## Tone + +- Punchy. Sarcastic when honest. Never condescending. +- Comments explain WHY, not WHAT. +- No "learning journey" language. No emojis (unless asked). +- Errors fail LOUDLY. + +--- + +## Bug reports + +A `.c` file doesn't compile? That's a bug. Open an issue with: +- OS + compiler version +- Exact command run +- Exact error output + +Maintainer: [ajithakdev](https://github.com/ajithakdev) · ajithkumarks2579@gmail.com / cseajithak@gmail.com diff --git a/ONBOARDING.md b/ONBOARDING.md index e7f3101..341eecb 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -59,26 +59,26 @@ cd CNoBS ### Windows (PowerShell, using clang) ```powershell -cd lessons -clang 01-output-first.c -o out.exe +cd lessons/00-basics +clang 01-types-and-output.c -o out.exe ./out.exe ``` One-liner: ```powershell -clang 01-output-first.c -o out.exe; if ($?) { ./out.exe } +clang 01-types-and-output.c -o out.exe; if ($?) { ./out.exe } ``` ### Windows (PowerShell, using gcc from MSYS2) ```powershell -cd lessons -gcc 01-output-first.c -o out.exe +cd lessons/00-basics +gcc 01-types-and-output.c -o out.exe ./out.exe ``` ### macOS / Linux / WSL / Git Bash ```bash -cd lessons -gcc 01-output-first.c && ./a.out +cd lessons/00-basics +gcc 01-types-and-output.c && ./a.out ``` (Use `clang` instead of `gcc` on macOS if you prefer — same flags.) @@ -99,18 +99,28 @@ If you see that — setup done. You're coding C. Do them in order. Each takes 10-15 min. ``` -lessons/01-output-first.c <- start -lessons/02-pointers-visual.c -lessons/03-arrays-memory.c -lessons/04-structs.c +lessons/00-basics/ <- start: types, ops, control flow +lessons/01-functions/ <- define, call, scope +lessons/02-pointers/ <- the hard part (6 lessons, take your time) +lessons/03-memory/ <- stack vs heap, malloc/free +lessons/04-strings/ <- char arrays, the \0 +lessons/05-structs/ <- your own types +lessons/06-file-io/ <- read + write files +lessons/07-advanced/ <- typedef, unions, bit ops | -challenges/01-fix-the-loop.c <- find + fix bugs -challenges/02-pointer-puzzle.c +challenges/ <- tiered break-it labs +practice/ <- bonus problems with skeletons +patterns/ <- real copy-paste snippets | -patterns/string-search.c <- real-code snippet - | -projects/guess/guess.c <- first real program -projects/phonebook/phonebook.c +projects/guess/ <- first real program +projects/phonebook/ <- structs + file I/O combined +projects/capstone/ <- expression evaluator (final) +``` + +After setup, also try the local test runner: +```bash +make compile-check +make test ``` **How to use a lesson:** @@ -131,8 +141,8 @@ If a lesson takes more than 20 min, skip the BREAK IT and come back. Don't stall | `'gcc' is not recognized` (Windows) | Compiler not on PATH. Reopen terminal after install. Or use `clang`. | | `&&` parser error in PowerShell 5.1 | Use `; if ($?) { ... }` instead. Or upgrade to PowerShell 7. | | `a.out` not found on Windows | Windows builds `a.exe`. Use `./a.exe` or compile with `-o name.exe`. | -| Segfault | You dereferenced a bad pointer. Go back to [lessons/02-pointers-visual.c](lessons/02-pointers-visual.c). | -| Garbage output | Probably uninitialized variable or buffer overrun. Read the BREAK IT note in lesson 03. | +| Segfault | You dereferenced a bad pointer. See [lessons/02-pointers/06-common-pointer-bugs.c](lessons/02-pointers/06-common-pointer-bugs.c) + [docs/SEGFAULT_DEBUGGING.md](docs/SEGFAULT_DEBUGGING.md). | +| Garbage output | Probably uninitialized variable or buffer overrun. See the BREAK IT note in [lessons/02-pointers/03-arrays-as-pointers.c](lessons/02-pointers/03-arrays-as-pointers.c). | | `undefined reference to ...` | Forgot to `#include` a header, or function name typo. | --- diff --git a/README.md b/README.md index 7d14c8d..75621d7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ > **C, No BS.** That's it. That's the name. +[![compile-check](https://github.com/ajithakdev/learn-c/actions/workflows/compile-check.yml/badge.svg)](https://github.com/ajithakdev/learn-c/actions/workflows/compile-check.yml) +[![test](https://github.com/ajithakdev/learn-c/actions/workflows/test.yml/badge.svg)](https://github.com/ajithakdev/learn-c/actions/workflows/test.yml) + **C without the bullshit.** No 400-page books. No "hello world" for 3 chapters. No theory dumps. Code that runs, breaks, and teaches you why — in 5 minutes or it's a bad lesson. > You have 20 minutes between classes. You learn by breaking things. You quit if the lesson doesn't pay off in 5. This repo is for you. @@ -12,117 +15,127 @@ Every language you use is written in C, runs on a runtime written in C, or borrows C's mental model (pointers, memory, structs). Learn C once, read the source of anything. It's also the fastest path to understanding *why* your Python is slow. +Deep dive: [docs/WHY_C_STILL_MATTERS.md](docs/WHY_C_STILL_MATTERS.md) + --- -## Learning Path +## Learning path ``` - START HERE - | - [ lessons/ ] 10-15 min each - | - +---------------+---------------+ - | | | - basics pointers structs - (types, if, (the hard part, (real data) - loops, fns) 5 lessons w/ - ASCII memory) - | | | - +-------+-------+ - | - [ challenges/ ] - break-it labs, copy-paste, - change ONE thing, see Y - | - [ patterns/ ] - snippets people actually - write (sort, search, concat) - | - [ projects/ ] - 1. guess-the-number (beginner) - 2. phonebook (intermediate) - | - SHIP + START HARD PART SHIP + | | | + v v v + 00-basics 01-functions 02-pointers 03-memory 04-strings + (3) (2) (6) (2) (3) + | + | 05-structs 06-file-io 07-advanced + v (2) (2) (3) + mnemonics + | + v + challenges/ tier 1 (foundational) + | tier 2 (pointers) + v tier 3 (integration) + practice/ easy / medium / hard + | + v + projects/ + guess (beginner) + phonebook (intermediate) + capstone (expression evaluator) ``` ---- - -## Quick Start (under 2 minutes) - -```bash -git clone -cd CNoBS/lessons -gcc 01-output-first.c && ./a.out -``` +23 lessons. ~10-15 min each. ~8 weeks at 4-5 hrs/week. -You need: -- A C compiler. **gcc** (Linux/Mac/WSL) or **clang** (Mac). Windows: install MSYS2 or use WSL. -- A terminal. That's it. +--- -Check it works: +## Quick start ```bash -gcc --version +git clone https://github.com/ajithakdev/learn-c CNoBS +cd CNoBS +gcc lessons/00-basics/01-types-and-output.c && ./a.out ``` -If you see a version number, you're done with setup. No IDE. No `package.json`. No `node_modules`. C is a 50-year-old language that still runs on a fresh laptop with zero config. +Full setup (Windows / macOS / Linux): [ONBOARDING.md](ONBOARDING.md). --- -## How Lessons Work (Reverse Teaching) +## How a lesson works -Every lesson follows the same pattern: - -1. **Output first.** Look at what it prints. Guess what the code does. -2. **Code reveal.** Read 20-30 lines max. +1. **Output first.** Look at expected stdout. Guess what the code does. +2. **Code reveal.** Read ≤30 lines. 3. **Why it works.** One paragraph. No theory dump. -4. **ASCII diagram.** See the memory, stack, or pointer. -5. **Break it.** Change one line. Predict the new output. Run it. +4. **ASCII diagram.** Memory, stack, or pointer. +5. **BREAK IT.** Change one line. Predict. Run. See. -You're not reading. You're debugging from minute one. +You're debugging from minute one. --- -## What You'll Know By The End - -You'll write C that allocates memory, manages pointers without segfaulting (most of the time), reads and writes files, builds small data structures (linked lists, hash tables — the easy way), and reads other people's C code without your eyes glazing over. You won't be a kernel hacker. You will be able to debug a segfault, understand why `strcpy` is dangerous, and follow along when someone explains how malloc works. +## Repo map -Honest scope: **beginner → comfortable intermediate**. Not "systems programmer." Not yet. +``` +CNoBS/ +├── README.md <- you are here +├── ONBOARDING.md <- compiler setup + first run +├── CONTRIBUTING.md <- PR rules +├── Makefile <- make compile-check / make test +├── .github/workflows/ <- CI: every PR compiles + diffs output +├── lessons/ <- atomic, ≤30 lines each +│ ├── 00-basics/ types, operators, control flow +│ ├── 01-functions/ defs, scope, stack +│ ├── 02-pointers/ 6 lessons, ASCII memory every step +│ ├── 03-memory/ stack vs heap, malloc/free +│ ├── 04-strings/ char arrays, string.h, strcpy dangers +│ ├── 05-structs/ your own types +│ ├── 06-file-io/ fopen/fgets/fprintf +│ └── 07-advanced/ typedef, unions, bit ops +├── challenges/ <- tiered break-it labs +│ ├── tier-1-foundational/ +│ ├── tier-2-pointers/ +│ ├── tier-3-integration/ +│ └── solutions/ (hidden via .gitignore) +├── practice/ <- bonus problems with skeletons +│ ├── 01-easy-warmup/ +│ ├── 02-medium/ +│ └── 03-hard/ +├── patterns/ <- real copy-paste snippets +├── mnemonics/ <- one-page memory aids (ASCII) +├── projects/ +│ ├── guess/ beginner +│ ├── phonebook/ intermediate +│ └── capstone/ expression evaluator +├── tests/ <- expected_outputs + test_lessons.sh +├── scripts/ <- compile_check.sh +└── docs/ <- optional deeper reads +``` --- -## Repo Map +## Running tests locally +```bash +make compile-check # compile every .c, fail on warning +make test # run lessons, diff stdout vs golden files ``` -CNoBS/ -├── README.md <- you are here -├── lessons/ <- atomic, ~10 min each -│ ├── 01-output-first.c -│ ├── 02-pointers-visual.c -│ ├── 03-arrays-memory.c -│ └── 04-structs.c -├── challenges/ <- break-it labs -│ ├── 01-fix-the-loop.c -│ └── 02-pointer-puzzle.c -├── patterns/ <- real snippets -│ └── string-search.c -└── projects/ - ├── guess/ <- beginner - │ └── guess.c - └── phonebook/ <- intermediate - └── phonebook.c -``` + +CI does the same on every push and PR. --- -## Anti-Hype Promise +## Anti-hype promise -- No "C is hard" gaslighting. It's small. K&R is 272 pages. Your JavaScript framework docs are longer. +- No "C is hard" gaslighting. K&R is 272 pages. Your JS framework docs are longer. - No theory before code. Ever. -- No file longer than 30 lines in lessons. If it's longer, it's a pattern. -- Every `.c` file compiles. If one doesn't, open an issue. +- No lesson > 30 lines. If longer, it's a pattern or project. +- Every `.c` file compiles. If one doesn't, open an issue. CI would have caught it. + +--- + +## Contributing -Now go run `lessons/01-output-first.c`. +PRs welcome. Rules in [CONTRIBUTING.md](CONTRIBUTING.md). Hard rule: don't break the build. --- diff --git a/docs/HOW_MALLOC_WORKS.md b/docs/HOW_MALLOC_WORKS.md new file mode 100644 index 0000000..0467e89 --- /dev/null +++ b/docs/HOW_MALLOC_WORKS.md @@ -0,0 +1,49 @@ +# How malloc Actually Works + +Optional reading. Skip if you just want to write C. + +--- + +## The short version + +`malloc(n)` asks the C runtime for `n` bytes from a region called the **heap**. The runtime tracks a free list of available chunks. `free(p)` returns the chunk to that list. + +The OS hands the C runtime large pages of memory via `brk` / `sbrk` (extend program break) or `mmap` (anonymous mapping). The runtime carves those pages into the small chunks you ask for. + +--- + +## What the runtime does + +1. Round your request up to an alignment boundary (usually 8 or 16 bytes). +2. Look in the free list for a chunk big enough. +3. If found, split it and return a pointer to the payload. +4. If not, ask the OS for more memory. +5. Store metadata in the bytes BEFORE the pointer you got back (size, prev/next free pointers). + +This is why writing one byte BEFORE your pointer corrupts the heap. You smashed the metadata. + +--- + +## Implementations + +- **ptmalloc2** — what glibc uses. Per-thread arenas, multiple bins. +- **jemalloc** — Facebook. Better under contention. +- **tcmalloc** — Google. Thread-local caches. +- **mimalloc** — Microsoft. Modern, fast. + +Same API. Different perf. Same bugs bite all of them. + +--- + +## Why free() doesn't shrink your process + +`free()` returns the chunk to the runtime, not to the OS. Your process's resident memory stays high until you exit (or call `malloc_trim`, glibc-only). This surprises people watching `top`. + +--- + +## See also + +- [mnemonics/MALLOC_RULES.txt](../mnemonics/MALLOC_RULES.txt) +- [lessons/03-memory/02-malloc-free.c](../lessons/03-memory/02-malloc-free.c) +- `man 3 malloc` +- Doug Lea's allocator paper (the basis for ptmalloc) diff --git a/docs/SEGFAULT_DEBUGGING.md b/docs/SEGFAULT_DEBUGGING.md new file mode 100644 index 0000000..21f07d3 --- /dev/null +++ b/docs/SEGFAULT_DEBUGGING.md @@ -0,0 +1,66 @@ +# Debugging a Segfault + +Your program crashed with `Segmentation fault`. That's the OS killing you for touching memory you don't own. Here's the playbook. + +--- + +## Step 1 — Compile with sanitizers + +```bash +gcc -g -fsanitize=address,undefined -fno-omit-frame-pointer file.c && ./a.out +``` + +AddressSanitizer (ASan) catches: +- Out-of-bounds reads/writes +- Use-after-free +- Double-free +- Memory leaks (with `ASAN_OPTIONS=detect_leaks=1`) + +UndefinedBehaviorSanitizer (UBSan) catches: +- Signed overflow +- Misaligned loads +- Null pointer arithmetic + +If ASan flags it, the report points to the exact line. + +--- + +## Step 2 — Run under gdb + +```bash +gcc -g file.c +gdb ./a.out +(gdb) run +(gdb) bt # backtrace when it crashes +(gdb) frame 0 # jump to the crash frame +(gdb) print var # inspect locals +``` + +--- + +## Step 3 — The usual suspects + +| Symptom | Likely cause | +|---------|--------------| +| Crash on first deref | NULL pointer | +| Crash on second run | Use-after-free OR uninitialized pointer | +| Crash in printf | Bad format specifier (`%s` for non-string, missing arg) | +| Crash in strlen / strcpy | Missing `\0` terminator | +| Crash deep in libc | You smashed the heap somewhere earlier | +| Stack overflow | Infinite recursion | + +--- + +## Step 4 — When ASan + gdb give nothing + +Heap corruption can crash far from the bug. Try: +- `valgrind ./a.out` (slower than ASan but catches different things) +- Reduce the test case until it crashes deterministically +- Print pointer values before each deref. Boring. Works. + +--- + +## See also + +- [lessons/02-pointers/06-common-pointer-bugs.c](../lessons/02-pointers/06-common-pointer-bugs.c) +- [mnemonics/POINTERS_101.txt](../mnemonics/POINTERS_101.txt) diff --git a/docs/WHY_C_STILL_MATTERS.md b/docs/WHY_C_STILL_MATTERS.md new file mode 100644 index 0000000..62e004d --- /dev/null +++ b/docs/WHY_C_STILL_MATTERS.md @@ -0,0 +1,47 @@ +# Why C Still Matters in 2026 + +Not nostalgia. Not a take. The facts. + +--- + +## What's written in C + +- Linux, Windows NT kernel, macOS XNU (parts) +- SQLite, Redis, PostgreSQL (core) +- CPython, Ruby MRI, PHP, Lua, R +- nginx, OpenSSL, curl, git +- FFmpeg, ImageMagick, GIMP +- The standard library of every other language + +Every program you run today touches a C codebase before its first instruction. + +--- + +## What C teaches you that other languages hide + +- **Memory has shape.** Stack vs heap. Alignment. Padding. Cache lines. +- **Pointers are just integers** with type metadata at compile time. +- **Strings are arrays.** Length is a walk. +- **The OS hands you bytes.** Files, sockets, malloc — all return bytes. + +Once you've written C, Python's GIL, Go's escape analysis, Rust's borrow checker — they all stop being magic. + +--- + +## When you'd actually write new C + +- Kernels, drivers, embedded firmware +- Performance-critical libraries (codecs, crypto, parsers) +- Language runtimes +- Interop layers (FFI, glue code) +- Hardware-near tools (debuggers, tracing) + +For most apps in 2026, write Rust, Go, or Zig instead. But you'll read C every day if you go deep on any system. + +--- + +## The Rust question + +Rust replaces C for memory safety + performance. It will keep eating the new-C-projects pie. Existing C is here for decades. + +Learn C to **read the past**. Learn Rust to **write the future**. Both fit in the same brain. From a2d232c6601d2102c4b0138a44b83d7f579ed4fb Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:17:20 +0530 Subject: [PATCH 12/15] fix(tests): keep trailing space in two goldens to match actual stdout --- tests/expected_outputs/00-basics_01-types-and-output.txt | 2 +- tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/expected_outputs/00-basics_01-types-and-output.txt b/tests/expected_outputs/00-basics_01-types-and-output.txt index 5f45a2d..52c709c 100644 --- a/tests/expected_outputs/00-basics_01-types-and-output.txt +++ b/tests/expected_outputs/00-basics_01-types-and-output.txt @@ -1,4 +1,4 @@ x = 5, y = 3 sum = 8 x is bigger -counting: 0 1 2 3 4 +counting: 0 1 2 3 4 diff --git a/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt b/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt index 6d7d785..02a522a 100644 --- a/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt +++ b/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt @@ -1,4 +1,4 @@ nums[0]=10 nums[1]=20 nums[2]=30 nums[3]=40 nums points to 10 *(nums+2) = 30 -walking: 10 20 30 40 +walking: 10 20 30 40 From 3a165b9a7acacdd34ac82624fa70b32d49f9631b Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:17:32 +0530 Subject: [PATCH 13/15] chore: force LF line endings via .gitattributes (golden output byte-exact) --- .gitattributes | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ccdaff2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf +*.c text eol=lf +*.h text eol=lf +*.sh text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.txt text eol=lf +Makefile text eol=lf From 892a68306d438ecfd6c41198f8b13c5affa18d12 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:19:20 +0530 Subject: [PATCH 14/15] fix(lessons): print space-separated lists without trailing space --- lessons/00-basics/01-types-and-output.c | 4 ++-- lessons/02-pointers/03-arrays-as-pointers.c | 4 ++-- tests/expected_outputs/00-basics_01-types-and-output.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lessons/00-basics/01-types-and-output.c b/lessons/00-basics/01-types-and-output.c index 92b323d..06745e1 100644 --- a/lessons/00-basics/01-types-and-output.c +++ b/lessons/00-basics/01-types-and-output.c @@ -28,8 +28,8 @@ int main(void) { if (x > y) printf("x is bigger\n"); else printf("y is bigger\n"); - printf("counting: "); - for (int i = 0; i < 5; i++) printf("%d ", i); + printf("counting:"); + for (int i = 0; i < 5; i++) printf(" %d", i); printf("\n"); return 0; diff --git a/lessons/02-pointers/03-arrays-as-pointers.c b/lessons/02-pointers/03-arrays-as-pointers.c index c5c1392..228ef50 100644 --- a/lessons/02-pointers/03-arrays-as-pointers.c +++ b/lessons/02-pointers/03-arrays-as-pointers.c @@ -40,8 +40,8 @@ int main(void) { printf("nums points to %d\n", *nums); printf("*(nums+2) = %d\n", *(nums + 2)); - printf("walking: "); - for (int i = 0; i < 4; i++) printf("%d ", *(nums + i)); + printf("walking:"); + for (int i = 0; i < 4; i++) printf(" %d", *(nums + i)); printf("\n"); return 0; diff --git a/tests/expected_outputs/00-basics_01-types-and-output.txt b/tests/expected_outputs/00-basics_01-types-and-output.txt index 52c709c..5f45a2d 100644 --- a/tests/expected_outputs/00-basics_01-types-and-output.txt +++ b/tests/expected_outputs/00-basics_01-types-and-output.txt @@ -1,4 +1,4 @@ x = 5, y = 3 sum = 8 x is bigger -counting: 0 1 2 3 4 +counting: 0 1 2 3 4 From e72498e1b6565348edef894827e5cdb3305932fb Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Tue, 26 May 2026 15:19:37 +0530 Subject: [PATCH 15/15] fix(tests): strip stale trailing space from arrays-as-pointers golden --- tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt b/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt index 02a522a..6d7d785 100644 --- a/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt +++ b/tests/expected_outputs/02-pointers_03-arrays-as-pointers.txt @@ -1,4 +1,4 @@ nums[0]=10 nums[1]=20 nums[2]=30 nums[3]=40 nums points to 10 *(nums+2) = 30 -walking: 10 20 30 40 +walking: 10 20 30 40