Unsafe Rust

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.