Skip to content

06.引用和借用

引用

IMPORTANT

引用是没有所有权指针

以下示例:

image-20250209203231975

一般情况下,我们在 L3 处使用 greet(m1, m2) 进行函数调用,此时 m1m2 的所有权将被转移到函数内部,此后,m1m2 将不再可用。为了解决这种问题,rust 提供了引用的方法,如上示例,调用时前面加上 & 符号,注意声明函数的参数时类型注解也加上 & 符号,调用内存详细过程变成了下图,看到 g1g2 指向了 m1m2 ,且最后 m1m2 仍可用。g1g2 是对变量 m1m2 的引用,本身并不具有数据(不像 m1m2 分别拥有自己指向的数据),因此当 g1g2 被释放时,依照当栈上的变量被释放,指向的堆上的数据也被释放的原则,由于本身不指向数据,原有的数据并不会被释放。

因此,引用指向了其它指针,指向了 m1m2 ,因此是指针;由于指向的不是具体的数据,因此没有所有权,称引用是不具备所有权的指针。

以下代码段说明了这一点:

rust
fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("World");
    println!("m1: {}, m2: {}", m1, m2);
    println!("m1 的内存地址:{:p}", &m1);
    greet(&m1, &m2);
    let s = format!("{}, {}", m1, m2);
}

fn greet(g1: &String, g2: &String) {
    println!("g1: {}, g2: {}", g1, g2);
    let address_in_g1 = g1 as *const String;
    println!("g1 存放的内容:{:p}", address_in_g1);
    println!("g1 的内存地址:{:p}", &g1);
}

输出:

m1: Hello, m2: World
m1 的内存地址:0x2a204ffab0
g1: Hello, g2: World
g1: Hello
g1 存放的内容:0x2a204ffab0
g1 的内存地址:0x2a204ff8a8

可以看到,g1 中存放的是 m1 所在的内存地址,当直接打印 m1 时,{}自动解析 m1 的内容(即指向 Hello 的内存地址)打印出 Hello。当打印 g1 时,自动解引用会首先根据 g1 中的指向 m1 的内存地址解析出 m1 的位置,然后根据 m1 的内容打印出 m1 指向的数据内容 Hello

解引用

上述的 println!() 宏实现了自动解引用,标准的解引用是使用 * 运算符,如下示例:

rust
let mut x = Box::new(1);
let a = *x;  // 使用 * 解引用,读取 heap 上的值,从而 a = 1
*x += 1;  // 修改 heap 上的值,从而 x 现在为 2
// 两次解引用
let r1 = &x;  // r1 是一个指向 x 的引用,指向 stack 上的 x
let b = **r1;  // 两次解引用,从而 b = 2
let r2 = &*x;  // r2 是指向 2 的引用,指向 heap 上的 2
let c = *r2;  // 一次解引用,从而 c = 2

图示如下:

image-20250209221329670

隐式解引用

在 rust 中有很多方法自动实现了解引用,而没有显式的使用 * 进行解引用,具体有 3 个示例:

示例 1:

通过变量本身带有的方法隐式解引用:

rust
let x = Box::new(-1);
let x_abs1 = i32::abs(*x);  // 显式解引用
let x_abs2 = x.abs();  // 隐式解引用
assert_eq!(x_abs1, x_abs2);
println!("x_abs1: {}, x_abs2: {}", x_abs1, x_abs2);

输出:x_abs1: 1, x_abs2: 1

其中,x.abs() 就实现了自动解引用

示例 2:

变量本身带有的方法可以实现多层解引用:

rust
let r = &x;  // 现在 r 是指向 x 的一个引用,不直接指向 -1
let r_abs1 = i32::abs(**r);  // 显式解引用需要两次 **
let r_abs2 = r.abs();  // 隐式解引用可以自动实现多层解引用
assert_eq!(r_abs1, r_abs2);
println!("r_abs1: {}, r_abs2: {}", r_abs1, r_abs2);

输出:r_abs1: 1, r_abs2: 1

示例 3:

反向解引用同样可以实现:

rust
let s = String::from("Hello");
let s_len1 = str::len(&s);  // 显式引用
let s_len2 = s.len();  // 隐式引用
assert_eq!(s_len1, s_len2);
println!("s_len1: {}, s_len2: {}", s_len1, s_len2);

