Skip to content

String 类型与移动语义

> 理解 String 的内存布局和所有权转移机制

本章目标

完成本章学习后,你将能够:

  • 理解 String 与 &str 的区别和内存布局
  • 掌握移动语义(Move)的工作原理
  • 理解克隆(Clone)与移动的区别
  • 分析所有权转移的性能影响

核心概念

String 类型详解

String 的内存布局

String 是 Rust 中最重要的类型之一,理解它的内存布局是掌握所有权的关键。

┌─────────────────────────────────────────────────────┐
│              String 内存布局详解                     │
├─────────────────────────────────────────────────────┤
│                                                     │
│  String 结构体(栈上,24 字节):                    │
│  ┌────────────────────────────────┐               │
│  │ 字段名       │ 大小    │ 说明   │               │
│  ├──────────────┼────────┼────────┤               │
│  │ ptr          │ 8 字节 │ 指向堆数据│             │
│  │ length       │ 8 字节 │ 已使用长度│             │
│  │ capacity     │ 8 字节 │ 总容量   │              │
│  └────────────────────────────────┘               │
│                                                     │
│  示例:let s = String::from("hello");              │
│                                                     │
│  栈内存:                                            │
│  ┌────────────────┬────────────────┐               │
│  │ ptr            │ 0x1000 ────────┼──┐            │
│  │ length         │ 5              │  │            │
│  │ capacity       │ 5              │  │            │
│  └────────────────┴────────────────┘  │            │
│                                        ▼            │
│  堆内存:                               │            │
│  ┌─────┬─────┬─────┬─────┬─────┐      │            │
│  │ 'h' │ 'e' │ 'l' │ 'l' │ 'o' │◀─────┘            │
│  └─────┴─────┴─────┴─────┴─────┘                   │
│  地址:0x1000                       │              │
│                                                     │
│  总大小:栈 24 字节 + 堆 5 字节                     │
│                                                     │
└─────────────────────────────────────────────────────┘

String 的三个字段详解

┌─────────────────────────────────────────────────────┐
│              String 三字段详解                       │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ptr(指针):                                       │
│  • 类型:*mut u8                                    │
│  • 作用:指向堆上存储的字符串数据                    │
│  • 地址:堆内存的起始位置                            │
│                                                     │
│  length(长度):                                    │
│  • 类型:usize                                      │
│  • 作用:当前字符串的长度(字节数)                  │
│  • 示例:"hello" 的 length = 5                      │
│                                                     │
│  capacity(容量):                                  │
│  • 类型:usize                                      │
│  • 作用:已分配的堆内存容量(字节数)                │
│  • 含义:可以存储的最大字节数,无需重新分配          │
│                                                     │
│  重要关系:                                          │
│  • length ≤ capacity                               │
│  • length 表示实际使用                              │
│  • capacity 表示预留空间                            │
│                                                     │
└─────────────────────────────────────────────────────┘

String 的容量增长策略

┌─────────────────────────────────────────────────────┐
│              String 容量增长策略                     │
├─────────────────────────────────────────────────────┤
│                                                     │
│  创建空字符串:                                      │
│  let mut s = String::new();                        │
│  length = 0, capacity = 0                          │
│                                                     │
│  添加字符后:                                        │
│  s.push('a');                                       │
│  length = 1, capacity = 1                          │
│                                                     │
│  继续添加:                                          │
│  s.push('b');                                       │
│  length = 2, capacity = 2                          │
│                                                     │
│  再次添加(触发重新分配):                          │
│  s.push('c');                                       │
│  length = 3, capacity = 4                          │
│  (分配策略:容量翻倍或按需增长)                    │
│                                                     │
│  push_str 后:                                       │
│  s.push_str("defg");                               │
│  length = 7, capacity = 8                          │
│                                                     │
│  堆内存变化可视化:                                  │
│  ┌─────────────────────────────────────────┐       │
│  │ 时刻 1: [a]                             │       │
│  │         len=1, cap=1                    │       │
│  ├─────────────────────────────────────────┤       │
│  │ 时刻 2: [a|b]                           │       │
│  │         len=2, cap=2                    │       │
│  ├─────────────────────────────────────────┤       │
│  │ 时刻 3: [a|b|c|?]                       │       │
│  │         len=3, cap=4                    │       │
│  │         (重新分配,旧数据复制到新位置)│       │
│  ├─────────────────────────────────────────┤       │
│  │ 时刻 4: [a|b|c|d|e|f|g|?]               │       │
│  │         len=7, cap=8                    │       │
│  └─────────────────────────────────────────┘       │
│                                                     │
└─────────────────────────────────────────────────────┘

