Dynamic Memory: malloc/free vs Box/Vec
Stack memory is fast but limited in size and lifetime. When you need memory that outlives a function call or whose size is not known at compile time, you allocate it on the heap. C gives you raw tools; Rust gives you safe abstractions over the same tools.
malloc and free
malloc requests bytes from the heap. free returns them. Everything between
is your responsibility.
/* malloc_basic.c */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(sizeof(int));
if (p == NULL) {
perror("malloc");
return 1;
}
*p = 42;
printf("*p = %d\n", *p);
free(p);
p = NULL; /* good practice: prevent use-after-free */
return 0;
}
Before malloc: After malloc:
Stack Stack Heap
+---+------+ +---+--------+ +----+
| p | NULL | | p | 0x5000-|->| 42 |
+---+------+ +---+--------+ +----+
After free:
Stack Heap
+---+------+ +----+
| p | NULL | | ?? | <-- memory returned to allocator
+---+------+ +----+
calloc and realloc
calloc allocates and zero-initializes. realloc resizes an existing
allocation.
/* calloc_realloc.c */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
/* calloc: allocate 5 ints, all zeroed */
int *arr = calloc(5, sizeof(int));
if (arr == NULL) {
perror("calloc");
return 1;
}
for (int i = 0; i < 5; i++) {
arr[i] = (i + 1) * 10;
}
/* realloc: grow to 10 ints */
int *tmp = realloc(arr, 10 * sizeof(int));
if (tmp == NULL) {
perror("realloc");
free(arr);
return 1;
}
arr = tmp;
/* New elements are uninitialized (not zeroed!) */
for (int i = 5; i < 10; i++) {
arr[i] = (i + 1) * 10;
}
for (int i = 0; i < 10; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
free(arr);
return 0;
}
Caution: Never do
arr = realloc(arr, new_size). Ifreallocfails, it returns NULL and you lose the original pointer -- a memory leak. Always use a temporary variable.
Memory Leaks
A memory leak occurs when allocated memory is never freed. The process holds onto memory it can never use again.
/* leak.c -- DO NOT DO THIS */
#include <stdio.h>
#include <stdlib.h>
void leaky(void)
{
int *p = malloc(1024);
if (p == NULL) return;
*p = 42;
/* forgot to free(p) -- leaked 1024 bytes */
}
int main(void)
{
for (int i = 0; i < 1000; i++) {
leaky(); /* leaks 1 MB total */
}
printf("Done (but leaked ~1 MB)\n");
return 0;
}
Run with Valgrind to detect:
$ gcc -g -o leak leak.c
$ valgrind --leak-check=full ./leak
...
==12345== LEAK SUMMARY:
==12345== definitely lost: 1,024,000 bytes in 1,000 blocks
Double-Free
Freeing the same pointer twice is undefined behavior. It can corrupt the allocator's internal data structures, leading to crashes or exploits.
/* double_free.c -- DO NOT DO THIS */
#include <stdlib.h>
int main(void)
{
int *p = malloc(sizeof(int));
*p = 42;
free(p);
/* free(p); */ /* UNDEFINED BEHAVIOR: double-free */
p = NULL; /* Setting to NULL after free prevents double-free */
free(p); /* free(NULL) is safe -- it does nothing */
return 0;
}
Caution: Double-free bugs are a major source of security vulnerabilities. Attackers can exploit heap corruption caused by double-free to execute arbitrary code.
Rust's Box<T>
Box<T> allocates a single value on the heap. When the Box goes out of
scope, the memory is freed automatically.
// box_demo.rs fn main() { let b = Box::new(42); println!("*b = {}", *b); // b goes out of scope here -> memory freed automatically // No free() needed. No leak possible. No double-free possible. }
Stack Heap
+---+---------+ +----+
| b | 0x5000 -|------>| 42 |
+---+---------+ +----+
When b drops:
- Heap memory at 0x5000 is freed
- b is gone from the stack
Rust's Vec<T>
Vec<T> is a growable heap array. It replaces C's malloc/realloc/free pattern.
// vec_grow.rs fn main() { let mut v: Vec<i32> = Vec::new(); println!("len={}, cap={}", v.len(), v.capacity()); for i in 0..10 { v.push(i * 10); println!("pushed {}: len={}, cap={}", i * 10, v.len(), v.capacity()); } println!("\ncontents: {:?}", v); }
$ rustc vec_grow.rs && ./vec_grow
len=0, cap=0
pushed 0: len=1, cap=4
pushed 10: len=2, cap=4
pushed 20: len=3, cap=4
pushed 30: len=4, cap=4
pushed 40: len=5, cap=8
pushed 50: len=6, cap=8
pushed 60: len=7, cap=8
pushed 70: len=8, cap=8
pushed 80: len=9, cap=16
pushed 90: len=10, cap=16
contents: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
Vec doubles its capacity when full (the exact growth factor is an
implementation detail). Compare this to manually calling realloc in C.
RAII and the Drop Trait
RAII (Resource Acquisition Is Initialization) means: tie resource cleanup to
scope exit. In Rust, every type can implement the Drop trait to run cleanup
code when a value goes out of scope.
// drop_demo.rs struct Resource { name: String, } impl Resource { fn new(name: &str) -> Self { println!("[{}] acquired", name); Resource { name: String::from(name), } } } impl Drop for Resource { fn drop(&mut self) { println!("[{}] released", self.name); } } fn main() { let _a = Resource::new("A"); { let _b = Resource::new("B"); let _c = Resource::new("C"); println!("-- end of inner scope --"); } // B and C dropped here (reverse order) println!("-- end of main --"); } // A dropped here
$ rustc drop_demo.rs && ./drop_demo
[A] acquired
[B] acquired
[C] acquired
-- end of inner scope --
[C] released
[B] released
-- end of main --
[A] released
Drop order is reverse of creation order, just like C++ destructors.
Driver Prep: The Rust-for-Linux project uses RAII extensively. When a device driver struct is dropped, it automatically unregisters the device, frees DMA buffers, and releases IRQs. This eliminates an entire class of kernel resource leaks.
C Equivalent of RAII: goto cleanup
C does not have destructors. The standard pattern is goto cleanup:
/* goto_cleanup.c */
#include <stdio.h>
#include <stdlib.h>
int process_data(int n)
{
int ret = -1;
int *buf1 = malloc(n * sizeof(int));
if (buf1 == NULL) goto out;
int *buf2 = malloc(n * sizeof(int));
if (buf2 == NULL) goto free_buf1;
/* Do work with buf1 and buf2 */
for (int i = 0; i < n; i++) {
buf1[i] = i;
buf2[i] = i * 2;
}
printf("Processed %d elements\n", n);
ret = 0;
free(buf2);
free_buf1:
free(buf1);
out:
return ret;
}
int main(void)
{
if (process_data(10) != 0) {
fprintf(stderr, "processing failed\n");
return 1;
}
return 0;
}
Driver Prep: This
goto cleanuppattern is the single most common pattern in Linux kernel code. Everyprobe()function looks like this. Rust's RAII replaces it entirely.
Detecting Memory Bugs
Valgrind
$ gcc -g -o program program.c
$ valgrind --leak-check=full --show-leak-kinds=all ./program
Valgrind instruments every memory access at runtime. It catches:
- Memory leaks
- Use-after-free
- Double-free
- Buffer overflows (heap)
- Uninitialized reads
AddressSanitizer (ASan)
$ gcc -g -fsanitize=address -o program program.c
$ ./program
ASan is faster than Valgrind (2x slowdown vs 20x). It catches the same bugs plus stack buffer overflows. Both GCC and Clang support it.
/* asan_demo.c -- compile with -fsanitize=address */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(5 * sizeof(int));
p[5] = 99; /* heap-buffer-overflow */
free(p);
return 0;
}
$ gcc -g -fsanitize=address -o asan_demo asan_demo.c && ./asan_demo
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 4 at 0x... thread T0
#0 0x... in main asan_demo.c:8
Try It: Compile the leak example above with
-fsanitize=addressand compare the output to Valgrind. ASan's reports are often more readable.
Side-by-Side: Linked List
This is the definitive comparison. A singly-linked list shows every difference between C and Rust memory management.
C Linked List
/* linked_list_c.c */
#include <stdio.h>
#include <stdlib.h>
struct node {
int value;
struct node *next;
};
struct node *list_push(struct node *head, int value)
{
struct node *n = malloc(sizeof(struct node));
if (n == NULL) {
perror("malloc");
exit(1);
}
n->value = value;
n->next = head;
return n;
}
void list_print(const struct node *head)
{
const struct node *cur = head;
while (cur != NULL) {
printf("%d -> ", cur->value);
cur = cur->next;
}
printf("NULL\n");
}
void list_free(struct node *head)
{
struct node *cur = head;
while (cur != NULL) {
struct node *next = cur->next;
free(cur);
cur = next;
}
}
int main(void)
{
struct node *list = NULL;
list = list_push(list, 10);
list = list_push(list, 20);
list = list_push(list, 30);
list_print(list); /* 30 -> 20 -> 10 -> NULL */
list_free(list);
return 0;
}
Three things that can go wrong in the C version:
- Forget
list_free-- memory leak - Use
listafterlist_free-- use-after-free - Free a node twice -- double-free
Rust Linked List
// linked_list_rust.rs enum List { Cons(i32, Box<List>), Nil, } use List::{Cons, Nil}; impl List { fn push(self, value: i32) -> List { Cons(value, Box::new(self)) } fn print(&self) { let mut current = self; loop { match current { Cons(val, next) => { print!("{} -> ", val); current = next; } Nil => { println!("Nil"); break; } } } } } fn main() { let list = Nil .push(10) .push(20) .push(30); list.print(); // 30 -> 20 -> 10 -> Nil // list goes out of scope here. // Each Box is dropped recursively. No leak. No double-free. }
$ rustc linked_list_rust.rs && ./linked_list_rust
30 -> 20 -> 10 -> Nil
Memory layout:
C version:
head -> [30|*]----> [20|*]----> [10|NULL]
malloc'd malloc'd malloc'd
(must free (must free (must free
manually) manually) manually)
Rust version:
list = Cons(30, Box::new(
Cons(20, Box::new(
Cons(10, Box::new(Nil))))))
Stack Heap Heap Heap
+------+ +------+ +------+ +------+
| list |------>| 30 | | 20 | | 10 |
+------+ | Box -|------>| Box -|------>| Nil |
+------+ +------+ +------+
(auto-drop) (auto-drop) (auto-drop)
No list_free function needed. When list goes out of scope, each Box drops
its contents, which triggers the next drop, recursively freeing the entire
chain.
Rust Note: For long lists, recursive drop can overflow the stack. In production, you would implement
Dropmanually with an iterative loop. The standard library'sLinkedList<T>handles this.
Comparing Memory Management Styles
+-------------------+-------------------------+------------------------+
| Operation | C | Rust |
+-------------------+-------------------------+------------------------+
| Allocate one | malloc(sizeof(T)) | Box::new(val) |
| Allocate array | malloc(n * sizeof(T)) | Vec::with_capacity(n) |
| Zero-allocate | calloc(n, sizeof(T)) | vec![0; n] |
| Resize | realloc(p, new_size) | v.reserve(additional) |
| Free | free(p) | automatic (Drop) |
| Detect leaks | valgrind, ASan | not needed* |
| Detect use-after | valgrind, ASan | compile error |
| Detect double-free| valgrind, ASan | compile error |
+-------------------+-------------------------+------------------------+
* Rust can still leak via mem::forget or Rc cycles, but accidental
leaks from forgetting free() are impossible.
std::mem::drop and Early Cleanup
Sometimes you want to free memory before a scope ends. Rust's drop() function
consumes a value, triggering its destructor.
// early_drop.rs fn main() { let data = vec![1, 2, 3, 4, 5]; println!("data = {:?}", data); drop(data); // free heap memory now // println!("{:?}", data); // ERROR: use of moved value println!("data has been freed"); }
This is safe because drop takes ownership (moves the value). After the move,
the compiler prevents any further access.
Try It: Create a struct that holds a large
Vec<u8>(say, 10 MB). Print its size, thendropit, then allocate another. Observe that you cannot accidentally use the first after dropping.
Knowledge Check
- What happens if
reallocfails and you wrotep = realloc(p, new_size)? - What is the Rust equivalent of C's
goto cleanuppattern? - Why can Rust guarantee no double-free at compile time?
Common Pitfalls
- Forgetting to check malloc's return -- it can return NULL on allocation failure.
- Using
reallocincorrectly -- always assign to a temporary first. - Mixing allocators -- do not
free()memory from a custom allocator, or vice versa. - Forgetting to free in every code path -- C's
goto cleanupexists because error paths leak memory. - Leaking
Rccycles in Rust --Rc<T>does not use a garbage collector. Cycles leak. UseWeak<T>to break them. - Calling
mem::forgetcasually -- it preventsDropfrom running. Use it only when you know what you are doing. - Heap fragmentation -- many small allocations can fragment memory. Use pool allocators or arena allocation for high-frequency allocation patterns.