当先锋百科网

首页 1 2 3 4 5 6 7

这一节主要介绍 Rust 中数组与指针的相关概念。希望通过本文,你对 Rust 有更深入的了解。

什么是数组?

数组是一组包含相同数据类型 `T` 的集合,存储在连续的内存区域中。理论上,内存(我们暂且不去讨论物理内存与虚拟内存)相当于一个类型为 `u8`、长度为 `usize` 的数组,内存操作相当于操作这个数组。因此,`usize` 可以表示每个内存地址。Rust 又规定,`isize` 的最大值是对象和数组大小的理论上限,这样可以确保 `isize` 可用于计算指向对象和数组的指针之间的差异,并可寻址对象中的每个字节及末尾的一个字节。

数组使用 `[]` 来创建,其大小在编译期间就已经确定,数组的类型被标记为 `[T; size]`,表示一个类型为 `T`,`size`个元组的数组。数组的大小是固定的,但其中的元素是可以被更改的。

接下来,我们创建一个类型为 `i32`,长度为8的数组,修改几个元素,并返回长度:

fn 

我们将其编译为汇编:

core::slice::<impl [T]>::len:
  sub rsp, 16
  mov qword ptr [rsp], rdi
  mov qword ptr [rsp + 8], rsi
  mov rax, qword ptr [rsp + 8]
  add rsp, 16
  ret

example::main:
  sub rsp, 40                     // 分配栈帧,rsp 寄存器存放当前函数的栈顶地址,
  lea rax, [rsp + 8]
  xor esi, esi
  mov rcx, rax
  mov rdi, rcx
  mov edx, 32
  mov qword ptr [rsp], rax
  call memset@PLT                 // 这里调用 memset 将数组的所有元素初始化为 0
  mov dword ptr [rsp + 8], 123    // 修改数组的第1个元素
  mov dword ptr [rsp + 12], 456   // 修改数组的第2个元素
  mov dword ptr [rsp + 36], 789   // 修改数组的第7个元素
  mov rax, qword ptr [rsp]
  mov rdi, rax
  mov esi, 8                      // 数组的长度是在编译时就确定的
  call qword ptr [rip + core::slice::<impl [T]>::len@GOTPCREL]
  add rsp, 40                     // 回收栈帧
  ret

你不必看懂上面的汇编代码。但是我们大概可以看到,数组的长度和内存占用大小,在编译时就已经确定。`rsp + 8` 为数组的地址,`[rsp + 8]` 就是这个地址对应的内存。瞧,在高级语言中操作内存,跟汇编中操作内存,很像的! 我们可以通过偏移量(索引)去操作内存中相应位置的值。

既然编译时就确定了数组的长度,如果越界访问数组,编译器会很容易检测出来:

fn main() {
    let mut array: [i32; 8] = [0; 8];

    array[10] = 123;
}
 --> <source>:4:5
  |
4 |     array[10] = 123;
  |     ^^^^^^^^^ index out of bounds: the len is 8 but the index is 10
  |

  = note: `#[deny(unconditional_panic)]` on by default

除了 `let mut array: [i32; 8] = [0; 8];` 这种初始化数组的语法,我们还可以:

fn main() {
    let mut array: [i32; 8] = [1, 2, 3, 4, 5, 6, 7, 8];

    array[0] = 123;
    array[1] = 456;
    array[7] = 789;
}

编译成汇编后:

example::main:
  sub rsp, 32
  mov dword ptr [rsp], 1
  mov dword ptr [rsp + 4], 2
  mov dword ptr [rsp + 8], 3
  mov dword ptr [rsp + 12], 4
  mov dword ptr [rsp + 16], 5
  mov dword ptr [rsp + 20], 6
  mov dword ptr [rsp + 24], 7
  mov dword ptr [rsp + 28], 8
  mov dword ptr [rsp], 123
  mov dword ptr [rsp + 4], 456
  mov dword ptr [rsp + 28], 789
  add rsp, 32
  ret

你可以看到,这次没有调用 `memset` 将数组初始化为零,而是直接修改相应的元素。

数组分配在栈上,但 Rust 中,栈的大小是有限制的,取决于操作系统的限制。比如 Linux 下默认为 8M,Windows 下默认为 2M。这太小了,很多情况下这是不够用的。我们可以利用 `Box`,将数组分配到堆上:

fn main() {
    let mut array: Box<[i32; 1024]> = Box::new([0; 1024]);

    array[0] = 123;
    array[1] = 456;
    array[1023] = 789;
}

