BasicsImpCodeStory_Memory.html
copyright © James Fawcett
Revised: 05/11/2026
8.0 Prologue
Every value a program creates occupies memory. How that memory is allocated, tracked,
and freed is one of the most consequential design decisions in a language. The spectrum
runs from fully manual control (C) through ownership systems (Rust) and reference
counting (Swift, CPython) to tracing garbage collection (Java, C#, Go). Each approach
trades off throughput, latency, safety, and programmer effort.
8.1 Stack vs Heap
The stack stores local variables and function call frames. Allocation
is a single pointer decrement; deallocation happens automatically when the function
returns. Stack storage is fast but limited (typically 1–8 MB per thread) and
cannot outlive its frame.
The heap provides dynamically allocated memory with programmer- or
runtime-controlled lifetime. It is large (limited by virtual address space) but
allocation and deallocation are expensive relative to stack operations, and fragmentation
accumulates over time.
// Rust: local variables on stack; Box<T> forces heap allocation
let x: i32 = 42; // stack
let b: Box<i32> = Box::new(42); // heap; freed when b is dropped
// C++: new/delete for heap; local variables on stack
int x = 42;
int* p = new int(42); // heap; must call delete p
Values that must outlive their creating scope, or whose size is unknown at compile
time, must live on the heap. Containers (Vec, String,
std::vector) allocate their elements on the heap while the container
header itself may be on the stack.
8.2 Ownership and Borrowing
Rust’s ownership system enforces memory safety at compile time with no garbage
collector. Three rules govern all values:
- Every value has exactly one owner.
- When the owner goes out of scope the value is dropped (memory freed).
- Values may be moved (ownership transfer) or borrowed (temporary reference).
let s1 = String::from("hello");
let s2 = s1; // s1 is moved; s1 is no longer valid
let s3 = String::from("world");
let r = &s3; // shared borrow; s3 still owns the data
println!("{r}"); // r valid here
// s3 dropped at end of scope
Borrowing rules: at any point in time, a value may have either
any number of shared (immutable) references or exactly one exclusive
(mutable) reference, but not both. These rules, checked at compile time, eliminate
data races and use-after-free without runtime overhead.
C++ move semantics (C++11) transfer resource ownership between objects but rely on
programmer discipline; there is no compiler enforcement equivalent to Rust’s
borrow checker.
8.3 Garbage Collection
A garbage collector automatically reclaims memory that is no longer
reachable. Programmers do not call free or delete; the
runtime periodically traces live objects and reclaims the rest.
Tracing GC (Java, C#, Go) uses a mark-and-sweep or copying algorithm.
Modern collectors are mostly concurrent (running alongside application threads) but
still impose pause overhead, memory overhead (live set × 2–5× for
efficient collection), and non-deterministic finalization.
When GC shines: large object graphs with complex sharing patterns,
rapid prototyping, applications where throughput matters more than latency.
When GC hurts: real-time systems with hard latency bounds, embedded
or resource-constrained devices, performance-sensitive inner loops.
8.4 Reference Counting
Reference counting tracks how many owners a heap allocation has.
When the count reaches zero the memory is freed immediately. This gives deterministic
destruction and avoids stop-the-world pauses, but cannot collect cycles.
// Rust: Rc<T> (single-thread), Arc<T> (thread-safe)
use std::rc::Rc;
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // reference count becomes 2
// both a and b freed when the last clone is dropped
// Rust: Weak<T> breaks reference cycles
use std::rc::Weak;
let weak: Weak<_> = Rc::downgrade(&a);
// C++: std::shared_ptr / std::weak_ptr
auto sp = std::make_shared<int>(42);
auto sp2 = sp; // ref count = 2
std::weak_ptr<int> wp = sp; // does not increment count
CPython uses reference counting as its primary memory management strategy, supplemented
by a cycle collector for objects that form cycles. Swift also uses automatic reference
counting (ARC) compiled into the binary.
8.5 Manual Memory Management
C and pre-RAII C++ require explicit malloc/free or
new/delete calls. The programmer is responsible for freeing
every allocation exactly once. Failure modes are use-after-free, double-free, and
memory leaks.
RAII (Resource Acquisition Is Initialization) is the C++ idiom that
ties resource lifetime to object lifetime. A destructor releases the resource when
the object goes out of scope, making manual management manageable. Rust’s
Drop trait is the direct equivalent; it is invoked automatically by the
ownership system.
// C: manual
int* p = (int*)malloc(sizeof(int));
*p = 42;
free(p); // must not forget; must not double-free
// C++: RAII via unique_ptr eliminates manual delete
auto p = std::make_unique<int>(42);
// p freed automatically when it goes out of scope
8.6 Epilogue
Memory management strategy shapes a language’s safety guarantees, performance
profile, and ergonomics. Rust’s ownership model achieves manual-level performance
with GC-level safety. The next chapter covers how code is organized into modules and
packages.
8.7 References
Rust Ownership - The Book
C++ Memory Management - cppreference
.NET Garbage Collection - Microsoft
CPython Garbage Collector - Dev Guide