String vs &str 详细对比

┌─────────────────────────────────────────────────────┐
│           String vs &str 详细对比                   │
├─────────────────────────────────────────────────────┤
│                                                     │
│  String(拥有所有权的字符串):                      │
│  ├── 可增长、可修改                                  │
│  ├── 数据存储在堆上                                  │
│  ├── 大小:24 字节(ptr + len + cap)               │
│  ├── 创建方式:                                      │
│  │   • String::from("hello")                       │
│  │   • "hello".to_string()                         │
│  │   • "hello".to_owned()                          │
│  │   • String::new() + push_str()                  │
│  ├── 所有权:拥有堆数据                              │
│  ├── 生命周期:跟随所有者                            │
│  └── 移动时:转移所有权                              │
│                                                     │
│  &str(字符串切片/借用):                           │
│  ├── 不可变(通常)                                  │
│  ├── 借用数据,不拥有所有权                          │
│  ├── 大小:16 字节(ptr + len)                     │
│  ├── 创建方式:                                      │
│  │   • "hello"(字符串字面量)                      │
│  │   • &String(从 String 借用)                    │
│  │   • &s[0..5](切片)                             │
│  ├── 所有权:借用,不拥有                            │
│  ├── 生命周期:依赖借用来源                          │
│  └── 移动时:只复制指针(16 字节)                   │
│                                                     │
└─────────────────────────────────────────────────────┘

内存布局对比

String 内存布局:
┌─────────────────────────────────────────┐
│           栈(Stack)                    │
│  ┌───────────┬───────────┬─────────┐   │
│  │   ptr     │   len     │  cap    │   │
│  │ (8 bytes) │ (8 bytes) │(8 bytes)│   │
│  └─────┬─────┴───────────┴─────────┘   │
│        │                                │
│        ▼                                │
│  ┌─────────────────────────────────┐   │
│  │       堆(Heap)                 │   │
│  │  [h|e|l|l|o]                    │   │
│  │  可以增长、修改                 │   │
│  │  需要手动释放或所有权自动释放   │   │
│  └─────────────────────────────────┘   │
│                                         │
│  总大小:栈 24B + 堆 lenB               │
└─────────────────────────────────────────┘

&str 内存布局(字符串切片):
┌─────────────────────────────────────────┐
│           栈(Stack)                    │
│  ┌───────────────┬───────────────┐      │
│  │    ptr        │     len       │      │
│  │  (8 bytes)    │  (8 bytes)    │      │
│  └───────┬───────┴───────────────┘      │
│          │                               │
│          ▼                               │
│  ┌─────────────────────────────────┐    │
│  │       借用的数据                 │    │
│  │  可能位置:                     │    │
│  │  • String 的堆数据              │    │
│  │  • 静态区(字面量)              │    │
│  │  • 其他内存位置                 │    │
│  │  不可修改                       │    │
│  └─────────────────────────────────┘    │
│                                         │
│  总大小:栈 16B                          │
└─────────────────────────────────────────┘

