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; the unsafety primarily lies in dereferencing them. Raw pointers can be obtained in several ways:

  • From references: References can be explicitly cast to raw pointers. For a variable x of type T, &x as *const T creates a const pointer, and &mut x as *mut T creates a mutable pointer. This explicit cast is a common way to convert a reference when its lifetime and validity are known but a raw pointer is needed (e.g., for FFI or certain low-level operations).
  • From data structures: Many standard library types that manage contiguous data, such as slices ([T]), Vec<T>, and String, provide methods like as_ptr() (to get a *const T) and as_mut_ptr() (to get a *mut T from a mutable instance). For example, my_slice.as_ptr() returns a *const T to the beginning of the slice’s data. These methods are the idiomatic way to obtain pointers to the internal buffer of such types, as shown in the pointer arithmetic examples later.
  • From memory addresses: An integer representing a memory address can be cast to a raw pointer (e.g., address_usize as *const T). This is highly platform-dependent and typically used for memory-mapped I/O or interacting with hardware. The programmer must ensure the address is valid, properly aligned, and points to memory of type T.
  • Null pointers: The std::ptr module provides null() to create a *const T null pointer and null_mut() for a *mut T null pointer (e.g., let p_null: *const T = std::ptr::null();).
  • From FFI calls: Functions defined in extern blocks (especially C functions) may return raw pointers.

Passing and storing raw pointers is generally safe. Comparing raw pointers for equality (e.g., using std::ptr::eq or the == operator) is also safe and compares their addresses. Ordinal comparison (<, >, etc.) on raw pointers is defined and performs a byte-wise comparison of the addresses; however, the resulting order may be inconsistent or platform-dependent if the pointers do not originate from, or point within, the same allocated object.

fn main() {
    let mut data = 10;

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

    // Safe: Create a raw pointer from an address (usize). 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);

    // Obtaining a pointer from a slice using as_ptr() is shown in section 25.3.2.
    // For example:
    // let numbers = [1, 2, 3];
    // let slice_ptr: *const i32 = numbers.as_ptr();
    // println!("Address from slice.as_ptr(): {:p}", slice_ptr);
}

You might wonder if 0 as *const T could also be used to create a null pointer, similar to how (T*)0 is used in C. Indeed, Rust defines that casting a literal 0 to a pointer type produces a null pointer, and the std::ptr::null() function is documented as being equivalent to 0 as *const T for creating a pointer to address zero. However, using std::ptr::null() and std::ptr::null_mut() is generally preferred in Rust. These functions clearly convey the intent to create a null pointer and promote consistency by using the dedicated API from the std::ptr module, which centralizes pointer-related utilities. While currently resulting in the same zero-address pointer on common platforms, the explicit functions are the idiomatic choice.

Dereferencing a raw pointer (*p) to access the pointed-to data is unsafe, requiring an unsafe block, because the pointer’s validity (i.e., being non-null, aligned, pointing to initialized memory of the correct type, and not dangling) 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 to calculate new pointer addresses based on existing ones. Common methods include add(count) for moving forward (with count as usize), sub(count) for moving backward (with count as usize), and offset(count) which takes a signed isize for count and can move in either direction. The offset method is often preferred for its versatility in handling both positive and negative displacements with a single method, as it directly accepts an isize argument. These operations adjust the pointer address by count * std::mem::size_of::<T>() bytes, similar to C pointer arithmetic.

All these fundamental pointer arithmetic methods (add, sub, offset) are unsafe functions. This is because even though the address calculations themselves typically handle overflow by wrapping (producing some address value rather than panicking the way standard integer + might in debug mode), the resulting pointer might be misaligned, point outside allocated memory regions, or be otherwise invalid to dereference. The unsafe contract means the caller is responsible for ensuring that the arguments (like count) are valid in the current context and that any subsequent use of the calculated pointer, especially dereferencing, is safe.

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

    unsafe {
        // Using offset(): move pointer to the third element (index 2).
        // 'offset' takes an isize, so it can be positive or negative.
        let third_elem_ptr = start_ptr.offset(2);
        println!("Third element (via offset(2)): {}", *third_elem_ptr); // Outputs 30

        // Using add(): move pointer to the second element (index 1).
        // 'add' takes a usize, for forward movement.
        let second_elem_ptr = start_ptr.add(1);
        println!("Second element (via add(1)): {}", *second_elem_ptr); // Outputs 20

        // Using offset() for backward movement.
        let first_elem_again_ptr = third_elem_ptr.offset(-2);
        println!("First element (via offset(-2)): {}", *first_elem_again_ptr); // 10

        // Calculating the difference between pointers (in units of T).
        // 'offset_from' is also an unsafe method.
        let diff = third_elem_ptr.offset_from(start_ptr);
        println!("Offset difference (third_elem_ptr from start_ptr): {}", diff); // 2

        // Creating a pointer outside the bounds is possible with these methods.
        // Dereferencing it is Undefined Behavior.
        // let invalid_ptr = start_ptr.offset(10); // Points beyond the allocation
        // println!("{}", *invalid_ptr); // Undefined Behavior!
    }
}

Pointer arithmetic should be used with extreme caution. Ensure that any pointer you dereference remains within the bounds of a single valid memory allocation and is properly aligned. Safer alternatives, like slice indexing (numbers[i]) or iterators, should always be preferred when applicable.

For scenarios where pointer arithmetic might overflow and wrapping behavior is explicitly desired for the address calculation itself, Rust provides wrapping_add(count), wrapping_sub(count), and wrapping_offset(count). Unlike their non-wrapping counterparts, these wrapping_* methods are safe to call (they are not unsafe fn). This is because they guarantee that the pointer address calculation itself will wrap on overflow (consistent with twos-complement arithmetic) instead of panicking or causing other undefined behavior from the arithmetic operation itself. This can be useful in certain low-level algorithms where pointer values are treated more like integers that are allowed to wrap around the address space.

However, it’s crucial to remember that while calling these wrapping_* methods is safe, dereferencing the resulting pointer still requires an unsafe block and is only permissible if the pointer is valid (non-null, aligned, pointing to accessible and correctly typed memory, etc.). Using a wrapped pointer that is invalid for dereferencing will lead to undefined behavior.

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.