Dynamic Memory

Course 2 · Ch 2
Dynamic Memory
The second half of this track's central payoff — what Rust's ownership model does automatically, done entirely by hand

c1-7 named dangling pointers as the exact bug class Rust's borrow checker exists to prevent. This chapter shows where those dangling pointers actually come from — and hands you the same responsibility Rust's ownership system otherwise carries for you.

Stack vs. Heap

The stack is automatic: a local variable's memory exists for exactly as long as its function is running, freed the instant that function returns — this is precisely what made returning &local_var in c1-7 a dangling pointer. The heap is manual: memory allocated there exists until something explicitly releases it, with no connection to any particular function's lifetime, and effectively unlimited size.

malloc

Allocates raw, uninitialized memory of a given byte size, returning void * — cast or assigned to a typed pointer. On failure, it returns NULL, which must always be checked; Rust, by contrast, typically aborts automatically on allocation failure rather than returning a value the caller might forget to verify.

int *nums = malloc(5 * sizeof(int)); if (nums == NULL) { // allocation failed — must be handled, not assumed away }

calloc

Like malloc, but zero-initializes the memory, and takes element count and element size as two separate arguments. The zero-init distinction genuinely matters — malloc's memory is garbage until written, and reading it before initializing is its own class of bug.

int *nums = calloc(5, sizeof(int)); // all 5 ints start at 0

realloc

Resizes a previous allocation — and may move it to an entirely new address. The returned pointer can genuinely differ from the one passed in, and the old pointer becomes invalid the instant realloc succeeds.

int *temp = realloc(nums, 10 * sizeof(int)); if (temp == NULL) { // realloc failed — nums is still valid and unchanged; don't overwrite it } else { nums = temp; // only reassign after confirming success }

free

Releases heap memory back to the system. Immediately afterward, the pointer that referenced it becomes a genuine dangling pointer — this is the exact mechanism c1-7 warned about, closing the loop directly: freeing is how dangling pointers are most commonly created in real code.

free(nums); // nums now points at freed memory — using it here is undefined behavior

Manual Memory Management vs. Rust's Ownership

This is the chapter's real subject. In C, you decide when to call free, and nothing enforces it. Forget it — a leak. Call it twice on the same pointer — a double-free, undefined behavior. Use the pointer after freeing — use-after-free, also undefined behavior. Rust's ownership model (rust1-3) tracks, at compile time, exactly when a value's owner goes out of scope, and automatically inserts the equivalent of free at precisely that point — no leaks in safe code, no double-frees, no use-after-free, guaranteed by the compiler rather than the programmer's own discipline. Every rule this chapter just described by hand is what Rust's ownership system enforces silently, every time, without exception.

ConceptRustC
When memory is freedautomatically, when the owner goes out of scopeonly when you explicitly call free()
Forgetting to freenot possible in safe codea memory leak
Freeing twicenot possible — ownership moves, can't double-dropa double-free — undefined behavior
Using memory after it's freedcompile error — the borrow checker rejects ita use-after-free — undefined behavior
Write the free right after the allocation
A genuinely effective habit: write the matching free immediately after an allocation, before writing the code that uses the memory in between — then move the free to its real, correct location. It's much harder to forget a free you already wrote once than one you were planning to add "later."
Never reassign realloc's result directly into the original pointer
ptr = realloc(ptr, new_size); is a real, common bug: if realloc fails and returns NULL, that NULL overwrites ptr — permanently losing the only reference to the still-valid original allocation, which now leaks with no way to free it. Always assign to a temporary variable first.

Coding Challenges

Challenge 1

Allocate an array of 5 ints with malloc, fill it with values 1 through 5, print them, then free the memory.

📄 View solution
Challenge 2

Allocate an array of 3 ints with calloc, print all three values before writing anything to them, then explain what they show and why, contrasted with what malloc's uninitialized memory would show instead.

📄 View solution
Challenge 3

Explain, precisely, why Rust's ownership model makes a double-free structurally impossible in safe code, rather than merely unlikely or discouraged.

📄 View solution

Chapter 2 Quick Reference

  • Stack — automatic, freed on function return; heap — manual, freed only by explicit action
  • malloc — uninitialized memory, must check for NULL
  • calloc — zero-initialized memory, count and size given separately
  • realloc — may move the allocation; always assign to a temp variable first, never the original pointer
  • free — releases memory; the pointer becomes dangling immediately afterward
  • Leaks, double-frees, use-after-free — all manual failure modes in C; all structurally prevented by Rust's ownership model at compile time
  • Next chapter: Memory Bugs — the tooling (valgrind) that catches exactly these mistakes