原始指针 raw pointer

Rust 支持原始指针, 原始指针与 C 语言中的指针是完全一样的.

与 C 语言一样, 可以用 *ptr 的方法对指针进行解引用, 用于访问指针指向的内存.

只读指针 const pointer

不能通过只读指针, 来修改指针指向的内存的值.

只读指针的形式是 *const T, 相当于 C 语言中的 const T*.

*const c_void 对应于 C 语言中的 const void*, 可以代表指向任意类型的指针, 使用时需要显式地转型.

具体来说, 只读的原始指针分为三种:

  • *const T, 指向元素 T 的原始指针
  • *const [T], 指向切片的原始指针
  • *const [T; N], 指向数组的原始指针, 这里包含了数组中的元素类型 T 以及元数的个数 N

*const T

这个原始指针的用处要更广泛一些, 它可以与 C 语言中的 const T* 进行互操作, 是 ABI 兼容的.

先看一个示例程序:

use std::ffi::c_void;
use std::ptr;

fn main() {
    let x: i32 = 42;
    // 指向基础数据类型 x 的原始指针
    let x_ptr: *const i32 = ptr::addr_of!(x);
    // 读取指针指向的内存所保存的值
    unsafe { assert_eq!(*x_ptr, 42); }
    // 另一种方式来读取
    unsafe { assert_eq!(x_ptr.read(), 42); }

    // 类型转换
    let void_ptr: *const c_void = x_ptr.cast();
    // 判断原始指针是否为空指针
    assert!(!void_ptr.is_null());

    let numbers = [1, 2, 3, 4, 5];
    // 指向数组中第一个元素的原始指针
    let first_num: *const i32 = numbers.as_ptr();
    // 访问数组中的第一个元素
    unsafe { assert_eq!(*first_num, 1); }
    // 访问数组中的第二个元素
    unsafe { assert_eq!(*first_num.add(1), 2) }
    // 访问数组中的第四个元素
    unsafe { assert_eq!(*first_num.offset(3), 4); }
    // 访问数组中的第五个元素
    unsafe { assert_eq!(*first_num.byte_offset((4 * size_of::<i32>()) as isize), 5) }
}

上面代码, 对应的内存操作如下图所示:

const ptr

下面还列出了指针的常用运算.

  • is_null(), 判断指针是否为空
  • cast(), 转换为指向另一种数据类型的指针
  • cast_mut(), 将不可变更指针转换为可变更指针
  • addr(), 得到指针的地址, 相当于 x_ptr as usize

指针偏移运算:

  • offset()
  • byte_offset()
  • wrapping_offset()
  • wrapping_byte_offset()
  • add()
  • byte_add()
  • wrapping_add()
  • wrapping_byte_add()
  • sub()
  • byte_sub()
  • wrapping_sub()
  • wrapping_byte_sub()

两指针之间的关系:

  • offset_from()
  • byte_offset_from()
  • sub_ptr()

*const [T]*const [T; N]

针对这两个指针的操作方式比较受限.

先看一个用例程序:

#![feature(array_ptr_get)]
#![feature(slice_ptr_get)]

use std::ptr;

fn main() {
    // 在栈上的整数数组
    let numbers: [i32; 5] = [1, 1, 2, 3, 5];

    // 指向数组的原始指针
    let num_ptr: *const [i32; 5] = &numbers as *const [i32; 5];
    assert!(!num_ptr.is_null());
    assert_eq!(size_of_val(&num_ptr), 8);
    unsafe { assert_eq!((*num_ptr)[0], 1); }
    unsafe { assert_eq!((*num_ptr)[4], 5); }

    // 得到 *const [T] 原始指针
    let num_ptr_slice: *const [i32] = &numbers as *const [i32];
    assert_eq!(size_of_val(&num_ptr_slice), 16);
    assert_eq!(num_ptr.as_slice(), num_ptr_slice);
    assert_eq!(num_ptr.as_ptr(), num_ptr_slice.as_ptr());
    assert_eq!(num_ptr_slice.len(), 5);
    unsafe { assert_eq!((*num_ptr_slice)[0], 1); }
    unsafe { assert_eq!((*num_ptr_slice)[4], 5); }

    let num_slice: &[i32] = &numbers;
    assert_eq!(num_slice.len(), 5);

    // 从 *const [T] 转换成 &[T]
    let num_slice2: &[i32] = unsafe {
        &*num_ptr_slice
    };
    // 这两个切片是完全相同的
    assert!(ptr::eq(num_slice, num_slice2));

    // 指向数组中第一个元素的原始指针
    let num_first_ptr: *const i32 = numbers.as_ptr();
    // 另一种方法, 得到指向数组中第一个元素的原始指针
    let num_first_ptr2: *const i32 = num_ptr_slice.cast();
    // 这两个原始指针指向同一个内存地址
    assert!(ptr::addr_eq(num_first_ptr, num_first_ptr2));
    unsafe { assert_eq!(*num_first_ptr, numbers[0]); }
}

*const [T] 本身是一个切片引用, 它的内存布局与 &[T] 是一致的, 甚至可以进行互换.

*const [T; N] 占用的内存中, 只有一个指向原始数组的指针, 并不包含数组的元素个数, 元素个数是编译器处理的.

其内存操作如下图所示:

const ptr array

可变更指针 mutable pointer

可变更指针的形式是 *mut T, 相当于 C 语言中的 T*.

*mut c_void 相当于 C 语言中的 void*, 可以代表指向任意类型的指针, 在使用时需要显式地转型.

所谓的可变更指针, 是可以通过该指针来修改指针所指向的内存的值.

与不可变更指针类型, 可变更指针也有三种形式:

  • *mut T, 指向元素 T 的原始指针
  • *mut [T], 指向切片的原始指针
  • *mut [T; N], 指向数组的原始指针, 这里包含了数组中的元素类型 T 以及元数的个数 N

