25.3 Raw Pointers: *const T and *mut T

Analogous to C pointers, Rust provides two raw pointer types:

  • *const T: A raw pointer to data of type T, indicating the pointer itself does not grant permission to mutate the data through it. Roughly corresponds to C’s const T*.
  • *mut T: A raw pointer to data of type T, indicating the pointer may be used to mutate the data. Roughly corresponds to C’s T*.

The const or mut primarily signifies the intended use and type system interaction, not necessarily the absolute immutability of the underlying memory (e.g., memory behind a *const T might still be mutated through other means, like an UnsafeCell or another *mut T, if done carefully).

Raw pointers differ significantly from Rust’s references (&T, &mut T):

  • They can be null.
  • They are not guaranteed to point to valid memory (could be dangling or uninitialized).
  • They do not have compiler-enforced lifetime constraints.
  • They can alias (e.g., multiple *mut T can point to the same location), but using them must still respect Rust’s aliasing rules to avoid UB (discussed below).
  • They require explicit dereferencing using the * operator, which is an unsafe operation.
  • They do not implement automatic dereferencing.

25.3.1 Creating and Using Raw Pointers

Creating raw pointers is safe. This is typically done by casting references or memory addresses (represented as integers). Passing, storing, or comparing raw pointers is also safe.

fn main() {
    let mut data = 10;

    // Safe: Create raw pointers from references.
    let p_const: *const i32 = &data;
    let p_mut: *mut i32 = &mut data;

    // Safe: Create a raw pointer from an address (integer). Caution: validity unknown.
    let address = 0x1234_5678_usize;
    let p_addr: *const i32 = address as *const i32;

    println!("Address from const reference: {:p}", p_const);
    println!("Address from mut reference:   {:p}", p_mut);
    println!("Address from integer literal: {:p}", p_addr);

    // Safe: Create and store a null pointer.
    let null_ptr: *const i32 = std::ptr::null();
    println!("Null pointer address:       {:p}", null_ptr);
}

Dereferencing a raw pointer (*p) to access the pointed-to data is unsafe, requiring an unsafe block, because the pointer’s validity cannot be guaranteed by the compiler.

fn main() {
    let mut num = 5;
    let p_const = &num as *const i32;
    let p_mut = &mut num as *mut i32;

    // Unsafe: Dereferencing requires an unsafe block.
    unsafe {
        println!("Reading via *const T: {}", *p_const);

        // Writing requires a *mut T.
        *p_mut = 10;
        println!("Reading via *mut T after write: {}", *p_mut);
    }
    println!("Final value of num: {}", num); // num is now 10

    // Example: Dereferencing an arbitrary address is highly likely UB.
    let invalid_addr = 0x1 as *const i32;
    // unsafe { println!("{}", *invalid_addr); } // Likely crash or incorrect behavior!
}

Important Note for C/C++ Programmers: Although raw pointers seem to bypass Rust’s borrowing rules (e.g., allowing multiple *mut T to the same data), Rust still imposes strict aliasing rules, even within unsafe code. The exact rules are formalized by models like Stacked Borrows or Tree Borrows (these models are still evolving). Violating these rules—for instance, writing through a *mut T while a shared reference &T to the same location exists and is considered “live”—is undefined behavior. This is stricter than C’s aliasing rules in some respects. Tools like Miri are invaluable for detecting such violations.

25.3.2 Pointer Arithmetic

Raw pointers support arithmetic via methods like offset(count), add(count), and sub(count). These operations adjust the pointer address by count * size_of::<T>() bytes, similar to C pointer arithmetic. Performing pointer arithmetic itself is unsafe because it can easily yield pointers outside allocated memory regions or cause misaligned access.

fn main() {
    let numbers = [10i32, 20, 30, 40, 50];
    let start_ptr: *const i32 = numbers.as_ptr(); // Pointer to the first element.

    unsafe {
        // Move pointer to the third element (index 2).
        // offset is generally preferred over add for forward/backward movement.
        let third_elem_ptr = start_ptr.offset(2);
        println!("Third element: {}", *third_elem_ptr); // Outputs 30

        // Using add: move pointer to the second element (index 1).
        let second_elem_ptr = start_ptr.add(1);
        println!("Second element: {}", *second_elem_ptr); // Outputs 20

        // Calculating the difference between pointers.
        let diff = third_elem_ptr.offset_from(start_ptr);
        println!("Offset difference: {}", diff); // Outputs 2

        // Creating a pointer outside the bounds is possible but dereferencing it is UB.
        // let invalid_ptr = start_ptr.offset(10);
        // println!("{}", *invalid_ptr); // Undefined Behavior!
    }
}

Pointer arithmetic should be used with extreme caution. Ensure that the resulting pointer remains within the bounds of a single valid memory allocation. Safer alternatives, like slice indexing (numbers[i]) or iterators, should always be preferred when applicable. The wrapping_offset, wrapping_add, and wrapping_sub methods perform arithmetic that wraps on overflow; these operations themselves are safe (as they don’t dereference), but using the resulting pointer might still be unsafe.

25.3.3 Fat Pointers

Raw pointers to Dynamically Sized Types (DSTs), such as slices ([T]) or trait objects (dyn Trait), are “fat pointers.” They consist of two components: the pointer to the data and associated metadata.

  • *const [T], *mut [T]: Contain the address of the first element and the number of elements (length).
  • *const dyn Trait, *mut dyn Trait: Contain the address of the object data and the address of its virtual method table (vtable).

Converting between thin pointers (*const T) and fat pointers usually requires specific functions like std::slice::from_raw_parts or std::slice::from_raw_parts_mut, which are often unsafe.