06.引用和借用
引用
IMPORTANT
引用是没有所有权的指针。
以下示例:

一般情况下,我们在 L3 处使用 greet(m1, m2) 进行函数调用,此时 m1 和 m2 的所有权将被转移到函数内部,此后,m1 和 m2 将不再可用。为了解决这种问题,rust 提供了引用的方法,如上示例,调用时前面加上 & 符号,注意声明函数的参数时类型注解也加上 & 符号,调用内存详细过程变成了下图,看到 g1 和 g2 指向了 m1 和 m2 ,且最后 m1 和 m2 仍可用。g1 和 g2 是对变量 m1 和 m2 的引用,本身并不具有数据(不像 m1 和 m2 分别拥有自己指向的数据),因此当 g1 和 g2 被释放时,依照当栈上的变量被释放,指向的堆上的数据也被释放的原则,由于本身不指向数据,原有的数据并不会被释放。
因此,引用指向了其它指针,指向了 m1 和 m2 ,因此是指针;由于指向的不是具体的数据,因此没有所有权,称引用是不具备所有权的指针。
以下代码段说明了这一点:
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!() 宏实现了自动解引用,标准的解引用是使用 * 运算符,如下示例:
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图示如下:

隐式解引用
在 rust 中有很多方法自动实现了解引用,而没有显式的使用 * 进行解引用,具体有 3 个示例:
示例 1:
通过变量本身带有的方法隐式解引用:
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:
变量本身带有的方法可以实现多层解引用:
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:
反向解引用同样可以实现:
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,需要几次解引用(几次 * 号)?

答案: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 了,存在安全问题。
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 的数据内容:

可以看到 cap(容量)为 3,因此,如果允许 v.push(4) 的正常执行,由于容量不够,因此会在内存中新开辟一段长度为 4 的区域,将原本的 3 个数据拷贝到此,然后添加元素 4,导致原本的 num 对第 3 个元素的引用变成了一个无效的指针指向。
可变借用
在可变借用中,被借用的变量将失去所有权限,包括读权限,直到借用结束。
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]num2 是 v2 的可变借用,因此 num2 可以修改 v2 中的元素 v2[2] ,如果程序较为复杂时,rust 无法得知在 num2 作用于结束前 v2[2] 元素的真实值,因此可能发生异常,这是 rust 不期望的,因此此时 v2 将不允许访问,也不允许编译通过。
CAUTION
在同一作用域内,不能同时存在对同一变量的可变和不可变借用。
以下示例:
let mut s = String::from("Hello");
let s2 = &s;
let s3 = &mut s;
s3.push_str(" World");
println!("s2: {s2}");无法通过编译,示意图如下:

如果允许可变和不可变借用的同时存在,一旦可变借用修改了数据,不可变借用仍以为自己指向的数据是旧数据,导致产生未定义行为。
Box
在 Rust 中,Box<T> 是一个 智能指针,用于在 堆上存储数据,访问值时需要 * 解引用。
示例:
let x = Box::new(-1);
println!("x: {}", *x);输出:
x: -1Box 的所有权
Box 变量的所有权会发生移动,当发生移动后,该变量对堆上的数据不再拥有所有权,而是转移给了另一个变量,如下:
let f = Box::new(-1);
println!("f: {}", f);
let g = f;
println!("g: {}", g);
// println!("f: {}", f); // 此时发生报错,因为 -1 的所有权已经从 f 转移给了 g,f 不再可用输出:
g: -1Box 的引用
Box 的引用可以被多个变量同时使用,而不存在所有权问题:
let h = Box::new(-1);
let i = &h;
let j = &h;
println!("i: {}, j: {}", *i, *j);输出:
i: -1, j: -1