字符串字面量 &str:
┌─────────────────────────────────────────┐
│           栈(Stack)                    │
│  ┌───────────────┬───────────────┐      │
│  │    ptr        │     len       │      │
│  │  (指向静态区) │    (5)        │      │
│  └───────┬───────┴───────────────┘      │
│          │                               │
│          ▼                               │
│  ┌─────────────────────────────────┐    │
│  │       静态区(编译时分配)       │    │
│  │  "hello\0"                      │    │
│  │  整个程序运行期间存在           │    │
│  │  类型:&'static str             │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

移动语义(Move)

什么是移动?

移动是指所有权从一个变量转移到另一个变量。对于堆分配的类型(如 String),移动只是转移所有权指针,不复制堆数据。

┌─────────────────────────────────────────────────────┐
│              移动语义详解                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  移动的本质:                                        │
│  • 转移所有权,不复制数据                            │
│  • 栈上的指针被复制(24 字节)                      │
│  • 堆上的数据不动                                    │
│  • 原变量失效                                        │
│                                                     │
│  为什么选择移动?                                    │
│  • 性能:避免昂贵的堆数据复制                        │
│  • 安全:防止双重释放                               │
│  • 明确:所有权关系清晰                              │
│                                                     │
└─────────────────────────────────────────────────────┘

移动的内存变化过程(Step-by-Step)

步骤 0:创建 String
let s1 = String::from("hello");

┌─────────────────────────────────────┐
│ 栈                    堆            │
│ ┌──────────────┐   ┌─────────┐    │
│ │ s1           │   │ "hello" │    │
│ │ ┌──────────┐ │   │ [h|e|l|l│    │
│ │ │ ptr ─────┼─┼──▶│  |o]    │    │
│ │ │ len = 5  │ │   └─────────┘    │
│ │ │ cap = 5  │ │                   │
│ │ └──────────┘ │                   │
│ └──────────────┘                   │
│                                     │
│ s1 是唯一所有者                     │
└─────────────────────────────────────┘

步骤 1:赋值开始
let s2 = s1;

编译器开始处理赋值操作
• 检查 s1 类型:String(堆分配)
• 检查 String 是否实现 Copy:否
• 决策:移动所有权

┌─────────────────────────────────────┐
│ 正在处理赋值...                      │
│                                     │
│ 操作:复制栈上的指针                 │
│ 状态:准备标记 s1 无效               │
└─────────────────────────────────────┘

步骤 2:栈指针复制
s2 在栈上创建,复制 s1 的三个字段

┌─────────────────────────────────────┐
│ 栈                    堆            │
│ ┌──────────────┐   ┌─────────┐    │
│ │ s1           │   │ "hello" │    │
│ │ ┌──────────┐ │   │ [h|e|l|l│    │
│ │ │ ptr ─────┼─┼──▶│  |o]    │    │
│ │ │ len = 5  │ │   └─────────┘    │
│ │ │ cap = 5  │ │   ↑              │
│ │ └──────────┘ │   │              │
│ ├──────────────┤   │              │
│ │ s2           │   │              │
│ │ ┌──────────┐ │   │              │
│ │ │ ptr ─────┼─┼──┼─▶(相同地址)│
│ │ │ len = 5  │ │                   │
│ │ │ cap = 5  │ │                   │
│ │ └──────────┘ │                   │
│ └──────────────┘                   │
│                                     │
│ s1 和 s2 都指向同一堆数据           │
│ (临时状态)                         │
└─────────────────────────────────────┘

步骤 3:所有权转移完成
s1 被标记为无效

┌─────────────────────────────────────┐
│ 栈                    堆            │
│ ┌──────────────┐   ┌─────────┐    │
│ │ s1 (无效)    │   │ "hello" │    │
│ │ ┌──────────┐ │   │ [h|e|l|l│    │
│ │ │ ptr ─────┼─┼─x │  |o]    │    │
│ │ │ len = 5  │ │   └─────────┘    │
│ │ │ cap = 5  │ │   ↑              │
│ │ └──────────┘ │   │              │
│ ├──────────────┤   │              │
│ │ s2 (所有者) │   │              │
│ │ ┌──────────┐ │   │              │
│ │ │ ptr ─────┼─┼──┼─▶            │
│ │ │ len = 5  │ │                   │
│ │ │ cap = 5  │ │                   │
│ │ └──────────┘ │                   │
│ └──────────────┘                   │
│                                     │
│ ✅ s2 现在是唯一所有者               │
│ ❌ s1 不能再被使用                   │
│ 堆数据没有移动                       │
└─────────────────────────────────────┘