在 rust 中的 String 中,变量 s 本身不是一个指针,而是一个结构体,其中包含 3 个元素:

  • 指向堆上数据的指针(这个指针指向 "Hello" 字符串的内存位置)
  • 字符串的长度
  • 字符串的容量

因此不能通过 * 来获取指针,使用 * 是尝试获取这个结构体,这在 rust 中是不被允许的。使用 & 方法,在 rust 中实现了可以拿到这个结构体中的指针指向的数据的方法,因此可以是哟个 & 获取到 Hello

此时,不叫作解引用,而是引用。

题目:

考虑以下程序,如果要访问 x 指向的 0,需要几次解引用(几次 * 号)?

image-20250210115831561

答案:3 次

解析:

  • *y 表示 x 的引用,是 x 在栈上的内存地址
  • **y 表示 x 本身,是根据 x 的内存地址找到的 x
  • ***y 表示 x 指向的数据 0,是根据 x 的内容(数据 0 在堆上的内存地址)找到的 0

不可变借用

在对一个可变数据进行不可变借用时,在不可变借用的变量的作用域结束前,该可变数据都不可以被修改。如下示例,虽然 v 是可变变量,但由于 num 是对 v 中的一个元素的不可变借用,因此在 num 的作用域结束前(或说 num 使用完成前),v 都不可以被修改。Rust 这样做的原因是避免若对原数据 v 进行修改时,num 原本指向的数据发生不期望的变化。如若允许 v 发生变化,则原本 num 指向的数据 3 可能就不是 3 了,存在安全问题。

rust
let mut v: Vec<i32> = vec![1, 2, 3];
let num: &i32 = &v[2];
// v.push(4);  // 报错,在 num 作用域结束前都不可对 v 进行修改
println!("Third element is {}", *num);

具体来讲:

查看变量 v 的数据内容:

image-20250210122037512

可以看到 cap(容量)为 3,因此,如果允许 v.push(4) 的正常执行,由于容量不够,因此会在内存中新开辟一段长度为 4 的区域,将原本的 3 个数据拷贝到此,然后添加元素 4,导致原本的 num 对第 3 个元素的引用变成了一个无效的指针指向

可变借用

在可变借用中,被借用的变量将失去所有权限,包括读权限,直到借用结束。

rust
println!("可变借用");
let mut v2 = vec![1,2,3];
println!("v2: {:?}", v2);
let num2 = &mut v2[2];
// 此时不可以访问 v2
*num2 = 4;
println!("v2: {:?}", v2);

输出:

可变借用
v2: [1, 2, 3]
v2: [1, 2, 4]

num2v2 的可变借用,因此 num2 可以修改 v2 中的元素 v2[2] ,如果程序较为复杂时,rust 无法得知在 num2 作用于结束前 v2[2] 元素的真实值,因此可能发生异常,这是 rust 不期望的,因此此时 v2 将不允许访问,也不允许编译通过。

CAUTION

在同一作用域内,不能同时存在对同一变量的可变和不可变借用

以下示例:

rust
let mut s = String::from("Hello");
let s2 = &s;
let s3 = &mut s;
s3.push_str(" World");
println!("s2: {s2}");

无法通过编译,示意图如下:

06.引用和借用

如果允许可变和不可变借用的同时存在,一旦可变借用修改了数据,不可变借用仍以为自己指向的数据是旧数据,导致产生未定义行为。

Box

Rust 中,Box<T> 是一个 智能指针,用于在 堆上存储数据,访问值时需要 * 解引用。

示例:

rust
let x = Box::new(-1);
println!("x: {}", *x);

输出:

x: -1

Box 的所有权

Box 变量的所有权会发生移动,当发生移动后,该变量对堆上的数据不再拥有所有权,而是转移给了另一个变量,如下:

rust
let f = Box::new(-1);
println!("f: {}", f);
let g = f;
println!("g: {}", g);
// println!("f: {}", f);  // 此时发生报错,因为 -1 的所有权已经从 f 转移给了 g,f 不再可用

输出:

g: -1

Box 的引用

Box 的引用可以被多个变量同时使用,而不存在所有权问题:

rust
let h = Box::new(-1);
let i = &h;
let j = &h;
println!("i: {}, j: {}", *i, *j);

输出:

i: -1, j: -1