但是,这并不是我们期望的那样:

example::main:
  mov eax, 4120
  call __rust_probestack
  sub rsp, rax
  xor esi, esi
  lea rax, [rsp + 24]
  mov rdi, rax
  mov eax, 4096
  mov rdx, rax
  mov qword ptr [rsp + 8], rax
  call memset@PLT
  mov rdi, qword ptr [rsp + 8]
  mov esi, 4
  call alloc::alloc::exchange_malloc
  mov rcx, rax
  lea rdx, [rsp + 24]
  mov rdi, rax
  mov rsi, rdx
  mov edx, 4096
  mov qword ptr [rsp], rcx
  call memcpy@PLT
  mov rax, qword ptr [rsp]
  mov qword ptr [rsp + 16], rax
  mov rax, qword ptr [rsp + 16]
  mov dword ptr [rax], 123
  mov rax, qword ptr [rsp + 16]
  mov dword ptr [rax + 4], 456
  mov rax, qword ptr [rsp + 16]
  mov dword ptr [rax + 4092], 789
  lea rdi, [rsp + 16]
  call qword ptr [rip + core::ptr::drop_in_place@GOTPCREL]
  add rsp, 4120
  ret

这段代码首先会在栈上分配好数组,再在堆上分配内存,然后将值拷贝到堆上。修改数组元素,需要先计算出数组的地址,然后根据偏移量(索引)去修改。我们能不能将数组直接分配到堆上呢?当然是可以的,请继续往下看。

什么是指针?

指针是一个包含内存地址的变量。在 Rust 中,指针包括裸指针(`*const T` 和 `*mut T`)、可变/不可变引用(也可以叫做借用)(`&mut T` 和 `&T`)和智能指针(`Box<T>`、`Rc<T>`、 `Arc<T>`、`Cell<T>`、`RefCell<T>` 、`UnsafeCell<T>` 等)。

如果获取数组的指针?

我们可以用 `&` 和 `&mut` 操作符取得数组的引用,再用 `as` 操作符将引用转换为裸指针:

fn main() {
    let mut array: [i32; 3] = [1, 2, 3];

    let ref1: &[i32; 3] = &array;

    let ptr1: *const [i32; 3] = ref1 as *const [i32; 3];

    let ref2: &mut [i32; 3] = &mut array;

    let ptr2: *mut [i32; 3] = ref2 as *mut [i32; 3];
}

我们可以用 `*` 去解引引用和裸指针,但是解引裸指针是 `unsafe` 的!需要放到 `unsafe {}` 块中。为什么不安全?后面会讲解。

fn main() {
    let mut array: [i32; 3] = [1, 2, 3];

    let ref1: &[i32; 3] = &array;

    let ptr1: *const [i32; 3] = ref1 as *const [i32; 3];

    unsafe {
        let mut array2: [i32; 3] = *ptr1;

        array2[0] = 123;
        array2[1] = 456;
        array2[2] = 789;

        if (array == array2) {

        }
    }
}

将上面代码编译后,你会发现结果并不是你预料中的那样。虽然我们解引了 `array` 的裸指针 `ptr1` 得到了 `array2`,但是修改 `array2` 的值并不会影响到 `array`。由于 `i32` 类型是实现了 `Copy`,`[i32; 3]` 也是实现了 `Copy`的,因此在解引的时候,会将 `array`复制一份。如果我们解引一个没有实现 `Copy`的类型:

fn main() {
    let s = String::new();

    let ptr: *const String = &s as *const String;

    unsafe {
        let s2: String = *ptr;
    }
}

这段代码是编译不过去的,编译器会告诉你:

error: src/main.rs:7: cannot move out of `*ptr` which is behind a raw pointer
error: src/main.rs:7: move occurs because `*ptr` has type `std::string::String`, which does not implement the `Copy` trait

Rust 通常情况下是不需要你手动管理内存给的,`String` 是一个分配在堆上的字符串类型,离开作用域后会自动释放堆内存。上面的代码,如果编译器不采取一些机制,阻止你这么做,让 `s` 和 `s2` 指向同一块内存,当 `s` 和 `s2` 离开作用域后,会让内存释放两次,这是不正确的。

不过编译器也提示你,将 `*ptr` 改为 `&*ptr` (help: consider borrowing here: `&*ptr`):

fn main() {
    let s = String::new();

    let ptr: *const String = &s as *const String;

    unsafe {
        let s2: &String = &*ptr;
    }
}

