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 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 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/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/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/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 874615b..75621d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # 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. + +[![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. @@ -10,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) +23 lessons. ~10-15 min each. ~8 weeks at 4-5 hrs/week. -```bash -git clone -cd CNoBS/lessons -gcc 01-output-first.c && ./a.out -``` - -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/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/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/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/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/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. + */ 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. diff --git a/lessons/01-output-first.c b/lessons/00-basics/01-types-and-output.c similarity index 93% rename from lessons/01-output-first.c rename to lessons/00-basics/01-types-and-output.c index 92b323d..06745e1 100644 --- a/lessons/01-output-first.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/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-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/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/03-arrays-memory.c b/lessons/02-pointers/03-arrays-as-pointers.c similarity index 94% rename from lessons/03-arrays-memory.c rename to lessons/02-pointers/03-arrays-as-pointers.c index c5c1392..228ef50 100644 --- a/lessons/03-arrays-memory.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/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/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 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 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. | ++-----------------------------------------------------------+ 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. */ 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; +} 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; +} 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