步骤 4:尝试使用 s1
println!("{}", s1);

┌─────────────────────────────────────┐
│ 编译器检测到使用无效变量             │
│                                     │
│ error[E0382]: use of moved value    │
│                                     │
│ 编译器阻止这个操作                   │
│ 防止潜在的内存安全问题               │
└─────────────────────────────────────┘

移动的性能分析

┌─────────────────────────────────────────────────────┐
│              移动 vs 复制的性能对比                  │
├─────────────────────────────────────────────────────┤
│                                                     │
│  移动(Move):                                      │
│  ┌────────────────────────────────────────────┐    │
│  │ 操作:复制栈上的 24 字节                     │    │
│  │ 成本:24 字节的栈复制                       │    │
│  │ 时间:~1 纳秒                               │    │
│  │ 堆数据:不动                                │    │
│  │ 结果:所有权转移                            │    │
│  └────────────────────────────────────────────┘    │
│                                                     │
│  深拷贝(Clone):                                   │
│  ┌────────────────────────────────────────────┐    │
│  │ 操作:复制栈 24 字节 + 堆数据               │    │
│  │ 成本:24 字节栈 + N 字节堆                  │    │
│  │ 时间:取决于堆数据大小                      │    │
│  │ 堆数据:完整复制                            │    │
│  │ 结果:两个独立副本                          │    │
│  └────────────────────────────────────────────┘    │
│                                                     │
│  示例对比:                                          │
│  let s1 = String::from("hello world!");           │
│                                                     │
│  移动:let s2 = s1;                                 │
│  • 复制:24 字节                                    │
│  • 时间:~1 纳秒                                    │
│                                                     │
│  克隆:let s2 = s1.clone();                        │
│  • 复制:24 字节栈 + 12 字节堆                     │
│  • 时间:~100 纳秒(包含堆分配)                   │
│  • 差异:约 100 倍                                  │
│                                                     │
│  大数据示例:                                        │
│  let large = String::from("1MB 数据...");         │
│                                                     │
│  移动:let s2 = large;                              │
│  • 复制:24 字节                                    │
│  • 时间:~1 纳秒                                    │
│                                                     │
│  克隆:let s2 = large.clone();                     │
│  • 复制:24 字节栈 + 1MB 堆                        │
│  • 时间:~1 毫秒                                    │
│  • 差异:约 1000 倍                                 │
│                                                     │
└─────────────────────────────────────────────────────┘

克隆(Clone)

什么是克隆?

克隆创建数据的完整副本,包括堆数据。

┌─────────────────────────────────────────────────────┐
│              克隆语义详解                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  克隆的本质:                                        │
│  • 创建完全独立的新副本                              │
│  • 复制栈上的所有字段                                │
│  • 复制堆上的所有数据                                │
│  • 原变量仍然有效                                    │
│                                                     │
│  克隆的特点:                                        │
│  • 显式调用:必须调用 .clone()                      │
│  • 性能开销:复制堆数据的成本                        │
│  • 内存占用:额外的堆内存                            │
│  • 独立所有权:两个变量各自管理                      │
│                                                     │
└─────────────────────────────────────────────────────┘

克隆的内存变化过程

步骤 0:创建 String
let s1 = String::from("hello");