我们利用 `&` 将裸指针转换为了 `&String`。这时候,`s` 和 `s2` 虽然指向了同一块内存,但是 `s2` 只是个不可变借用,并没有这块内存的所有权,只是临时借来用用,用完会还回去。

但是,问题又出来了,我们将上面的代码修改一下:

fn main() {
    let mut s = String::new();

    let ptr: *const String = &s as *const String;

    unsafe {
        let s2: &String = &*ptr;

        s.push('a');

        let len = s2.len();

        println!("{:?}", len); // 1
    }
}

根据你之前学习过的所有权的知识,上面代码是可能是无法编译通过的——可变引用与不可变引用不能同时存在(`s2` 是 `s`的不可变引用,但是后面却修改了 `s` 的值)。但是,上面的代码能编译通过,并且能正确打印出 `s2` 的长度为1。

我们将上面代码修改成通常的方式:

fn main() {
    let mut s = String::new();

    let s2: &String = &s;

    s.push('A');

    let len = s2.len();

    println!("{:?}", len);
}

这绝对是编译不过去的:

4 |     let s2: &String = &s;
  |                       -- immutable borrow occurs here
5 |
6 |     s.push('A');
  |     ^^^^^^^^^^^ mutable borrow occurs here
7 |
8 |     let len = s2.len();
  |               -- immutable borrow later used here

为什么在那种情况下 Rust 不能保证所有权机制呢?或者是,利用裸指针突破所有权机制,会造成什么样的后果?(虽然上面那段代码符合逻辑,在其他语言中也允许那么做)

我们一开始提到 “`usize` 可以表示每个内存地址”,”内存是一个大数组“。没错,裸指针其实就是个 `usize`!它存储的值,就是内存地址。

fn main() {
    let mut s = String::new();

    let ptr: *const String = &s as *const String;
    let index: usize = ptr as usize;

    println!("{:x}", index); // 类似于 7fff0ede3988

    let ptr2: *const String = index as *const String;

    unsafe {
        let s2: &String = &*ptr2;

        s.push('a');

        let len = s2.len();

        println!("{:?}", len); // 1
    }
}

我们将裸指针转换为 `usize`,可以再将 `usize` 转换为裸指针。在转换的过程中,会丢掉上下文信息,让编译器无法判定 `s2` 是 `s` 的不可变引用。这也为我们提供了一个豁口,得以让我们暂时突破所有权机制,去实现一些高效的数据结构。

裸指针是不安全的,在你不清楚自己在做什么时,请不要碰裸指针!在你不清楚自己在做什么时,请不要碰裸指针!在你不清楚自己在做什么时,请不要碰裸指针!

比如这段代码:

fn s_ptr() -> *const String {
    let s = "hello".to_string();
    let ptr: *const String = &s as *const String;
    ptr
}

fn main() {
    let ptr2: *const String = s_ptr();

    unsafe {
        let s2: &String = &*ptr2;

        let len = s2.len();

        println!("{:?}", len);
        println!("{}", s2); // segmentation fault (core dumped)
    }
}

Rust 会阻止你返回局部变量的引用,但是并没有阻止你返回裸指针。函数 `s_ptr` 中,你虽然返回出了 `s` 的裸指针,但是 `s_ptr` 调用结束后,会释放 `s` 的内存。`ptr2` 是一个悬垂指针(dangling pointer),当你解引 `ptr2` 得到 `s2` 时,`s2` 是一个悬垂引用(dangling references)。不过在正常的 Rust 代码中,编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域 —— 前提是你不碰这些 `unsafe` 的东西。

可以通过裸指针修改数组吗?

当然可以!

我们先看这段代码:

fn main() {
    let array: [i32; 3] = [1, 2, 3];

    println!("{:?}", array); // [1, 2, 3]

    let ptr: *const i32 = &array as *const [i32; 3] as *const i32;

    unsafe {
        let a = ((ptr as usize) + 0) as *const i32;
        println!("{:?}", *a); // 1

        let b = ((ptr as usize) + 4) as *const i32;
        println!("{:?}", *b); // 2

        let c = ((ptr as usize) + 8) as *const i32;
        println!("{:?}", *c); // 3
    }
}

在这段代码中,我们将数组的裸指针 `*const [i32; 3]` 转换为 `as *const i32`,也就是第一个元素的地址。然后通过偏量去访问其他元素。由于 `i32` 类型占个字节,因此第2和第3个元素的偏移量分别是4和8。

我们再次修改代码:

fn main() {
    let array: [i32; 3] = [1, 2, 3];

    println!("{:?}", array); // [1, 2, 3]

    let ptr: *const i32 = &array as *const [i32; 3] as *const i32;

    unsafe {
        let a = ((ptr as usize) + 0) as *mut i32;

        let a2: &mut i32 = &mut *a;
        *a2 = 123;

        let b = ((ptr as usize) + 4) as *mut i32;

        let b2: &mut i32 = &mut *b;
        *b2 = 456;

        let c = ((ptr as usize) + 8) as *mut i32;

        let c2: &mut i32 = &mut *c;
        *c2 = 789;
    }

    println!("{:?}", array); // [123, 456, 789]
}

跟上面的代码不同的是,我们利用 `&mut *` 将裸指针转换为 `&mut i32`,再修改。最后打印 `array`,你可以看到数组已经被修改了。注意,`*const T 和 *mut T` 是可以利用 `as` 互相转换的,并不像 `&mut T` 能转换为 `&T`,而 `&T` 不能转换为 `&mut T`。虽然你可以利用裸指针作为媒介,将 `&T` 转换为 `&mut T`,在你不清楚你在做什么时,请不要这么做!

我们可以利用标准库[link](pointer - Rust)去简化上面的代码:

fn main() {
    let array: [i32; 3] = [1, 2, 3];

    println!("{:?}", array); // [1, 2, 3]

    let ptr: *mut i32 = &array as *const [i32; 3] as *mut i32;

    unsafe {
        println!("{:?}", ptr.add(0).read()); // 1

        ptr.add(1).write(456); // 第2个元素
    }

    println!("{:?}", array); // [1, 456, 3]
}

`add` 方法会帮你计算偏移量。然后用 `read` 和 `write` 就可以读写相应位置的值。还要说明的是,`*const T` 和 `*mut T` 是实现了 `Copy`的。

如何直接将数组分配到堆上?

Rust [标准库](std::alloc - Rust)提供了 `std::alloc::alloc`、`std::alloc::dealloc` 和 `std::alloc::realloc` 等函数,对应于 `C` 语言的 `calloc`、`free` 和 `realloc`。利用这几个函数,我们可以手动管理堆内存。

use std::alloc::{self, Layout};
use std::mem;

fn main() {
    unsafe {
        // 长度为32的i32数组
        let layout = Layout::from_size_align_unchecked(32 * mem::size_of::<i32>(), mem::size_of::<i32>());

        // 分配内存
        let ptr: *mut i32 = alloc::alloc(layout) as *mut i32;

        println!("{:?}", ptr.read());

        ptr.write(123);

        println!("{:?}", ptr.read());

        ptr.add(1).write(456);

        println!("{:?}", ptr.add(1).read());

        // 释放内存
        alloc::dealloc(ptr as *mut u8, layout);
    }
}

这段代码在堆上分配一个长度为32的 `i32` 数组。`alloc` 函数返回一个 `*mut u8` 指针,我们转换为 `*mut i32` 之后就可以想上一小节那样读写元素了。

更进一步,我们可以利用标准库提供的 [`slice`](slice - Rust) 类型:

use std::alloc::{self, Layout};
use std::mem;
use std::slice;

fn main() {
    unsafe {
        // 长度为32的i32数组
        let layout = Layout::from_size_align_unchecked(32 * mem::size_of::<i32>(), mem::size_of::<i32>());

        // 分配内存
        let ptr: *mut i32 = alloc::alloc(layout) as *mut i32;

        let slice: &mut [i32] = slice::from_raw_parts_mut(ptr, 32);

        slice[0] = 123;
        slice[1] = 456;
        slice[2] = 789;

        println!("{:?}", slice); // [123, 456, 789, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

        println!("{:?}", &slice[..3]); // [123, 456, 789]

        // 释放内存
        alloc::dealloc(ptr as *mut u8, layout);
    }
}
```

利用 `slice`,我们可以方便的操作数组,我们不能修改切片的长度,但是可以从旧切片得到一个新切片。`slice` 是一个胖指针,除了指针外,还包含了长度。瞧:

```rust
struct FatPtr<T> {
    data: *const T,
    len: usize
}

我们还可以实现动态增长的数组:

use std::alloc::{self, Layout};
use std::mem;
use std::slice;