*mut T

先看一个示例程序:

use std::ptr;

fn main() {
    let mut x: i32 = 42;
    // 指向基础数据类型 x 的原始指针
    let x_ptr: *mut i32 = ptr::addr_of_mut!(x);
    // 修改指针指向的内存所保存的值
    unsafe { *x_ptr = 43; }
    // 读取数值
    unsafe { assert_eq!(x_ptr.read(), 43); }

    // 类型转换
    let i8_ptr: *mut i8 = x_ptr.cast();
    #[cfg(target_endian = "little")]
    unsafe {
        assert_eq!(*i8_ptr, 43);
    }
    #[cfg(target_endian = "big")]
    unsafe {
        assert_eq!(*i8_ptr.add(3), 43);
    }

    let mut numbers = [1, 2, 3, 4, 5];
    // 指向数组中第一个元素的原始指针
    let first_num: *mut i32 = numbers.as_mut_ptr();
    // 修改数组中的第一个元素
    unsafe { *first_num = 2; }
    // 修改数组中的第二个元素
    unsafe { *first_num.add(1) = 4; }
    // 修改数组中的第二个元素
    unsafe { *first_num.wrapping_add(2) = 6; }
    // 修改数组中的第四个元素
    unsafe { *first_num.offset(3) = 8; }
    // 修改数组中的第五个元素
    unsafe { *first_num.byte_offset((4 * size_of::<i32>()) as isize) = 10; }

    assert_eq!(numbers, [2, 4, 6, 8, 10]);
}

上面代码, 对应的内存操作如下图所示:

mut ptr

这里的, i8_ptr 比较有意思, 它只是指向了 x 的第一个字节, 如果是小端 (little endian) 的系统, 里面存放的数值恰好是 43.

下面还列出了可变更指针的常用运算.

  • is_null(), 判断指针是否为空
  • cast(), 转换为指向另一种数据类型的指针
  • cast_const(), 将可变更指针转为 *const T
  • addr(), 得到指针的地址, 相当于 x_ptr as usize

指针偏移运算:

  • offset()
  • byte_offset()
  • wrapping_offset()
  • wrapping_byte_offset()
  • add()
  • byte_add()
  • wrapping_add()
  • wrapping_byte_add()
  • sub()
  • byte_sub()
  • wrapping_sub()
  • wrapping_byte_sub()

两指针之间的关系:

  • offset_from()
  • byte_offset_from()
  • sub_ptr()

*mut [T]*mut [T; N]

先看一个用例程序:

#![feature(array_ptr_get)]
#![feature(slice_ptr_get)]

use std::ptr;

fn main() {
    // 在栈上的整数数组
    let mut numbers: [i32; 5] = [1, 2, 3, 4, 5];

    // 指向数组的原始指针
    let num_ptr: *mut [i32; 5] = ptr::addr_of_mut!(numbers);
    assert!(!num_ptr.is_null());
    assert_eq!(size_of_val(&num_ptr), 8);
    unsafe {
        (*num_ptr)[0] = 2;
        (*num_ptr)[1] = 4;
    }

    // 得到 *mut [T] 原始指针
    let num_ptr_slice: *mut [i32] = ptr::addr_of_mut!(numbers);
    assert_eq!(size_of_val(&num_ptr_slice), 16);
    assert_eq!(num_ptr.as_mut_slice(), num_ptr_slice);
    assert_eq!(num_ptr.as_mut_ptr(), num_ptr_slice.as_mut_ptr());
    assert_eq!(num_ptr_slice.len(), 5);
    unsafe {
        (*num_ptr_slice)[2] = 6;
        (*num_ptr_slice)[3] = 8;
    }

    let num_slice: &mut [i32] = &mut numbers;
    assert_eq!(num_slice.len(), 5);
    num_slice[4] = 10;

    // 从 *mut [T] 转换成 &[T]
    let num_slice2: &mut [i32] = unsafe {
        &mut *num_ptr_slice
    };
    // 这两个切片是完全相同的
    assert!(ptr::eq(num_slice, num_slice2));

    assert_eq!(numbers, [2, 4, 6, 8, 10]);

    // 指向数组中第一个元素的原始指针
    let num_first_ptr: *mut i32 = numbers.as_mut_ptr();
    // 另一种方法, 得到指向数组中第一个元素的原始指针
    let num_first_ptr2: *mut i32 = num_ptr_slice.cast();
    // 这两个原始指针指向同一个内存地址
    assert!(ptr::addr_eq(num_first_ptr, num_first_ptr2));
    unsafe { assert_eq!(*num_first_ptr, 2); }
}

*mut [T] 本身是一个切片引用, 它的内存布局与 &mut [T] 是一致的, 甚至可以进行互换.

*mut [T; N] 占用的内存中, 只有一个指向原始数组的指针, 并不包含数组的元素个数, 元素个数是编译器处理的.

其内存操作如下图所示:

mut ptr array

模拟 C++ 中的 const_cast::

前文有介绍过, Rust 中声明的变量默认都是只读的, 除非显式地声明为 mut, 比如 let mut x = 42;. 但有时候, 可能需要实现像 C++ 中的 const_cast() 那样的类型转换工作, 以方便在函数内部修改一个不可变变量的值.

以下代码片段演示了如何通过原始指针进行类型转换的操作:

fn main() {
    let x = 42;
    let x_ptr = &x as *const i32;
    let x_mut_ptr: *mut i32 = x_ptr.cast_mut();
    unsafe {
        x_mut_ptr.write(43);
    }
    assert_eq!(x, 43);
}

上面的示例中, 通过取得只读变量 x 的内存地址, 直接将新的值写入到该地址, 就可以强制修改它的值.