┌─────────────────────────────────────┐
│ 栈                    堆            │
│ ┌──────────────┐   ┌─────────┐    │
│ │ s1           │   │ "hello" │    │
│ │ ptr = 0x1000┼──▶│ 地址A   │    │
│ │ len = 5     │   └─────────┘    │
│ │ cap = 5     │                   │
│ └──────────────┘                   │
└─────────────────────────────────────┘

步骤 1:调用 clone()
let s2 = s1.clone();

开始克隆过程:
• 分配新的堆内存
• 复制堆数据到新位置
• 创建新的栈结构体

┌─────────────────────────────────────┐
│ 正在克隆...                          │
│                                     │
│ 1. 分配堆内存(新地址 B)           │
│ 2. 复制数据:"hello" → 新位置       │
│ 3. 创建 s2,指向新地址              │
└─────────────────────────────────────┘

步骤 2:克隆完成

┌─────────────────────────────────────────────┐
│ 栈                    堆(两个独立副本)    │
│ ┌──────────────┐   ┌─────────┐             │
│ │ s1           │   │ "hello" │ (地址A)     │
│ │ ptr = 0x1000┼──▶│ [h|e|l|l│             │
│ │ len = 5     │   │   |o]   │             │
│ │ cap = 5     │   └─────────┘             │
│ ├──────────────┤   ┌─────────┐             │
│ │ s2           │   │ "hello" │ (地址B)     │
│ │ ptr = 0x2000┼──▶│ [h|e|l|l│             │
│ │ len = 5     │   │   |o]   │             │
│ │ cap = 5     │   └─────────┘             │
│ └──────────────┘                           │
│                                             │
│ ✅ s1 和 s2 都有效                           │
│ ✅ 各自拥有独立的堆数据                       │
│ ✅ 修改其中一个不影响另一个                   │
└─────────────────────────────────────────────┘

步骤 3:修改 s2
let mut s2 = s2;
s2.push_str(" world");

┌─────────────────────────────────────────────┐
│ 栈                    堆                    │
│ ┌──────────────┐   ┌─────────┐             │
│ │ s1           │   │ "hello" │             │
│ │ ptr = 0x1000┼──▶│ (不变)  │             │
│ │ len = 5     │   └─────────┘             │
│ │ cap = 5     │                           │
│ ├──────────────┤   ┌──────────────┐       │
│ │ s2           │   │ "hello world"│       │
│ │ ptr = 0x2000┼──▶│ [h|e|l|l|o| │       │
│ │ len = 11    │   │  w|o|r|l|d] │       │
│ │ cap = 11    │   └──────────────┘       │
│ └──────────────┘                           │
│                                             │
│ s1 保持不变                                 │
│ s2 独立修改                                 │
└─────────────────────────────────────────────┘

实战案例

案例 1:函数参数传递

问题描述

理解函数参数传递时的所有权转移。

代码实现

rust
fn main() {
    println!("=== 函数参数所有权转移 ===");
    
    let s = String::from("hello");
    println!("调用前: s = {}", s);
    
    process_string(s);  // s 的所有权移动到函数
    
    // println!("调用后: s = {}", s);  // ❌ 错误:s 已移动
    
    println!("\n=== 使用引用避免移动 ===");
    let s2 = String::from("world");
    println!("调用前: s2 = {}", s2);
    
    borrow_string(&s2);  // 借用,不移动所有权
    println!("调用后: s2 = {}", s2);  // ✅ 正确:s2 仍然有效
    
    println!("\n=== 返回所有权 ===");
    let s3 = String::from("rust");
    let s4 = transform_and_return(s3);
    println!("返回后: s4 = {}", s4);
}

fn process_string(s: String) {
    println!("函数内: s = {}", s);
}  // s 在这里被释放

fn borrow_string(s: &String) {
    println!("函数内(借用): s = {}", s);
}  // s 是引用,不会释放原数据

fn transform_and_return(s: String) -> String {
    let mut s = s;
    s.push_str(" is great!");
    s  // 返回所有权
}
▶ Run

运行结果