fn main() {
    unsafe {
        // 长度为32的i32数组
        let layout = Layout::from_size_align_unchecked(32 * mem::size_of::<i32>(), mem::align_of::<i32>());

        // 分配内存
        let mut ptr: *mut i32 = alloc::alloc(layout) as *mut i32;

        // 扩容
        ptr = alloc::realloc(ptr as *mut u8, layout, 64 * mem::size_of::<i32>()) as *mut i32;

        let slice: &mut [i32] = slice::from_raw_parts_mut(ptr, 64);

        slice[0] = 123;
        slice[1] = 456;
        slice[2] = 789;

        println!("{:?}", &slice[..3]); // [123, 456, 789]

        // 释放内存
        alloc::dealloc(ptr as *mut u8, layout);
    }
}

原理是这样的。我们可以继续封装一下:

use std::alloc::{self, Layout};
use std::mem;
use std::slice;
use std::ops;

pub struct MyArray<T: Sized> {
    ptr: *mut T,
    capacity: usize,
    len: usize
}

impl<T: Sized> MyArray<T> {
    pub fn with_capacity(capacity: usize) -> MyArray<T> {
        let elem_size = mem::size_of::<T>();
        let alloc_size = capacity * elem_size;
        let align = mem::align_of::<T>();

        let layout = Layout::from_size_align(alloc_size, align).unwrap();

        let ptr = unsafe {
            alloc::alloc(layout) as *mut T
        };

        MyArray {
            ptr,
            capacity,
            len: 0
        }
    }

    pub fn double(&mut self) {
        let elem_size = mem::size_of::<T>();
        let new_cap = 2 * self.capacity;
        let new_size = new_cap * elem_size;

        let align = mem::align_of::<T>();
        let size = mem::size_of::<T>() * self.capacity;
        let layout = Layout::from_size_align(size, align).unwrap();

        unsafe {
            self.ptr = alloc::realloc(self.ptr as *mut u8, layout, new_size) as *mut T;
        }

        self.capacity = new_cap;
    }

    pub fn capacity(&self) -> usize {
        self.capacity
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn push(&mut self, value: T) {
        if self.len == self.capacity {
            self.double()
        }

        unsafe {
            self.ptr.add(self.len).write(value);
            self.len += 1;
        }
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            None
        } else {
            self.len -= 1;
            unsafe {
                Some(self.ptr.add(self.len).read())
            }
        }
    }

    pub fn as_slice(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.ptr, self.len) }
    }

    pub fn as_mut_slice(&self) -> &mut [T] {
        unsafe { slice::from_raw_parts_mut(self.ptr, self.len) }
    }
}

impl<T: Sized> Drop for MyArray<T> {
    fn drop(&mut self) {
        let align = mem::align_of::<T>();
        let size = mem::size_of::<T>() * self.capacity;
        let layout = Layout::from_size_align(size, align).unwrap();

        for _ in 0..self.len {
            self.pop();
        }

        unsafe {
            alloc::dealloc(self.ptr as *mut u8, layout);
        }
    }
}

impl<T> ops::Deref for MyArray<T> {
    type Target = [T];

    fn deref(&self) -> &[T] {
        self.as_slice()
    }
}

impl<T> ops::DerefMut for MyArray<T> {
    fn deref_mut(&mut self) -> &mut [T] {
        self.as_mut_slice()
    }
}

fn main() {
    let mut array: MyArray<i32> = MyArray::with_capacity(3);

    array.push(1);
    array.push(2);
    array.push(3);

    println!("{:?}", array[0]); // 1

    println!("{:?}", &array[..]); // [1, 2, 3]

    array.pop();

    println!("{:?}", &array[..]); // [1, 2]

    array.push(4);
    array.push(5);

    println!("{:?}", &array[..]); // [1, 2, 4, 5]
    println!("{:?}", array.capacity()); // 6
}

我们只实现了几个基本的方法。在 `MyArray<T>` 结构体中包含一个指针,`capacity` 表示分配的容量,`len` 表示当前使用的长度。添加元素时,如果容量不够,对底层数组进行扩容。我们实现了 `Deref` 和 `DerefMut`,就可以方便的利用 `slice` 提供的一些方法。最后,利用 `Drop` 释放内存。

这不就是 [`Vec`](std::vec::Vec - Rust) 嘛!

我们可以去标准库源码看 `Vec` 的实现,这是 `Vec` 的结构:

pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

pub struct RawVec<T, A: Alloc = Global> {
    ptr: Unique<T>,
    cap: usize,
    a: A,
}

pub struct Unique<T: ?Sized> {
    pointer: *const T,
    _marker: PhantomData<T>,
}

`Unique` 是个智能指针,并不能在标准库以外的地方去使用。不过当你熟悉 Rust 的之后,你可以创建你自己的智能指针。

这一章节的内容就到这里,我们下节再见!