How to use raw pointers

Declare raw pointers as *const T or *mut T and dereference them inside an unsafe block to access memory directly.

When references hit a wall

You are building a graph data structure. Nodes need to point to their neighbors. Some neighbors point back. You try using references, but the borrow checker rejects the code. It sees a cycle and refuses to compile. Or you are writing a memory allocator. You need to track chunks of memory by their addresses, and references carry lifetime metadata that gets in the way. Or you are calling a C library that expects a pointer to a buffer. References won't cross the FFI boundary.

Rust's ownership model prevents cycles and guarantees safety, but it cannot represent every valid memory layout. Raw pointers exist to fill that gap. They let you work with memory addresses directly, bypassing the borrow checker. They also remove all safety guarantees. The compiler stops protecting you. You become responsible for every byte.

What a raw pointer actually is

Raw pointers come in two flavors: *const T and *mut T. They are just memory addresses. The compiler treats them as integers that point to a location in memory. They carry no lifetime information. They carry no ownership. They do not check for null. They do not check bounds.

Think of a reference as a tracked rental. The system knows who has the item, when the rental expires, and whether anyone else has it. A raw pointer is a GPS coordinate written on a napkin. The coordinate is just a number. The map does not tell you if there is a building at that location. The building might have been demolished. The building might be on fire. Someone else might be inside. The napkin does not care. You just have the address.

*const T indicates a pointer to constant data. You can read through it. Multiple *const T pointers can point to the same location. *mut T indicates a pointer to mutable data. You can read and write through it. The rules for *mut T are stricter. You must guarantee exclusive access. If you have a *mut T, no other pointer or reference can access that memory.

Creating and dereferencing pointers

You create a raw pointer by casting a reference using as. You dereference a raw pointer using the * operator, but only inside an unsafe block. The compiler enforces the unsafe boundary to mark code where you are taking responsibility for safety.

/// Demonstrates creating and dereferencing raw pointers.
fn main() {
    let val = 42;
    
    // Cast a shared reference to a raw const pointer.
    // The pointer holds the address of val.
    let ptr_const = &val as *const i32;
    
    // Cast a mutable reference to a raw mut pointer.
    let mut num = 10;
    let ptr_mut = &mut num as *mut i32;
    
    // Dereferencing requires unsafe because the compiler cannot verify safety.
    // E0133 triggers if you try to dereference outside unsafe.
    unsafe {
        println!("Const: {}", *ptr_const);
        println!("Mut: {}", *ptr_mut);
        
        // Writing through a *mut pointer is allowed in unsafe.
        *ptr_mut = 20;
    }
    
    println!("After write: {}", num);
}

The as cast converts the reference to a pointer. The reference still exists. The cast does not invalidate the reference. You must ensure the reference is no longer used after you start mutating through the raw pointer. If you keep the reference alive and write through the pointer, you create undefined behavior. The compiler may optimize based on the assumption that the shared reference is immutable.

The compiler rejects dereferencing a raw pointer outside an unsafe block with E0133 (dereference of raw pointer requires unsafe). This error is a hard gate. It forces you to acknowledge that you are entering a zone where the compiler cannot verify memory safety.

Treat the unsafe block as a quarantine zone. Keep it small. Isolate the pointer operations. The rest of your code remains safe.

The aliasing contract

The difference between *const and *mut is not just about mutability. It is about aliasing. This is the rule that trips up most developers.

*const T allows aliasing. You can have multiple *const T pointers to the same memory. You can read through all of them. This matches the behavior of shared references.

*mut T requires exclusive access. If you have a *mut T, you must guarantee that no other pointer or reference accesses that memory. This includes other *mut T pointers, *const T pointers, and references. If you violate this rule, you trigger undefined behavior. The compiler assumes exclusive access for *mut T and may reorder memory operations or cache values based on that assumption.

Consider this scenario. You have a &T reference. You cast it to *mut T. You write through the *mut T. The original &T reference is still in scope. This is undefined behavior. The compiler generated code that assumes the &T points to immutable data. Writing through the *mut T breaks that assumption. The optimizer might have loaded the value once and never reloaded it. Your write goes unnoticed. The program behaves incorrectly without crashing.

You can cast a &T to *mut T, but you must ensure the &T is dead before you dereference the *mut T. The cast itself is safe. The usage is not.

Trust the aliasing rules. They are the only thing standing between your code and memory corruption. If you cannot prove exclusive access, you cannot use *mut T.

Moving data without dropping

Dereferencing a raw pointer with *ptr behaves like a reference. It reads the value. If the value is moved out, it drops the old value. This causes problems when you are moving data without dropping, such as in a custom allocator or a swap function.

Rust provides std::ptr::read and std::ptr::write for these cases. ptr::read copies the bits from the pointer without dropping the source. ptr::write writes bits to the pointer without dropping the existing value.

use std::ptr;