=== 函数参数所有权转移 ===
调用前: s = hello
函数内: s = hello

=== 使用引用避免移动 ===
调用前: s2 = world
函数内(借用): s = world
调用后: s2 = world

=== 返回所有权 ===
返回后: s4 = rust is great!

内存变化过程

函数参数所有权转移:
┌─────────────────────────────────────────────┐
│ main 函数栈              堆                  │
│ ┌──────────────┐   ┌─────────┐             │
│ │ s            │──▶│ "hello" │             │
│ └──────────────┘   └─────────┘             │
│                                             │
│ 调用 process_string(s):                    │
│ 所有权移动到函数参数                         │
│                                             │
│ process_string 函数栈       堆              │
│ ┌──────────────┐       ┌─────────┐         │
│ │ s (参数)     │──────▶│ "hello" │         │
│ └──────────────┘       └─────────┘         │
│                                             │
│ main 函数栈                                 │
│ ┌──────────────┐                           │
│ │ s (无效)     │                           │
│ └──────────────┘                           │
│                                             │
│ 函数结束:s 被释放,堆数据被释放             │
└─────────────────────────────────────────────┘

使用引用:
┌─────────────────────────────────────────────┐
│ main 函数栈              堆                  │
│ ┌──────────────┐   ┌─────────┐             │
│ │ s2           │──▶│ "world" │             │
│ └──────────────┘   └─────────┘             │
│                                             │
│ 调用 borrow_string(&s2):                   │
│ 只传递引用(指针)                           │
│                                             │
│ borrow_string 函数栈        堆              │
│ ┌──────────────┐       ┌─────────┐         │
│ │ s (&String) │──────▶│ "world" │         │
│ │ (借用)       │       │         │         │
│ └──────────────┘       └─────────┘         │
│                                             │
│ main 函数栈                                 │
│ ┌──────────────┐                           │
│ │ s2 (有效)    │──▶(仍然拥有)             │
│ └──────────────┘                           │
│                                             │
│ 函数结束:引用失效,但数据不释放             │
│ s2 继续有效                                 │
└─────────────────────────────────────────────┘

案例 2:集合类型中的所有权

问题描述

理解 Vec、HashMap 等集合类型中的所有权管理。

代码实现

rust
fn main() {
    println!("=== Vec 中的所有权 ===");
    
    let mut vec: Vec<String> = Vec::new();
    
    vec.push(String::from("hello"));  // 所有权移动到 Vec
    vec.push(String::from("world"));
    vec.push(String::from("rust"));
    
    println!("Vec 内容: {:?}", vec);
    
    // 取出元素
    let first = vec.remove(0);  // 移除并获取所有权
    println!("取出: {}", first);
    println!("剩余: {:?}", vec);
    
    println!("\n=== HashMap 中的所有权 ===");
    
    use std::collections::HashMap;
    
    let mut map = HashMap::new();
    
    let key = String::from("name");
    let value = String::from("Rust");
    
    map.insert(key, value);  // key 和 value 都移动到 HashMap
    
    // println!("key: {}", key);  // ❌ 错误:key 已移动
    // println!("value: {}", value);  // ❌ 错误:value 已移动
    
    println!("HashMap: {:?}", map);
    
    // 获取值
    if let Some(v) = map.get("name") {
        println!("获取值(借用): {}", v);
    }
    
    println!("\n=== 结构体中的所有权 ===");
    
    struct User {
        name: String,
        email: String,
    }
    
    let name = String::from("Alice");
    let email = String::from("alice@example.com");
    
    let user = User {
        name,  // name 移动到结构体
        email,  // email 移动到结构体
    };
    
    // println!("name: {}", name);  // ❌ 错误:name 已移动
    
    println!("用户名: {}", user.name);
    println!("邮箱: {}", user.email);
}
▶ Run

运行结果

=== Vec 中的所有权 ===
Vec 内容: ["hello", "world", "rust"]
取出: hello
剩余: ["world", "rust"]

