C Fundamentals
A Complete 8-Chapter Programming Course
Table of Contents
- Getting Started
- Variables & Basic Types
- Operators & Control Flow
- Loops
- Functions
- Arrays
- Pointers
- Strings
Getting Started
rust1-1 opened by naming the tradeoff Rust refuses to accept: manual-control performance without manual-control memory bugs. C is the language that tradeoff was named against โ no garbage collector, no borrow checker, no compiler enforcing safety at all. Just the programmer, trusted completely. This course starts exactly where that trust begins.
The Compile-Then-Link Model
C has no virtual machine and no interpreter โ source code becomes a real, standalone executable in two genuinely separate steps. Compilation turns each .c file into an object file (machine code, but not yet a runnable program). Linking then combines every object file, plus any libraries being used, into one final executable. A single gcc command usually runs both steps back to back โ but they remain conceptually distinct, and Chapter 5 of Course 2 (Multi-File Projects) is built entirely around treating them as such.
Installing a Compiler: gcc and clang
Unlike Rust's rustup โ one official installer managing one official toolchain โ C has no single canonical compiler. gcc (GNU Compiler Collection) and clang (LLVM's compiler) are the two most common; both compile standard C, both are genuinely widely used, and neither is "more official" than the other the way rustc is for Rust.
The Smallest C Program
#include <stdio.h> pulls in the standard I/O library's declarations โ printf isn't a language keyword, it's an ordinary library function, declared in that header. int main() is the entry point, same role as Rust's fn main() โ but notice the explicit return 0;: C's main reports a real exit status back to the operating system, and leaving it out (pre-C99) was undefined behavior, a theme this course returns to often.
Compiling & Running
There is no single-command equivalent of cargo run or go run โ compiling and running are always two separate commands. -o hello names the output executable explicitly.
gcc hello.c with no -o flag silently compiles to a file named a.out โ a genuine, still-common beginner surprise. Always name the output explicitly.
No Built-In Package Manager or Build Tool
This is the first real toolchain difference from both Rust and Go. Go's toolchain is minimal but complete โ go build handles everything. Rust bundles even more into cargo โ building, dependencies, testing, all official. C has none of this as an official standard: no built-in package registry, no single blessed build tool. Third-party tools like make (Course 2's own Chapter 5) and CMake exist and are genuinely widely used โ but nothing plays the role cargo plays for Rust. This isn't an oversight; C predates the very idea of a language-integrated package manager by decades.
| Concept | Rust | C |
|---|---|---|
| Compile & run | cargo run (one step) | gcc file.c -o out && ./out (two steps) |
| Toolchain manager | rustup (official, singular) | none โ gcc or clang, your choice |
| Package manager | cargo + crates.io | none official โ make/CMake handle builds only |
| Print a line | println!(...) | printf("...\n") |
| Entry point | fn main() | int main() |
Coding Challenges
Write a C program that prints your name and a short greeting using two separate printf calls. Compile it with gcc, explicitly naming the output file, and run it.
๐ View solutionCompile a program without the -o flag. Identify the name of the resulting executable, then run it directly by that name.
๐ View solutionExplain, in your own words, why C having no official package manager or build tool is a genuinely different situation than Rust or Go โ not simply "C is missing a feature," but a real consequence of when and why the language was designed.
๐ View solutionChapter 1 Quick Reference
- Compile-then-link: two conceptually separate steps, usually run together by one
gcccommand - gcc / clang โ no single official compiler, unlike Rust's
rustc #include <stdio.h>โ pulls in library declarations;printfis an ordinary function, not a keywordint main() { ... return 0; }โ the entry point; the explicit return value mattersgcc file.c -o nameโ always name the output; omitting-oproducesa.out- No official package manager or build tool โ
make/CMake fill that gap unofficially, covered in Course 2 - Next chapter: variables, basic types, and why the standard doesn't fix their exact sizes
Variables & Basic Types
Chapter 1 showed C trusting the programmer with the toolchain itself. This chapter shows the same trust applied to something Rust and Go both nail down precisely: how big a number actually is.
Declaring Variables
type name = value; โ no let, no var, no inference. Every variable's type is written explicitly, every time.
The Basic Types
intโ a whole numberfloatโ single-precision floating pointdoubleโ double-precision floating point, the default for real workcharโ a single byte, holding a small integer that's interpreted as a character (there is no separate "character" type underneath โchargenuinely is a small integer)
sizeof and Why Type Sizes Aren't Fixed
This is the chapter's real subject. In Rust, i32 is exactly 32 bits, everywhere, by definition โ the type's name states its size as a language guarantee. In C, the standard only guarantees minimums: int is guaranteed to be at least 16 bits, but is commonly 32 bits on today's platforms โ "commonly," not "guaranteed."
Writing genuinely portable C โ code that behaves identically across every platform โ means never assuming int is exactly 4 bytes, something Rust or Go code never has to worry about at all.
Fixed-Width Types via <stdint.h>
C99 added exactly the guarantee Rust and Go bake in by default โ opt-in, not automatic.
Signed vs. Unsigned
Every integer type has a signed (default) and unsigned variant. This distinction hides a genuine asymmetry worth flagging now, well before Course 3's Undefined Behavior chapter covers it in full: unsigned overflow is well-defined โ it wraps around (UINT_MAX + 1 becomes 0). Signed overflow is undefined behavior โ the standard makes no promise about what happens at all, not even wraparound.
| Concept | Rust | C |
|---|---|---|
| Integer size guarantee | i32/i64 โ exact, by definition | int/long โ minimum only, platform-dependent |
| Opt-in exact width | n/a โ always exact | int32_t/uint64_t via <stdint.h> |
| Unsigned overflow | panics in debug, wraps in release | always wraps โ well-defined |
| Signed overflow | panics in debug, wraps in release | undefined behavior โ no guarantee at all |
sizeof(int) looks like a function call, but sizeof is genuinely a compile-time operator โ sizeof x (no parentheses) works too when applied directly to a variable. The parenthesized form is just the conventional style.
Coding Challenges
Write a program that prints the sizeof int, char, float, double, and long on your system, one per line, using %zu format specifiers.
๐ View solutionDeclare an unsigned char variable, set it to 255, add 1 to it, and print the result. Explain what happened and why it's well-defined behavior rather than a bug.
๐ View solutionExplain why int32_t from stdint.h is a genuinely different guarantee than plain int, and why Rust's i32 never needed an equivalent "opt-in exact width" type at all.
๐ View solutionChapter 2 Quick Reference
- int/float/double/char โ explicit types, no inference
charis genuinely a small integer, not a distinct character type- The standard guarantees minimum sizes only โ
intis commonly 4 bytes, never guaranteed <stdint.h>'sint32_t/uint64_tโ opt-in, Rust/Go-style exact widths- Unsigned overflow โ well-defined wraparound; signed overflow โ undefined behavior, no guarantee
sizeofis an operator, not a function โ parentheses are convention, not a requirement- Next chapter: operators, control flow, and C's historical lack of a true boolean type
Operators & Control Flow
Chapter 2 showed the standard being deliberately vague about integer sizes. This chapter shows the same looseness applied to something even more fundamental โ what counts as "true."
Arithmetic, Comparison & Logical Operators
The familiar set โ + - * / %, == != < > <= >=, && || ! โ behaves mostly as expected, with one early trap: dividing two integers truncates, it doesn't round.
No True Boolean Before C99
This is the real subject of the chapter. Original C had no boolean type at all. Truth was just an int: 0 means false, any nonzero value means true โ including negative numbers. Every if, every while, every logical expression in classic C evaluates to a plain integer, not a distinct boolean value the way Rust's bool always has been.
<stdbool.h> in Modern C
C99 added bool, true, and false โ but even now, they're not a genuinely new primitive the way Rust's bool is. Under the hood, bool is a macro for _Bool (an integer type that can only hold 0 or 1), and true/false are just 1 and 0 in disguise.
if / else
Because truth is just "nonzero," C allows any expression as a condition โ not just a genuine comparison.
This flexibility hides a classic, still-common bug: writing = (assignment) where == (comparison) was meant. if (x = 5) compiles cleanly, assigns 5 to x, and the condition is then "is 5 nonzero" โ always true.
switch
Cases fall through by default โ execution continues into the next case unless an explicit break stops it. Rust's match arms never fall through; each one is fully self-contained.
| Concept | Rust | C |
|---|---|---|
| Boolean type | bool โ a real primitive, always existed | _Bool/bool since C99 โ really an int in disguise |
| Condition requirement | must be a genuine bool | any nonzero expression works |
| match/switch fallthrough | never โ each arm is isolated | falls through by default without break |
= vs == mistake is common enough that -Wall flags it directly. Compiling with warnings enabled from day one catches a real class of bugs the language itself won't stop you from writing.
match, C's switch silently falls through into the next case unless every case ends in break โ a missing break doesn't warn by default and can execute code that was never meant to run for that case.
Coding Challenges
Write a program computing 7 / 2 as an int and 7.0 / 2 as a double, printing both. Explain the difference in the output.
๐ View solutionWrite a switch statement over an int day (1-3) that prints a different message for each day, deliberately omitting one break to demonstrate fallthrough, then fix it.
๐ View solutionExplain why if (x = 5) compiles without error in C, what it actually does, and why the equivalent mistake isn't possible in Rust.
๐ View solutionChapter 3 Quick Reference
- Integer division truncates โ
5 / 2is2, not2.5 - Classic C has no boolean type โ
0is false, any nonzero value is true <stdbool.h>(C99+) โbool/true/false, but really_Bool(an int) underneath- Any nonzero expression is a valid
ifcondition โ not just genuine comparisons = vs ==in a condition compiles silently โ enable-Wallto catch itswitchfalls through by default โ every case needs an explicitbreak- Next chapter: loops โ for/while/do-while and break/continue
Loops
Loops are one of the few places C's syntax and Rust's genuinely diverge in shape, not just in guarantees. This chapter covers all three C loop forms and where each one's closest Rust equivalent actually is.
The Three-Part for Loop
Init, condition, and increment, all in the parentheses. Rust has no equivalent syntax at all โ Rust's own for only ever iterates over a range or an iterator (for i in 0..5), never a raw three-part counter. C's version is lower-level: nothing stops the counter, the bound, or the step from being anything at all.
while and do-while
while checks its condition before the first iteration โ the body might never run. do-while checks after โ the body always runs at least once. Rust has no do-while construct at all; the closest equivalent is a plain loop { ... if !condition { break; } }, spelled out manually rather than given its own keyword.
break and continue
Same meaning as in Rust โ break exits the loop immediately, continue skips straight to the next iteration.
Loop Variable Scope: C89 vs. C99
Genuinely older C (C89) required loop counters to be declared before the loop, since variable declarations had to appear at the top of a block โ for (int i = ...) wasn't legal syntax at all. C99 relaxed this, allowing the now-familiar in-loop declaration. Legacy C codebases still sometimes show the older style; recognizing it matters when reading real, older C.
Infinite Loops
C has no dedicated keyword for "loop forever" โ the idiom is for (;;) or while (1), both relying on an empty or always-true condition. Rust's loop keyword states the same intent explicitly and self-documentingly, rather than as a side effect of an empty condition.
| Concept | Rust | C |
|---|---|---|
| Counted loop | for i in 0..5 | for (int i = 0; i < 5; i++) |
| Check-after loop | no dedicated keyword | do { ... } while (cond); |
| Infinite loop | loop { ... } | for (;;) or while (1) |
for (int i = 0; ...) keeps the counter's scope tightly bound to where it's actually used โ simply better practice in new code, even though the older C89 style remains valid and appears in legacy codebases.
} while (cond); โ the semicolon after the closing parenthesis is required and easy to forget, since no other block-ending construct in C needs one.
Coding Challenges
Write a for loop that prints the numbers 1 through 10, then rewrite the same logic using a while loop instead.
๐ View solutionWrite a do-while loop that runs its body at least once even though its condition is false from the start, printing a message that proves it ran. Explain why a plain while loop couldn't do this.
๐ View solutionExplain why C's for (;;) idiom for an infinite loop is a side effect of the loop's general syntax, while Rust's loop keyword is a dedicated, self-documenting construct โ and what that difference reflects about each language's design philosophy.
๐ View solutionChapter 4 Quick Reference
for (init; cond; incr)โ Rust has no equivalent syntax, only range/iterator-basedforwhileโ checks before;do-whileโ checks after, always runs at least oncebreak/continueโ same meaning as Rust- C89 required loop variables declared before the loop; C99 allows in-loop declaration
for (;;)/while (1)โ C's infinite-loop idiom, vs. Rust's dedicatedloopkeyworddo { ... } while (cond);โ don't forget the trailing semicolon- Next chapter: functions โ declarations, definitions, and pass-by-value by default
Functions
Chapter 1 briefly used main without explaining what makes a function "known" to the compiler. This chapter covers that directly โ and sets up the exact problem pointers exist to solve next.
Function Syntax
Declarations vs. Definitions
A declaration (or prototype) tells the compiler a function exists and its exact signature โ return type, name, parameter types โ without providing a body. A definition provides the actual body. C compiles top to bottom in a single pass: calling a function before the compiler has seen either its declaration or its definition is an error. Rust, by contrast, allows calling any function defined anywhere in the same module, in any order โ no forward declaration needed.
Header Files
A .h file conventionally holds declarations, shared across multiple .c files via #include โ the mechanism that lets one source file call a function defined in a completely different one. Course 2's Multi-File Projects & Makefiles chapter covers this in full; for now, know that a header is essentially a shared table of "these functions exist, here are their signatures."
Pass-by-Value by Default
Function arguments are copied into the function โ modifying a parameter inside the function has no effect on the caller's original variable. This is genuinely worth a three-way comparison: Go is also pass-by-value by default, exactly like C โ a rare point of agreement between the two. Rust is the outlier, requiring an explicit choice: pass by value (moves or copies), or pass a reference (&T/&mut T) deliberately.
Simulating Pass-by-Reference With Pointers
To let a function genuinely modify the caller's variable, C requires passing a pointer explicitly โ the caller's address, not its value. This is exactly what the very next chapter covers in depth; for now, just recognize that "I need the function to change my variable" is the specific problem pointers solve.
| Concept | C | Go | Rust |
|---|---|---|---|
| Default argument passing | by value (copy) | by value (copy) | by value โ moves or copies |
| Passing a reference | explicit pointer (&var) | explicit pointer (&var) | explicit reference (&var / &mut var) |
| Forward declaration needed? | yes โ top-to-bottom compilation | no | no |
int and accept the code โ a real, dangerous default mostly removed in modern C, but worth recognizing if reading older, pre-standard codebases.
Coding Challenges
Write a function multiply(int a, int b) that returns the product of its two arguments, declared with a prototype above main and defined below it. Call it from main and print the result.
๐ View solutionWrite a function that attempts to double an int parameter by reassigning it inside the function body. Call it from main with a variable set to 10, then print the variable afterward. Explain the output.
๐ View solutionExplain why Rust doesn't require forward declarations for functions defined later in the same file, while C does โ what does this reveal about how each language's compiler actually processes source code?
๐ View solutionChapter 5 Quick Reference
- Declaration/prototype โ signature only, no body; definition โ the actual body
- C compiles top to bottom โ a function must be declared or defined before it's called
- Header files (.h) โ shared declarations across multiple .c files, via #include
- Arguments are passed by value by default โ a real point of agreement with Go, unlike Rust's explicit reference choice
- Modifying a parameter inside a function never affects the caller's variable โ pointers (next chapter) are how C works around this
- Next chapter: arrays โ fixed size, no bounds checking, and the first real "manual control" moment
Arrays
Every chapter so far has shown small toolchain and syntax differences from Rust. This one is different โ arrays are where C's total absence of a safety net becomes something you can actually see happen.
Declaring & Initializing Arrays
A plain C array has a fixed size, known at compile time โ genuinely comparable to Rust's own fixed-size [T; N] array (not Vec, which can grow). Both languages have this exact concept. What happens when you go past the end is where they stop agreeing.
No Bounds Checking โ The Real Difference
Accessing values[10] on a 5-element array is not an error in C. It's not a panic. It compiles, it runs, and it reads or writes whatever memory happens to sit past the array โ memory that belongs to something else entirely. Rust's [T; N], by contrast, checks every index access at runtime and panics on an out-of-bounds index โ a controlled, visible failure instead of silent memory corruption.
A Concrete Demonstration
Two adjacent local variables often sit next to each other in memory. Writing past the end of one array can silently overwrite the other โ no error, no warning, just a value that changed for no visible reason.
Course 2's Memory Bugs chapter covers this class of problem in full, with real tooling (valgrind) to catch it. For now, the point is simpler: C genuinely will not stop this.
Array-to-Pointer Decay, Previewed
Chapter 5 established pass-by-value as C's default. Arrays quietly break that rule: passing an array to a function actually passes a pointer to its first element, not a copy of the whole array. Chapter 7 covers pointers in full โ this is just naming the exception now, so it doesn't feel unexplained later.
Getting the Array's Size
sizeof(arr) / sizeof(arr[0]) computes the element count โ but only where the array was actually declared. Once it decays to a pointer (inside a function it was passed to), sizeof on the parameter gives the pointer's size, not the array's.
| Concept | Rust [T; N] | C |
|---|---|---|
| Fixed size, known at compile time | yes | yes |
| Out-of-bounds access | panics โ a controlled, visible failure | undefined behavior โ silent, no check at all |
| Passed to a function | by value (or reference, explicit) โ stays a real array | decays to a pointer to the first element |
sizeof(arr) / sizeof(arr[0]) only works in the scope where the array itself was declared. Compute it there, and pass the count as a separate parameter โ a genuinely common, real beginner trap otherwise.
Coding Challenges
Declare an int array of 6 elements, initialize it with values, and print each element using a for loop and the sizeof(arr)/sizeof(arr[0]) trick to determine the loop bound.
๐ View solutionDeclare a 3-element array and a separate int variable directly after it. Write past the end of the array (e.g. arr[4] or arr[5]) and print the separate variable afterward. Explain what you observe and why C allows it.
๐ View solutionExplain why sizeof(arr)/sizeof(arr[0]) gives the correct element count in the function where an array is declared, but gives a wrong answer if computed inside a different function the array was passed into.
๐ View solutionChapter 6 Quick Reference
- C arrays are fixed-size, known at compile time โ genuinely comparable to Rust's
[T; N] - No bounds checking, ever โ an out-of-range access is undefined behavior, not an error or a panic
- Rust's
[T; N]checks every access and panics on out-of-bounds โ the central contrast this chapter exists to demonstrate - Arrays passed to a function decay to a pointer to the first element โ breaking Chapter 5's pass-by-value rule
sizeof(arr)/sizeof(arr[0])only works where the array was declared โ not after it decays to a pointer- Next chapter: pointers โ the mechanism arrays already quietly depend on
Pointers
Chapter 5 needed a pointer to let a function modify a caller's variable. Chapter 6's arrays turned out to already secretly be pointers. This chapter finally names the mechanism directly โ and closes the loop this whole course opened back in Chapter 1.
What a Pointer Actually Is
A pointer is a variable whose value is a memory address. & (address-of) gets a variable's address; * (dereference) accesses the value stored at an address.
Why C Needs Pointers
- Simulating pass-by-reference โ Chapter 5's own unsolved problem: pass
&xinstead ofx, and the function can modify the caller's variable through the pointer - Working with arrays โ Chapter 6's decay is pointer usage; every array access secretly goes through this exact mechanism
- Dynamic memory โ Course 2's central chapter; memory allocated at runtime is only ever reachable through a pointer
Pointer Arithmetic
p + 1 doesn't advance by one byte โ it advances by sizeof the pointed-to type. This is exactly what makes array indexing work: arr[i] is literally defined as *(arr + i), the same operation spelled two different ways.
The Null Pointer & Dereferencing It
NULL represents "points at nothing." Dereferencing a null (or uninitialized) pointer is undefined behavior โ typically a crash, but never guaranteed. Rust's safe code has no null pointers at all โ Option<T> replaces the entire concept, forcing an explicit check before any value can be used, eliminating this bug category by design rather than by convention.
What Rust's Borrow Checker Exists to Prevent
Here's the real payoff. A dangling pointer is a pointer to memory that's already been freed, or has gone out of scope โ the pointer still looks perfectly valid, but what it points to is gone. C lets you create and use one freely; it's simply undefined behavior. Rust's borrow checker makes this a compile error โ the program literally won't build if a reference could ever outlive the data it points to. This is the exact mechanism Rust's own course named as its founding goal back in rust1-1, and it's this specific bug class it exists to eliminate.
| Concept | Rust | C |
|---|---|---|
| Null values | no null โ Option<T>, checked explicitly | NULL โ dereferencing it is undefined behavior |
| Dangling references | compile error โ the borrow checker refuses to build | undefined behavior โ compiles and runs, may crash later or corrupt memory |
| Pointer arithmetic | not available on safe references | freely available โ scales by the pointed-to type's size |
NULL, the moment it's declared.
&local_var hands the caller a pointer to memory that's already gone the instant the function returns โ a classic dangling-pointer bug, and precisely the class of error Rust's borrow checker refuses to compile.
Coding Challenges
Declare an int variable, a pointer to it, print the value through the pointer, then modify the value through the pointer and print the original variable directly to confirm it changed.
๐ View solutionWrite a function swap(int *a, int *b) that swaps the values two pointers point to. Call it from main with two variables and print both before and after to prove the swap worked.
๐ View solutionExplain, precisely, what a dangling pointer is, why C allows creating and using one, and why Rust's borrow checker refuses to compile a program that could ever produce one.
๐ View solutionChapter 7 Quick Reference
&โ address-of;*โ dereference (also used in a declaration to mean "this is a pointer")- Pointers solve pass-by-reference, array access, and (Course 2) dynamic memory
- Pointer arithmetic scales by the pointed-to type's size โ
arr[i]is literally*(arr + i) NULLโ dereferencing it is undefined behavior; Rust'sOption<T>removes null entirely- Dangling pointer โ points to freed/out-of-scope memory; UB in C, a compile error in Rust
- Never return the address of a local variable
- Next chapter: strings โ char arrays, null termination, and the classic buffer-overflow risk
Strings
C has no dedicated string type. Everything Chapters 6 and 7 covered โ fixed-size arrays, no bounds checking, pointer arithmetic โ applies directly to text, exactly as-is. This closing chapter of Course 1 is where those two chapters' consequences become the most historically significant.
Strings Are char Arrays
A string is just an array of char, nothing more.
Null Termination
The '\0' byte marks the end. There is no separate length field anywhere โ C strings carry no length of their own. Rust's String stores its length directly, alongside the data; finding a C string's length means scanning forward until '\0' is found, an O(n) operation every single time, unlike Rust's O(1) length lookup.
String Literals vs. Char Arrays
A real, important distinction: char *s = "hello" points at a string literal, which may live in read-only memory โ writing through s is undefined behavior. char s[] = "hello" copies the characters into a genuinely mutable local array.
string.h Functions
strcpy vs. strncpy โ The Classic Buffer Overflow
strcpy copies a string with zero bounds checking โ exactly Chapter 6's array-overflow problem, applied specifically to text. Copying a longer string into a smaller buffer silently writes past its end. This single mistake is one of the most historically significant vulnerability classes in software security.
strncpy takes a maximum length and is safer โ but has its own genuine gotcha: if the source is at least n characters long, strncpy does not guarantee the result is null-terminated at all.
| Concept | Rust String | C |
|---|---|---|
| Length tracking | stored directly โ O(1) lookup | none โ scan for '\0', O(n) every time |
| Bounds-checked copy | always, by design | strcpy โ never; strncpy โ partially, with its own gotcha |
| Mutability of a literal | n/a โ owned data is always mutable if declared so | a string literal is read-only; modifying it is UB |
"hello" (5 characters) needs 6 bytes, not 5 โ one extra for '\0'. Forgetting this is one of the most common sizing mistakes in C.
>= n, strncpy copies exactly n bytes and stops โ with no guarantee the result is null-terminated. Treating strncpy as a fully "safe" drop-in replacement for strcpy is itself a real, common mistake.
Coding Challenges
Declare a char array using the char s[] = "..." form, print it, modify one character, and print it again to confirm the modification worked.
๐ View solutionWrite a function that computes a string's length by manually scanning for the null terminator (without calling strlen), and compare its result against the real strlen() for a test string.
๐ View solutionExplain the specific gotcha with strncpy that makes it not a fully safe replacement for strcpy, and describe what a caller must do afterward to guarantee the result is properly null-terminated.
๐ View solutionChapter 8 Quick Reference โ Course 1 Complete
- A C string is just a
chararray โ no dedicated string type exists '\0'marks the end โ no separate length is ever stored, unlike Rust'sStringchar *s = "..."is a read-only literal;char s[] = "..."is a genuine mutable copystrcpyโ zero bounds checking, the classic buffer-overflow sourcestrncpyโ bounded, but doesn't guarantee null-termination if the source is>= ncharacters- Always size a buffer for the content plus one byte for
'\0' - Course 1 complete. Course 2 picks up with structs, unions, and dynamic memory โ C's own central safety-tradeoff chapter