/// Moves a value out of a raw pointer without dropping the source.
/// Returns the value and leaves the memory uninitialized.
fn take_value(ptr: *const i32) -> i32 {
    unsafe {
        // ptr::read copies the value but does not drop it.
        // The memory at ptr is now uninitialized.
        ptr::read(ptr)
    }
}

/// Writes a value to a raw pointer without dropping the existing value.
/// Useful for overwriting memory that should not be dropped.
fn overwrite_value(ptr: *mut i32, val: i32) {
    unsafe {
        // ptr::write places val at ptr without dropping what was there.
        ptr::write(ptr, val);
    }
}

Using *ptr = val drops the old value before writing. If the old value owns resources, those resources get freed. If you are reusing a buffer, you do not want to drop the old value. You want to overwrite it. ptr::write does exactly that.

Using *ptr to move a value drops the source. If you move the same memory twice, you get a double drop. ptr::read avoids the drop. You must ensure the memory is not dropped later. The caller takes responsibility for the value.

Use ptr::read when you need to move data without triggering a drop. Use *ptr when you want to read and drop. Confusing the two leads to use-after-free or memory leaks.

Convention: NonNull and Box

The community follows specific conventions when working with raw pointers. These conventions improve safety and performance.

Use std::ptr::NonNull<T> instead of *mut T inside structs. NonNull<T> is a wrapper that guarantees the pointer is not null. On some platforms, it is smaller than *mut T because the null value is used as a tag. It also signals intent. Readers know the pointer is valid. You can convert NonNull<T> to *mut T using as_ptr().

use std::ptr::NonNull;

/// A node in a linked list using NonNull for the next pointer.
struct Node {
    data: i32,
    // NonNull documents that next is never null.
    next: Option<NonNull<Node>>,
}

Use Box::into_raw to create raw pointers from Box. Do not cast &*box to *mut T. Box::into_raw consumes the box and returns a pointer without dropping the value. It transfers ownership to the pointer. Casting a reference creates a pointer while the box still exists. If the box drops, the pointer dangles.

/// Creates a node and returns a raw pointer.
/// The caller owns the allocation and must free it.
fn create_node(data: i32) -> *mut Node {
    let node = Box::new(Node { data, next: None });
    // Box::into_raw leaks the box and returns a pointer.
    // No drop occurs. The pointer is valid.
    Box::into_raw(node)
}

Convention aside: Rc::clone(&data) is preferred over data.clone() for reference counting types. The explicit form makes it clear that you are cloning the reference, not the data. For raw pointers, there is no clone. You copy the pointer value. The copy is shallow. Both pointers point to the same memory.

Follow these conventions. They make your code predictable and compatible with the ecosystem.

Pitfalls that cause undefined behavior

Raw pointers introduce undefined behavior. Undefined behavior means the compiler can do anything. The program might crash. It might corrupt memory. It might appear to work and fail later. Here are the common pitfalls.

Dangling pointers. You create a pointer to a value. The value drops. The pointer still holds the address. Dereferencing the pointer accesses freed memory. The compiler does not track lifetimes for raw pointers. You must manage the lifetime manually.

Double free. You have two raw pointers to the same allocation. You free both. The allocator corrupts its internal state. Subsequent allocations fail or overwrite random memory. Ensure each allocation is freed exactly once.

Aliasing violations. You have a *mut T and a &T to the same memory. You write through the *mut T. The compiler optimized based on the &T. The write is lost. The program behaves incorrectly. Ensure exclusive access for *mut T.

Null dereference. You dereference a null pointer. The program crashes. Raw pointers can be null. Check for null before dereferencing, or use NonNull<T> to enforce non-nullability.

Out of bounds. You create a pointer and offset it past the allocation. You dereference the offset pointer. You access memory you do not own. The allocator detects the violation and aborts, or you corrupt another object. Calculate offsets carefully.

Treat the SAFETY comment as a proof. If you cannot write down the invariants that prevent these pitfalls, you do not have a safe wrapper.

Decision matrix

Use raw pointers for FFI when you call C code or interact with hardware registers that expect memory addresses. Use raw pointers when implementing a safe abstraction like a linked list, a tree, or a custom allocator where the data structure itself manages memory layout. Use raw pointers for performance-critical inner loops only when profiling proves that safe abstractions add measurable overhead and you can isolate the unsafe code in a small helper. Reach for references when lifetimes are simple and the borrow checker accepts your code. Raw pointers introduce undefined behavior risks that rarely justify the convenience.

Do not use raw pointers to bypass the borrow checker. The borrow checker exists to prevent bugs. Bypassing it moves the burden to you. If the borrow checker rejects your code, refactor the design. Use RefCell for interior mutability. Use Rc for shared ownership. Use raw pointers only when the safe tools cannot express the memory layout you need.

Counter-intuitive but true: the more you use raw pointers, the harder the rest of your code becomes to reason about. Isolate unsafe code. Build a safe API around it. Expose only the safe interface to the rest of the program.

Where to go next