=== HashMap 中的所有权 ===
HashMap: {"name": "Rust"}
获取值(借用): Rust

=== 结构体中的所有权 ===
用户名: Alice
邮箱: alice@example.com

案例 3:字符串处理管道

问题描述

构建一个字符串处理管道,所有权在函数间传递。

代码实现

rust
fn main() {
    let text = String::from("  hello, rust world!  ");
    
    println!("原始: \"{}\"", text);
    
    // 所有权管道
    let processed = trim_string(text);
    println!("修剪后: \"{}\"", processed);
    
    let processed = to_uppercase(processed);
    println!("大写后: \"{}\"", processed);
    
    let processed = add_prefix(processed);
    println!("添加前缀: \"{}\"", processed);
    
    let final_result = reverse_string(processed);
    println!("反转后: \"{}\"", final_result);
}

fn trim_string(s: String) -> String {
    s.trim().to_string()
}

fn to_uppercase(s: String) -> String {
    s.to_uppercase()
}

fn add_prefix(s: String) -> String {
    format!("PREFIX: {}", s)
}

fn reverse_string(s: String) -> String {
    s.chars().rev().collect()
}
▶ Run

运行结果

原始: "  hello, rust world!  "
修剪后: "hello, rust world!"
大写后: "HELLO, RUST WORLD!"
添加前缀: "PREFIX: HELLO, RUST WORLD!"
反转后: "!DLROW TSUR ,OLLEH :XERP"

所有权流转图

┌─────────────────────────────────────────────────────┐
│              字符串处理管道所有权流转                │
├─────────────────────────────────────────────────────┤
│                                                     │
│  main()                                             │
│    │                                                │
│    ├─▶ trim_string(text)                           │
│    │    • text 移动到函数                           │
│    │    • 函数返回新 String                         │
│    │    • processed 获得所有权                      │
│    │                                                │
│    ├─▶ to_uppercase(processed)                     │
│    │    • processed 移动到函数                      │
│    │    • 函数返回新 String                         │
│    │    • processed 获得新所有权                    │
│    │                                                │
│    ├─▶ add_prefix(processed)                       │
│    │    • processed 移动到函数                      │
│    │    • 函数返回新 String                         │
│    │    • processed 获得新所有权                    │
│    │                                                │
│    ├─▶ reverse_string(processed)                   │
│    │    • processed 移动到函数                      │
│    │    • 函数返回新 String                         │
│    │    • final_result 获得所有权                   │
│    │                                                │
│    ▼                                                │
│  final_result 在 main 结束时释放                   │
│                                                     │
│  特点:                                              │
│  • 每一步所有权明确传递                              │
│  • 无内存泄漏                                       │
│  • 无悬垂指针                                       │
│  • 编译器保证安全                                    │
│                                                     │
└─────────────────────────────────────────────────────┘

常见错误

错误 1:使用已移动的值

错误代码

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 移动到 s2
    
    println!("{}", s1);  // ❌ 错误:s1 已无效
}
▶ Run

完整编译错误信息

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
3 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value before moving it
  |
4 |     let s2 = s1.clone();
  |              ++++++++++++

修复方法

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 克隆
    
    println!("s1 = {}", s1);  // ✅ 正确
    println!("s2 = {}", s2);  // ✅ 正确
}
▶ Run

错误 2:函数调用后使用变量

错误代码

rust
fn process(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    process(s);  // s 移动到函数
    
    println!("{}", s);  // ❌ 错误:s 已移动
}
▶ Run

完整编译错误信息

error[E0382]: borrow of moved value: `s`
 --> src/main.rs:9:20
  |
8 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
9 |     process(s);
  |             - value moved here
10 |     println!("{}", s);
  |                    ^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

修复方法 1:使用引用

rust
fn process(s: &String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    process(&s);  // 借用
    
    println!("{}", s);  // ✅ 正确
}
▶ Run

