Raw Pointers
Raw pointers—written *const T (immutable raw pointer) and *mut T (mutable raw pointer)—are unchecked memory addresses that lack the compile-time safety guarantees of Rust references (&T and &mut T). Note that in *const T, the token const is part of the type name (there is no *T type in Rust), indicating that the pointee cannot be mutated directly through that pointer without casting it to *mut T.
Comparing References (&T / &mut T) and Raw Pointers (*const T / *mut T)
| Property | References (&'a T, &'a mut T) |
Raw Pointers (*const T, *mut T) |
|---|---|---|
| Nullability | Never null (niche-optimized in Option<&T>) |
Can be null (std::ptr::null(), std::ptr::null_mut()) |
| Alignment | Guaranteed aligned to align_of::<T>() |
Can hold unaligned addresses |
| Pointee validity | Guaranteed to point to a live, initialized T for 'a |
Can dangle or point to uninitialized / freed memory |
| Aliasing rules | Strictly enforced (many &T XOR one &mut T) |
Multiple *mut T and *const T can alias the same address |
| Lifetime parameter | Carries compile-time lifetime 'a |
No lifetime parameter ('static type regardless of pointee lifetime) |
| Copy semantics | &T is Copy; &mut T is not Copy (move-only) |
Both *const T and *mut T implement Copy and Clone |
Thread safety (Send / Sync) |
Send/Sync based on T |
Neither Send nor Sync by default (prevents accidental cross-thread aliasing) |
| Automatic dereferencing | Auto-derefs on . method calls and Deref |
Does not auto-deref to T; requires unsafe { (*ptr).field } |
Variance and NonNull<T>
While *const T is covariant over T (like &T) and *mut T is invariant over T (like &mut T), standard library data structures (Box<T>, Vec<T>, Rc<T>, LinkedList<T>) frequently use std::ptr::NonNull<T> instead of *mut T:
*mut T (8 bytes) NonNull<T> (8 bytes)
+----------------------------------+ +----------------------------------+
| Any 64-bit address (including 0) | | Guaranteed != 0 (niche = 0) |
| Invariant over T | | Covariant over T |
| Option<*mut T> = 16 bytes | | Option<NonNull<T>> = 8 bytes |
+----------------------------------+ +----------------------------------+
use std::ptr::{self, NonNull};
fn main() {
let raw_const: *const u64 = ptr::null();
let raw_mut: *mut u64 = ptr::null_mut();
println!("raw_const.is_null(): {}", raw_const.is_null());
println!("raw_mut.is_null(): {}", raw_mut.is_null());
println!("size of Option<*mut u64>: {} bytes", size_of::<Option<*mut u64>>());
println!("size of Option<NonNull<u64>>: {} bytes", size_of::<Option<NonNull<u64>>>());
let mut x: u64 = 99;
let non_null = NonNull::from(&mut x);
println!("non_null address: {:p}", non_null);
}
raw_const.is_null(): true
raw_mut.is_null(): true
size of Option<*mut u64>: 16 bytes
size of Option<NonNull<u64>>: 8 bytes
non_null address: 0x16fdfee88
Pointer Type Casting (ptr.cast::<U>())
Instead of using ptr as *const U (which can accidentally cast between pointer types and integers or change mutability simultaneously), raw pointers provide .cast::<U>() (*const T -> *const U and *mut T -> *mut U) and .cast_mut() / .cast_const() for explicit, self-documenting conversions.