C dynamic allocation usually starts with:
void *p = malloc(size);
p = realloc(p, new_size);
free(p);
Ordinary Rust code rarely calls the allocator directly. It usually uses owning types:
Box<T> -> one T on the heap
Vec<T> -> growable contiguous array on the heap
String -> growable UTF-8 text on the heap
These types allocate and release memory internally, but callers usually do not write free. When the value leaves scope, Drop releases the owned allocation.
Box Is One Heap Object
C allocation for one struct:
struct Config *cfg = malloc(sizeof *cfg);
if (!cfg) {
return -1;
}
cfg->port = 1883;
cfg->retry = 3;
free(cfg);
In Rust, if a value really needs to live on the heap, use Box<T>:
struct Config {
port: u16,
retry: u8,
}
let cfg = Box::new(Config {
port: 1883,
retry: 3,
});
Box<Config> means:
owns one Config on the heap
When cfg leaves scope, the Config and its heap allocation are released.
Not Every Value Needs Box
C developers often connect “create an object” with malloc. In Rust, many values can just be local values:
let cfg = Config {
port: 1883,
retry: 3,
};
Common reasons to use Box<T> include:
- the value is large and you do not want it on the stack
- the type size is hard to express directly, such as recursive data
- a stable heap address is needed
- trait objects, such as
Box<dyn Writer> - ownership should be passed as one heap object
A recursive structure:
enum List {
Cons(i32, Box<List>),
Nil,
}
Without Box, List would contain another full List, expanding forever. Box<List> puts the next node on the heap, so the current enum stores one owning pointer.
Vec Is a Growable Array
C code often maintains a dynamic array manually:
uint8_t *buf = malloc(capacity);
size_t len = 0;
if (len == capacity) {
capacity *= 2;
buf = realloc(buf, capacity);
}
buf[len++] = byte;
Rust usually uses Vec<T>:
let mut buf = Vec::new();
buf.push(0x10);
buf.push(0x20);
Read Vec<T> as:
ptr + len + capacity
Inspect length and capacity:
println!("len = {}", buf.len());
println!("capacity = {}", buf.capacity());
Reserve capacity:
let mut buf = Vec::with_capacity(1024);
If capacity is not enough, Vec reallocates and moves the data. That is close to C realloc.
Reallocation Invalidates Old Pointers
Using an old pointer after realloc is a classic C bug:
uint8_t *old = buf;
buf = realloc(buf, new_capacity);
old[0] = 1; // may be dangling
Safe Rust makes this relationship hard to write accidentally. But if you take a raw pointer from a Vec and then grow the Vec, you are responsible:
let mut buf = Vec::with_capacity(1);
buf.push(1);
let ptr = buf.as_ptr();
buf.push(2); // may reallocate
// ptr may now be invalid; do not dereference it
Do not expose a Vec’s internal pointer as a long-lived API detail. This matters especially for FFI: if C stores the pointer, Rust must not casually reallocate the Vec.
Vec and &[T] Are API Boundaries
If a function only reads data, it should not take Vec<T>:
fn checksum(data: &[u8]) -> u32 {
data.iter().map(|b| *b as u32).sum()
}
If a function creates and returns dynamic data, return Vec<T>:
fn make_packet(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len() + 2);
out.push(0xAA);
out.extend_from_slice(payload);
out.push(0x55);
out
}
The important split:
&[T] -> borrows existing data
Vec<T> -> owns growable data
Do not use Vec<T> everywhere just because the underlying representation includes a pointer and length.
String Is the Text Version of Vec
C dynamic strings often look like:
char *s = malloc(capacity);
strcpy(s, "device");
free(s);
Rust String is owned, growable UTF-8 text:
let mut name = String::from("device");
name.push_str("-01");
Compare it with Vec<u8>:
Vec<u8> -> owns growable bytes
String -> owns growable UTF-8 text
If a function only reads text, take &str:
fn print_name(name: &str) {
println!("{name}");
}
If it creates and returns text, return String:
fn make_topic(device: &str) -> String {
format!("devices/{device}/status")
}
String is not C char *. It is not guaranteed to be NUL-terminated, and its bytes must be valid UTF-8. C string boundaries still use CString and CStr.
Containers Release by Ownership
C APIs often rely on matching allocation and release functions:
uint8_t *make_packet(size_t *out_len);
void free_packet(uint8_t *p);
Rust returns an owning type:
fn make_packet() -> Vec<u8> {
vec![0xAA, 0x01, 0x55]
}
The caller receives the Vec<u8> and therefore receives responsibility for the allocation. But the responsibility is not a manual free; the value releases memory when it leaves scope.
If a function takes Vec<u8>:
fn consume_packet(packet: Vec<u8>) {
println!("len = {}", packet.len());
}
The caller transfers ownership to the function. When the function ends, packet is released.
If the function only borrows:
fn inspect_packet(packet: &[u8]) {
println!("len = {}", packet.len());
}
The caller still owns the original Vec.
When clone Is the Copy
C copying dynamic memory often means malloc + memcpy:
uint8_t *copy = malloc(len);
memcpy(copy, src, len);
Rust clones owned data:
let a = vec![1_u8, 2, 3];
let b = a.clone();
For Vec and String, clone() copies the underlying data and allocates new memory. It is not a cheap pointer copy.
If the function only reads, do not use clone just to avoid ownership errors:
fn print_len(data: &[u8]) {
println!("{}", data.len());
}
let data = vec![1_u8, 2, 3];
print_len(&data);
If borrowing solves the problem, do not hide it with a copy.
What to Keep
The first mapping from C dynamic allocation to Rust:
malloc one object -> Box<T>, but not every object needs Box
malloc array / realloc -> Vec<T>
dynamic C string -> String, if it is UTF-8 text
pointer + len read -> &[T]
pointer + len write -> &mut [T]
malloc + memcpy copy -> clone, but it copies data
free -> owning value drops when it leaves scope
Rust does not remove heap allocation. It puts heap allocation behind owning types. The point of Box, Vec, and String is not “a nicer malloc”; it is making ownership, growth, and release part of the type.