Skip to content

生命周期基础

> 理解生命周期的本质,掌握防止悬垂引用的核心机制,学会通过内存布局分析生命周期问题。

为什么需要生命周期?

生命周期语法

概念名称: 生命周期标注确保引用有效范围,防止悬垂引用。

语法结构:
┌──────────────────────────────────────┐
│  fn 函数名<'a>(参数: &'a 类型) -> &'a 类型│
│        ↑       ↑           ↑          │
│        生命周期参数  借用标注  返回值标注 │
│                                       │
│  fn longest<'a>(x: &'a str, y: &'a str)│
│              -> &'a str               │
│                                       │
│  结构体生命周期:                      │
│  struct Foo<'a> { field: &'a str }   │
│                                       │
│  简写:'a, 'b, 'static                │
└──────────────────────────────────────┘

最简示例

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = "hello";
    let s2 = "world";
    println!("{}", longest(s1, s2));
}
▶ Run

从实际问题出发:悬垂引用

rust
// ❌ 错误示例:返回局部变量的引用
fn get_reference() -> &i32 {
    let x = 5;      // x 在栈上分配
    &x              // 返回 x 的引用
}                   // x 在这里被释放!

// 编译器错误:
// error[E0106]: missing lifetime specifier
//  --> src/main.rs:1:23
//   |
// 1 | fn get_reference() -> &i32 {
//   |                       ^ expected named lifetime parameter
//   |
//   = help: this function's return type contains a borrowed value,
//           but there is no value for it to be borrowed from
▶ Run

内存布局分析:

函数执行前:            函数执行中:            函数返回后:
┌──────────┐          ┌──────────┐          ┌──────────┐
│   栈帧    │          │   栈帧    │          │   栈帧    │
│          │          │ ┌────────┐│          │          │
│          │          │ │ x = 5  ││          │          │
│          │          │ └────────┘│          │          │
└──────────┘          └──────────┘          └──────────┘
                      返回 &x ──────>       ❌ x 已释放!
                      引用指向已释放内存

生命周期的本质

┌─────────────────────────────────────────────────────────┐
│                   生命周期的本质                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  定义:引用有效的代码范围                                 │
│                                                         │
│  编译器的职责:                                           │
│  ├── 检查所有引用是否在其生命周期内有效                   │
│  ├── 确保没有悬垂引用                                    │
│  └── 编译时验证,零运行时开销                            │
│                                                         │
│  与其他语言对比:                                         │
│  ├── C/C++:手动管理,use-after-free 漏洞常见           │
│  ├── Java/Python:垃圾回收器,运行时开销                 │
│  └── Rust:编译器生命周期检查,安全且高效                │
│                                                         │
│  防止的错误类型:                                         │
│  ├── Use-after-free:访问已释放的内存                   │
│  ├── Dangling pointer:指针指向无效地址                 │
│  └── Double free:重复释放                              │
│                                                         │
└─────────────────────────────────────────────────────────┘

完整错误案例分析

案例 1:返回局部变量引用

rust
// ❌ 错误:返回栈上数据的引用
fn bad_return() -> &String {
    let s = String::from("hello");
    &s  // 错误:s 在函数结束时被释放
}

// 编译器错误:
// error[E0515]: cannot return reference to local variable `s`
//  --> src/main.rs:2:5
//   |
// 2 |     &s
//   |     ^^ returns a reference to data owned by the current function
//   |
// note: `s` is borrowed here
//  --> src/main.rs:1:9
//   |
// 1 |     let s = String::from("hello");
//   |         - binding `s` declared here

// ✅ 正确方案 1:返回所有权
fn good_return_owned() -> String {
    let s = String::from("hello");
    s  // 所有权转移给调用者
}

// ✅ 正确方案 2:接收输入引用
fn good_return_input(s: &String) -> &String {
    s  // 返回输入的生命周期
}

// ✅ 正确方案 3:使用 'static(仅适用于常量数据)
fn good_return_static() -> &'static str {
    "hello"  // 字符串字面量存储在静态区
}
▶ Run

案例 2:引用生命周期不匹配

