借东西:引用与借用

原理

引用(Reference)在底层是裸指针(pointer)加编译期约束。&T 编译后就是一个 *const T&mut T*mut T,零运行时开销。但借用检查器(borrow checker)为它们加了严格的别名规则。

核心规则来自编译器 NLL(Non-Lexical Lifetimes)分析:

  • 任意时刻,要么一个可变引用(&mut),要么多个不可变引用(&),不能共存
  • 引用的生命周期不能超过被引用数据的生命周期

这条规则的根本目的是防止数据竞争和悬垂指针。LLVM 的 noalias 属性由此获得保证,启用大量优化。违反规则导致编译错误,而非运行时崩溃。

&str 是宽指针(2*usize: ptr + len),指向 UTF-8 字节序列。&String 通过 Deref trait 自动转型为 &str。两者均不拥有数据。

C: 引用与指针对比
C: 指针


语法

创建引用

let s = String::from("hello");
let r = &s;          // 不可变引用
let mut m = s;
let rm = &mut m;     // 可变引用

解引用

let x = 5;
let r = &x;
assert_eq!(5, *r);   // * 显式解引用
println!("{}", r);   // 打印自动解引用

借用规则

规则含义
多个 &T可同时存在(共享借用)
单一 &mut T唯一可变引用(独占借用)
&T&mut T不能共存
let mut s = String::from("hi");
let r1 = &s;
let r2 = &s;        // 合法:多个不可变引用
// let r3 = &mut s; // 错误:已有不可变引用存在
println!("{} {}", r1, r2);
let r3 = &mut s;    // 合法:r1 和 r2 已不再使用

函数参数中的引用

fn len(s: &String) -> usize { s.len() }       // 只读借用
fn append(s: &mut String) { s.push_str("!"); } // 可变借用
 
let mut s = String::from("hello");
len(&s);
append(&mut s);

悬垂引用(编译器阻止)

// fn dangle() -> &String {
//     let s = String::from("hi");
//     &s  // 错误:s 被释放后引用悬空
// }

String vs &str

类型所有权可变大小
String拥有数据24B(ptr+len+cap)
&str借用16B(ptr+len)
&String借用8B(单指针)→ Deref 为 &str

实践

力扣问题

力扣: 力扣字符串处理题 — 涉及字符串切片

fn main() {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();
    let mut s = String::from(input.trim());
    // 操作 &s[..] 和 &mut s
}

AI 自检

  1. &mut 为何不能与 & 共存?从 CPU 缓存一致性和 LLVM noalias 优化角度解释。
  2. &str&String 在栈上的字节数分别是多少?为什么后者可通过 Deref 自动转换?