修复方法 2:返回所有权

rust
fn process(s: String) -> String {
    println!("{}", s);
    s  // 返回所有权
}

fn main() {
    let s = String::from("hello");
    let s = process(s);  // 获取返回的所有权
    
    println!("{}", s);  // ✅ 正确
}
▶ Run

错误 3:部分移动

错误代码

rust
fn main() {
    let s = String::from("hello");
    let vec = vec![s];  // s 移动到 Vec
    
    println!("{}", s);  // ❌ 错误:s 已移动
}
▶ Run

修复方法

rust
fn main() {
    let s = String::from("hello");
    let vec = vec![s.clone()];  // 克隆
    
    println!("{}", s);  // ✅ 正确
    println!("vec = {:?}", vec);
}
▶ Run

性能分析

移动 vs 克隆的性能基准测试

rust
use std::time::Instant;

fn main() {
    println!("=== 性能基准测试 ===");
    
    // 创建大字符串
    let large_string = String::from("x").repeat(1_000_000);  // 1MB
    
    // 测试移动性能
    let start = Instant::now();
    let moved = large_string;  // 移动
    let move_duration = start.elapsed();
    
    // 测试克隆性能
    let start = Instant::now();
    let cloned = moved.clone();  // 克隆
    let clone_duration = start.elapsed();
    
    println!("移动耗时: {:?}", move_duration);
    println!("克隆耗时: {:?}", clone_duration);
    println!("性能差异: {} 倍", 
        clone_duration.as_nanos() / move_duration.as_nanos());
    
    println!("\n=== 内存使用分析 ===");
    println!("移动后:");
    println!("  • 堆内存:1 个 1MB 字符串");
    println!("  • 栈内存:24 字节(指针结构)");
    
    println!("克隆后:");
    println!("  • 堆内存:2 个 1MB 字符串");
    println!("  • 栈内存:48 字节(两个指针结构)");
    println!("  • 总内存:2MB + 48B");
}
▶ Run

运行结果示例

=== 性能基准测试 ===
移动耗时: 1ns
克隆耗时: 500μs
性能差异: 500000 倍

=== 内存使用分析 ===
移动后:
  • 堆内存:1 个 1MB 字符串
  • 栈内存:24 字节(指针结构)

克隆后:
  • 堆内存:2 个 1MB 字符串
  • 栈内存:48 字节(两个指针结构)
  • 总内存:2MB + 48B

最佳实践

✅ 推荐做法

  • 默认使用移动:不需要保留原值时,让所有权自动转移
  • 函数参数优先引用:函数不需要所有权时使用 &String&str
  • 返回所有权显式标注:返回值明确所有权转移
  • 大数据谨慎克隆:克隆大数据会显著影响性能

❌ 避免做法

  • 不要过度克隆:大量克隆会导致性能问题
  • 不要忽略所有权:理解所有权转移避免编译错误
  • 不要在循环中移动:循环中移动会导致第一次后就失败

选择建议

场景推荐原因
函数参数(只读)&str更通用,接受 String 和字面量
结构体字段String拥有所有权,生命周期独立
字符串字面量&'static str编译时已知,零成本
需要修改String可增长、可修改
函数返回String转移所有权给调用者
临时使用&str&String借用,避免所有权转移

小结

本章核心知识点

概念关键字/语法核心要点
StringString::from()堆分配,24字节栈结构
&str"literal", &s[0..n]借用,16字节栈结构
移动let s2 = s1;转移所有权,不复制堆数据
克隆s.clone()完整复制,包含堆数据

学习检查清单

  • [ ] 理解 String 的内存布局(ptr、len、capacity)
  • [ ] 掌握 String 与 &str 的区别
  • [ ] 理解移动语义的工作原理
  • [ ] 知道何时使用克隆
  • [ ] 能够分析所有权转移的性能影响

下一章

下一章将学习函数中的所有权传递和 Copy trait。

➡️ 函数与 Copy