rust
fn main() {
    let r;              // ---------+-- 'r
                        //          |
    {                   //          |
        let x = 5;      // -+-- 'x  |
        r = &x;         //  |       |
    }                   // -+-------+-- x 被释放

    println!("{}", r);  //          +-- ❌ 使用已释放的引用
}

// 编译器错误:
// error[E0597]: `x` does not live long enough
//  --> src/main.rs:6:13
//   |
// 5 |         let x = 5;
//   |             - binding `x` declared here
// 6 |         r = &x;
//   |             ^^ borrowed value does not live long enough
// 7 |     }
//   |     - `x` dropped here while still borrowed
// 8 | 
// 9 |     println!("{}", r);
//   |                    - borrow later used here
▶ Run

内存布局图解:

时间线:
  ┌───────┬───────┬───────┬───────┬───────┐
  │ let r │ let x │ r=&x  │  }    │println│
  └───────┴───────┴───────┴───────┴───────┘
    t0      t1      t2      t3      t4

栈内存变化:

t0: let r;
┌──────────────────┐
│ r = 未初始化      │
└──────────────────┘

t1: let x = 5;
┌──────────────────┐
│ r = 未初始化      │
│ x = 5            │
└──────────────────┘

t2: r = &x;
┌──────────────────┐
│ r ────┐           │
│       ↓           │
│ x = 5 │           │  ✅ r 指向 x
└───────┴──────────┘

t3: } (x 离开作用域)
┌──────────────────┐
│ r ────┐           │
│       ↓           │  ❌ x 已释放
│ [已释放]          │     但 r 仍指向它!
└──────────────────┘

t4: println!("{}", r);  // ❌ 访问无效内存!

生命周期与借用规则的关系

rust
fn main() {
    let mut data = String::from("hello");

    // 规则 1:多个不可变借用可以共存
    let r1 = &data;      // 不可变借用开始
    let r2 = &data;      // 另一个不可变借用
    println!("{} {}", r1, r2);  // ✅ 可以同时使用

    // 规则 2:不可变借用存在时,不能可变借用
    // data.push_str(" world");  // ❌ 错误:已有不可变借用
    
    // r1, r2 生命周期结束
    drop(r1);
    drop(r2);  // 现在没有借用了

    // 规则 3:可变借用独占访问
    let r3 = &mut data;   // 可变借用开始
    r3.push_str(" world"); // ✅ 可以修改
    
    // 规则 4:可变借用存在时,不能有任何其他借用
    // let r4 = &data;     // ❌ 错误:已有可变借用
    // println!("{}", r3); // 最后一次使用 r3
    
    // r3 生命周期结束
    println!("{}", data);  // ✅ 可以再次不可变借用
}
▶ Run

借用规则内存图解:

时间线与借用状态:

时刻 1: let r1 = &data;
┌─────────────────┐
│ data: "hello"   │
│ r1 ────────┐    │
│            ↓    │
└────────────────┘
借用状态:不可变借用 × 1

时刻 2: let r2 = &data;
┌─────────────────┐
│ data: "hello"   │
│ r1 ────────┐    │
│            ↓    │
│ r2 ────────┼─┐  │
│            ↓ ↓  │
└────────────────┘
借用状态:不可变借用 × 2 ✅ 允许

时刻 3: data.push_str(...); // ❌ 被拒绝
┌─────────────────┐
│ data: "hello"   │ ← 尝试可变借用
│ r1, r2 仍活跃   │ ← 但不可变借用仍存在
└────────────────┘
错误:cannot borrow `data` as mutable because it is also borrowed as immutable

时刻 4: r1, r2 生命周期结束,let r3 = &mut data;
┌─────────────────┐
│ data: "hello    │
│ world"          │
│ r3 ────────┐    │
│            ↓    │
└────────────────┘
借用状态:可变借用 × 1 ✅ 允许(独占访问)

小结

  • 生命周期确保引用在其指向的数据有效期内有效
  • 编译器检查所有引用,防止悬垂引用和 use-after-free
  • 生命周期是编译时验证,零运行时开销
  • 借用规则与生命周期配合,保证内存安全
  • 理解内存布局和错误信息是解决生命周期问题的关键

练习题

详见:练习题