C Advanced
A Complete 6-Chapter Programming Course
Table of Contents
- Bit Manipulation
- Building Data Structures From Scratch
- Concurrency with pthreads
- Undefined Behavior Deep Dive
- Debugging & Tooling
- Capstone: Building a Small Project
Bit Manipulation
Course 3 starts at the smallest possible unit: individual bits. c1-3 listed the bitwise operators briefly; this chapter finally uses each one deliberately, and shows what real code built on them looks like.
The Bitwise Operators, In Depth
&(AND) โ masking: clears every bit not set in both operands|(OR) โ setting: turns on every bit set in either operand^(XOR) โ toggling: flips exactly the bits set in one operand but not the other~(NOT) โ inverting: flips every bit<</>>(shifts) โ multiply/divide by a power of two, one bit position at a time
Bit Flags
Packing several independent boolean flags into individual bits of one integer โ a compact, historically common C idiom.
Rust typically reaches for a dedicated bitflags crate, or simply a struct of separate bool fields, rather than packing everything into one integer by hand. C's version is partly a legacy of byte-economy habits โ but it's still extremely common in real-world APIs.
Bit Fields in Structs
The :N syntax packs sub-byte-width fields into fewer total bytes โ genuinely useful for memory-constrained code or mapping directly onto hardware registers.
A real caveat: the exact bit ordering and packing within a bit-field struct is implementation-defined โ not portable across different compilers or platforms, unlike every other struct layout rule this course has relied on.
A Real Example โ POSIX Open Flags
The exact bit-flag idiom from this chapter, used constantly in real systems code โ three independent flags OR'd together into one argument, exactly the pattern just built by hand above.
| Concept | Rust | C |
|---|---|---|
| Multiple boolean flags | bitflags crate, or a struct of bools | OR-together bits in one integer |
| Sub-byte struct fields | not a language feature โ hand-packed if needed | native bit-field syntax (:N), but implementation-defined layout |
FLAG_A communicates intent; 1 alone does not.
int shifted by 32 is a genuinely easy mistake to make, and Course 3's own Undefined Behavior Deep Dive covers exactly why the compiler is allowed to assume this never happens.
Coding Challenges
Define three named flag constants using left-shift (FLAG_READ, FLAG_WRITE, FLAG_EXEC), combine all three into one unsigned int with OR, then check and print whether each individual flag is set using AND.
๐ View solutionStarting from a flags variable with FLAG_READ and FLAG_WRITE both set, clear just FLAG_WRITE using AND with a NOT'd mask, and print the flags variable's value before and after to confirm only FLAG_WRITE was removed.
๐ View solutionExplain why bit-field struct layouts are described as implementation-defined rather than fully portable, and what practical risk this creates if a bit-field struct is used to interpret data written by a different compiler or platform.
๐ View solutionChapter 1 Quick Reference
&mask,|set,^toggle,~invert,<</>>shift- Bit flags โ OR to set, AND with a NOT'd mask to clear, AND to check
- Bit-field structs (
:N) pack sub-byte fields โ but layout is implementation-defined, not portable - POSIX
open()'s flag argument is the exact bit-flag idiom, used in real systems code - Shifting by
>=the type's bit width, or left-shifting a negative value, is undefined behavior - Next chapter: building data structures from scratch โ a linked list and a hash table, no standard-library containers
Building Data Structures From Scratch
C's standard library has no growable list, no hash map, nothing beyond fixed-size arrays. This chapter builds two of the most fundamental structures by hand โ deliberately, to see exactly what a language's standard collections are actually doing underneath.
No Standard Containers
Rust's Vec and HashMap are genuinely part of the language's own standard toolkit โ reach for them, they're just there. C offers nothing equivalent at all. Every real C project either hand-rolls its own structures, exactly as this chapter does, or pulls in a third-party library. This is a real, structural gap, not a minor inconvenience.
A Singly Linked List
Every node is its own heap allocation โ exactly c2-2's discipline, now applied repeatedly, once per node.
Traversing and Freeing the List
A real, easy trap: freeing a node and then reading its ->next pointer is a use-after-free โ exactly c2-3's own bug catalog. The next pointer must be saved before the current node is freed, every time.
A Simple Hash Table
An array of "buckets," each bucket its own linked list of key-value pairs, plus a hash function mapping a key to a bucket index.
Collisions & Separate Chaining
Two keys hashing to the same bucket index don't overwrite each other โ they're both appended to that bucket's own linked list, exactly the structure built earlier in this chapter, reused as the collision-handling mechanism itself. Rust's HashMap handles collisions through a much more sophisticated, already-optimized algorithm internally โ the programmer never sees or thinks about it at all.
| Concept | Rust | C |
|---|---|---|
| Growable list | Vec<T> โ built in | hand-rolled linked list, or a third-party library |
| Key-value map | HashMap<K, V> โ built in, collisions handled internally | hand-rolled, separate chaining written by hand |
| Freeing every element | automatic โ Drop runs for the whole structure | manual โ every single node must be individually freed |
HashMap is actually doing underneath its convenient interface โ not just an abstraction to trust blindly.
c2-2, but now multiplied across potentially many separate allocations instead of just one.
Coding Challenges
Build a singly linked list by calling push_front three times with different values, print every value by traversing the list, then free the entire list correctly.
๐ View solutionRewrite free_list so that it reads head->next AFTER calling free(head) instead of before, deliberately reintroducing the bug the chapter warns about. Explain exactly what goes wrong and why.
๐ View solutionExplain why separate chaining (appending to a bucket's own linked list) is a workable way to handle two keys that hash to the same bucket index, and what would go wrong if a hash table simply overwrote whatever was already in a bucket instead.
๐ View solutionChapter 2 Quick Reference
- C has no built-in growable list or hash map โ every project hand-rolls or imports one
- A linked list node is its own heap allocation โ one
malloc/freepair per node - Always save
->nextbefore callingfreeon the current node - A hash table is buckets plus a hash function; separate chaining handles collisions by appending to a bucket's own list
- Rust's
Vec/HashMapprovide all of this built in, with collisions and freeing handled automatically - Next chapter: concurrency with pthreads โ threads, mutexes, and race conditions
Concurrency with pthreads
Every "manual control" story this course has told โ pointers, memory, now data structures โ has a concurrency counterpart. This chapter shows what happens when multiple threads touch the same data with nothing forcing them to coordinate.
Creating a Thread
pthread_create's third argument is a function pointer โ a real, direct application of c2-7's own material.
A Genuine Race Condition
Two threads both running this concurrently will, almost every time, produce a final counter value less than 200,000 โ counter++ is really read-then-modify-then-write, and two threads can interleave those three steps, silently losing updates. The exact final value varies from run to run โ a genuinely non-deterministic bug.
Mutexes
Locking around the critical section ensures only one thread executes it at a time โ the race is genuinely fixed.
What C Does Not Check
This is the real payoff. Nothing in C's type system or compiler prevents forgetting to lock lock before touching counter โ any thread can read or write counter directly, at any point, with zero compile-time enforcement. The mutex is purely a convention the programmer must remember to follow at every single access site, with nothing tying the mutex to the data it's meant to protect.
Rust's Send and Sync โ Fearless Concurrency, Revisited
rust2-5's own "fearless concurrency" is exactly the fix for what C leaves entirely unchecked. Send and Sync are marker traits the compiler checks โ a type that isn't Sync genuinely cannot be shared across threads without going through a synchronization primitive like Mutex<T> at all; the code simply won't compile otherwise. Critically, Rust's Mutex<T> wraps the data itself โ the compiler refuses to compile any access to the inner value without first acquiring the lock. In C, the mutex and the data are two separate, unrelated variables, connected by nothing but the programmer's own memory.
| Concept | Rust | C |
|---|---|---|
| Mutex-to-data relationship | Mutex<T> wraps the data โ enforced by the type system | two separate variables โ connected only by convention |
| Accessing data without locking | compile error โ the data isn't reachable at all | compiles fine โ a genuine, undetected race condition |
| Unlocking on an early return | automatic โ MutexGuard's Drop unlocks it | manual โ every exit path must call unlock explicitly |
pthread_mutex_unlock leaves the mutex permanently locked โ the next thread to call lock on it blocks forever. Rust's MutexGuard unlocks automatically via Drop when it goes out of scope, on every exit path, including early returns โ the same automatic-cleanup guarantee c2-2 already established for memory, now applying to locks as well.
Coding Challenges
Write a program that spawns two threads, each incrementing a shared global counter 100,000 times with no mutex protection. Run it a few times and report whether the final value is consistently 200,000.
๐ View solutionFix Challenge 1's program by adding a pthread_mutex_t around the increment. Run it several times and confirm the final value is now consistently and correctly 200,000.
๐ View solutionExplain precisely why Rust's Mutex<T> makes an unprotected access to shared data a compile error, while C's pthread_mutex_t makes the identical mistake something that compiles and runs, with only sometimes-visible consequences.
๐ View solutionChapter 3 Quick Reference
pthread_create/pthread_joinโ spawn and wait for a thread; the entry point is a function pointercounter++is read-modify-write, not atomic โ concurrent access without synchronization is a real race conditionpthread_mutex_lock/unlockaround a critical section fixes the race- Nothing in C ties a mutex to the data it protects โ pure programmer convention, unchecked by the compiler
- Rust's
Mutex<T>wraps the data itself, andMutexGuard's automatic unlock (via Drop) prevents forgotten-unlock deadlocks entirely - Next chapter: Undefined Behavior Deep Dive โ every UB thread this course has flagged, finally covered in full
Undefined Behavior Deep Dive
"Undefined behavior" has appeared in nearly every chapter of this course. This one finally defines it precisely, explains the genuinely counter-intuitive reason it's dangerous, and catalogs every instance flagged so far in one place.
What "Undefined Behavior" Actually Means
The C standard defines three genuinely different categories, and conflating them is a real, common mistake:
- Unspecified behavior โ the standard allows several possible results; the implementation picks one, but doesn't have to document which
- Implementation-defined behavior โ unspecified, but the implementation must document its choice consistently (
c3-1's bit-field ordering is exactly this) - Undefined behavior โ the standard imposes literally no requirements at all. Anything is a conforming result โ including things that look nonsensical, like appearing to work perfectly, or genuinely misbehaving in a way that has nothing obviously to do with the actual bug
Why the Compiler Is Allowed to Assume UB Never Happens
This is the real, counter-intuitive payoff. Compilers don't merely fail to detect UB โ they actively use its absence as a proven fact during optimization. If code contains a signed overflow, the compiler is standard-sanctioned in assuming it never actually occurs, and can eliminate branches or checks that logically depended on that "impossible" case โ producing results that look like a compiler bug, but are the compiler correctly exploiting a guarantee the standard itself grants it. The C community's own phrase for the theoretical extreme of this is that UB could licitly make "demons fly out of your nose" โ a real, standard-permitted outcome, however absurd it sounds.
A Consolidated Catalog, By Chapter
- Signed integer overflow โ
c1-2 - Out-of-bounds array access โ
c1-6 - Dereferencing NULL or a dangling pointer โ
c1-7 - Buffer overflow via strcpy โ
c1-8 - Use-after-free, double-free โ
c2-2,c2-3 - Reading the "wrong" union member โ
c2-1, in most cases - Excessive or negative shifts โ
c3-1 - An unprotected data race โ
c3-3
Signed Overflow, Revisited With the Real Mechanism
The programmer intended this loop to terminate once i overflows. But since signed overflow is UB, the compiler is entitled to assume i + 1 > i is always true โ overflow "can't happen" โ and may optimize this into a genuine infinite loop, silently removing the termination condition the programmer relied on. This is a real, documented class of surprising optimizer behavior, not a hypothetical.
Strict Aliasing
A new rule: the compiler assumes two pointers of different, unrelated types never point at the same memory (with narrow exceptions like char *). Reinterpreting a float * as an int * and dereferencing it violates this โ undefined behavior โ and permits the compiler to reorder or cache reads through such pointers in ways that produce genuinely wrong results if the assumption turns out to be false. c2-1's union type-punning gotcha is closely related: a union is one of the narrow, standard-sanctioned ways to reinterpret bits in some cases โ a raw pointer cast between unrelated types is not, and violates strict aliasing outright.
Rust's Answer
Rust's entire type system and borrow checker exist substantially to make every category on this chapter's own catalog structurally impossible to write in safe code โ not detected as a separate check layered on top, but literally inexpressible. rust3-2's own unsafe blocks are the deliberate, opt-in escape hatch โ the one place a Rust programmer takes on exactly the same responsibility C always has, by choice, in a clearly marked and narrow scope.
| UB category | C | Rust |
|---|---|---|
| Signed overflow | UB โ compiler may assume it never happens | panics in debug builds, wraps in release โ always defined |
| Out-of-bounds access | UB โ no check at all | panics โ checked on every access |
| Type punning | UB via raw pointer casts; unions are narrowly sanctioned | requires an explicit unsafe block |
-fsanitize=undefined compiles in instrumentation specifically for UB classes like signed overflow and strict-aliasing violations โ genuinely distinct from c2-3's AddressSanitizer, which targets memory-safety bugs specifically. Real projects commonly run both together.
Coding Challenges
For each entry in this chapter's consolidated UB catalog, name which specific chapter first introduced it and, in one sentence each, what triggers it.
๐ View solutionExplain why "the compiler is allowed to assume UB never happens" is a genuinely different, stronger claim than "the compiler doesn't check for UB" โ and why that distinction is what makes the i + 1 > i infinite-loop example possible.
๐ View solutionExplain the difference between unspecified, implementation-defined, and undefined behavior, giving one concrete example of each from this course.
๐ View solutionChapter 4 Quick Reference
- Unspecified โ one of several allowed results, undocumented; implementation-defined โ unspecified, but documented; undefined โ no requirements at all
- The compiler actively assumes UB never happens, and optimizes on that assumption โ not merely "fails to catch it"
- A full UB catalog spans nearly every chapter of this course, from signed overflow to data races
- Strict aliasing โ pointers of unrelated types are assumed never to overlap; violating it is UB
-fsanitize=undefined(UBSan) โ a distinct tool from ASan, targeting overflow/aliasing-class UB specifically- Rust's type system makes this entire catalog structurally inexpressible in safe code โ
unsafeis the deliberate, narrow opt-out - Next chapter: Debugging & Tooling โ gdb, valgrind, ASan/UBSan, and static analysis, brought together
Debugging & Tooling
c2-3 introduced valgrind and AddressSanitizer; c3-4 added UBSan. This chapter adds the one genuinely interactive tool โ gdb โ and shows how a real project uses all of them together.
gdb โ The GNU Debugger
Compile with -g to include debug symbols. Core commands: break (set a breakpoint), run, next/step (advance a line, stepping into or over calls), print (inspect a variable), backtrace (the call stack), continue.
A genuinely rare moment of overlap rather than contrast: Rust compiles to native code too, and debugs through the same underlying mechanism (breakpoints, stepping, variable inspection) via gdb or lldb. Most of this course has been about differences โ this is a real, shared skill.
A Worked gdb Session
Post-Mortem Debugging With Core Dumps
A crashed program can leave behind a core dump โ a snapshot of its memory at the moment of the crash. gdb ./program core inspects that exact state after the fact, genuinely useful for bugs that are hard to reproduce interactively on demand.
Bringing the Whole Toolkit Together
| Tool | Category | Recompile needed? |
|---|---|---|
| valgrind | Memory bugs (c2-3) | no |
| AddressSanitizer | Memory bugs (c2-3) | yes โ fast, precise |
| UBSan | Undefined behavior (c3-4) | yes |
| gdb | Interactive step-through + post-mortem | no (with -g symbols) |
These are complementary, not competing. A real workflow often compiles with ASan and UBSan together for routine testing, then reaches for gdb once a bug is narrowed down for deep, interactive investigation.
Static Analysis
A genuinely different category: tools that examine source code without ever running it โ clang-tidy, cppcheck. They catch uninitialized variables, some overflow patterns, and suspicious casts before the program executes at all, complementing every runtime tool covered so far. Rust's own compiler performs far more of this kind of analysis by default, as a mandatory part of compilation โ borrow checking and type checking aren't optional add-ons. In C, static analysis is a separate, optional tool a team has to deliberately choose to run; nothing about the compiler itself enforces it.
pipelines1's own CI/CD material โ not something reached for only after a bug report arrives.
valgrind, ASan, and UBSan only catch bugs actually exercised by the specific code path run during that particular execution. A bug hiding in an untested branch remains completely invisible to all of them โ these tools verify what they observe, not what could theoretically happen.
Coding Challenges
Compile the chapter's buggy.c with -g, run it under gdb, set a breakpoint inside the loop, and step through enough iterations to observe i reaching 5 โ one past the array's valid indices. Report what print arr[i] shows at that point.
๐ View solutionExplain why valgrind and AddressSanitizer, run against the exact same test input, might catch a bug on one execution but not on a different one โ and what this implies about test coverage more broadly.
๐ View solutionExplain the key difference between static analysis tools (like clang-tidy) and the dynamic tools (valgrind, ASan, UBSan, gdb) covered earlier in the chapter, and why Rust's compiler performing similar analysis by default is a meaningfully different guarantee than C teams choosing to run a static analyzer.
๐ View solutionChapter 5 Quick Reference
gdbโbreak/run/next/step/print/backtrace, compiled with-g- Debugging via breakpoints/stepping is a genuinely shared skill with Rust, not a C-only technique
- Core dumps enable post-mortem inspection of a crash's exact state after the fact
- valgrind/ASan/UBSan/gdb are complementary โ a real project uses several together
- Static analysis (clang-tidy, cppcheck) examines source without running it โ optional in C, mandatory-by-default in Rust's own compiler
- No tool here catches bugs in code paths that were never actually exercised
- Next chapter: the capstone โ building a real CLI tool combining everything from this entire track
Capstone: Building a Small Project
Twenty chapters, one piece at a time. This closing chapter builds kvstore โ a real, persisted, command-line key-value store โ combining structs, a hand-built hash table, dynamic memory, file I/O, and a genuine multi-file Makefile build into one working program.
The Project โ A Persistent Key-Value Store
Every value survives between runs โ loaded from disk on startup, saved back after any change.
The Data Structure
c3-2's hash table, reused directly โ buckets of linked key-value pairs, separate chaining for collisions.
Reading & Writing Persistence
c2-6's File I/O โ a simple text format, one key=value per line, loaded on startup and rewritten after every mutation.
The CLI Interface
c1-5's functions and c1-8's string handling, applied to argv.
Memory Management Throughout
Every key and value is duplicated onto the heap when inserted โ c2-2's discipline, applied consistently end to end: freed on delete, and freed entirely on program exit, matching c3-2's own free_list-style traversal, now applied per bucket.
Structuring the Project With a Makefile
Split into hashtable.c/.h, storage.c/.h, and main.c โ c2-5's real multi-file discipline, with a Makefile tying it all together.
Where Each Piece Came From
| Piece | Chapter |
|---|---|
| Hash table + separate chaining | c3-2 |
| File save/load format | c2-6 |
| CLI argument parsing, string comparison | c1-5, c1-8 |
| malloc/free discipline for every key/value | c2-2 |
| Multi-file build, Makefile | c2-5 |
| Verified with valgrind/ASan during development | c2-3, c3-5 |
What's Still Out of Scope, Honestly
kvstore is not thread-safe โ no locking (c3-3) protects concurrent access to the file or the hash table. Values are plain strings only, not arbitrary binary data. There's no recovery logic for a corrupted save file. And no automated test suite or CI pipeline is wired up in this chapter itself, even though valgrind/ASan were used manually during development โ a real next step, not something this capstone claims to have solved.
Closing the Course & Track
From c1-1's compile-then-link model to a real, persisted, multi-file program โ every chapter in between added one specific piece of manual control C leaves entirely to the programmer, and one specific comparison to what Rust's compiler enforces instead. Twenty-one chapters, one throughline: C trusts you completely. Understanding exactly what that trust costs, and exactly what a compiler-enforced alternative buys back, is the actual lesson this entire track was built to teach.
Coding Challenges
Identify which specific chapter each of the following pieces of kvstore came from: (a) the separate-chaining collision strategy, (b) the save-file format, (c) the discipline of freeing every key and value on program exit.
๐ View solutionExplain why kvstore, as described in this chapter, is not safe if two instances of it are run concurrently against the same kvstore.db file โ name the specific chapter's material that would need to be applied to fix this.
๐ View solutionAcross the entire 21-chapter C track, name the single idea that recurs most often, and explain in your own words why understanding it deeply matters more than memorizing any individual function or syntax rule.
๐ View solutionChapter 6 Quick Reference โ Course & Track Complete
kvstorecombines: hash table (c3-2), file persistence (c2-6), CLI parsing (c1-5/c1-8), malloc/free discipline (c2-2), and a real Makefile build (c2-5)- Still out of scope, honestly: thread safety, binary values, corrupted-file recovery, automated testing
- Every discipline from Chapter 1 onward still applies at capstone scale โ nothing gets relaxed
- Course 3 complete โ C Advanced, 6 chapters
- Full C track complete โ Fundamentals + Intermediate + Advanced, 21 chapters across 3 courses