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
▶ Runfn 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 // 返回所有权
}运行结果
=== 函数参数所有权转移 ===
调用前: 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
▶ Runfn 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);
}运行结果
=== Vec 中的所有权 ===
Vec 内容: ["hello", "world", "rust"]
取出: hello
剩余: ["world", "rust"]
=== HashMap 中的所有权 ===
HashMap: {"name": "Rust"}
获取值(借用): Rust
=== 结构体中的所有权 ===
用户名: Alice
邮箱: alice@example.com案例 3:字符串处理管道
问题描述
构建一个字符串处理管道,所有权在函数间传递。
代码实现
rust
▶ Runfn 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()
}运行结果
原始: " 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
▶ Runfn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 移动到 s2
println!("{}", s1); // ❌ 错误:s1 已无效
}完整编译错误信息
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
▶ Runfn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 克隆
println!("s1 = {}", s1); // ✅ 正确
println!("s2 = {}", s2); // ✅ 正确
}错误 2:函数调用后使用变量
错误代码
rust
▶ Runfn process(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
process(s); // s 移动到函数
println!("{}", s); // ❌ 错误:s 已移动
}完整编译错误信息
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
▶ Runfn process(s: &String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
process(&s); // 借用
println!("{}", s); // ✅ 正确
}修复方法 2:返回所有权
rust
▶ Runfn process(s: String) -> String {
println!("{}", s);
s // 返回所有权
}
fn main() {
let s = String::from("hello");
let s = process(s); // 获取返回的所有权
println!("{}", s); // ✅ 正确
}错误 3:部分移动
错误代码
rust
▶ Runfn main() {
let s = String::from("hello");
let vec = vec![s]; // s 移动到 Vec
println!("{}", s); // ❌ 错误:s 已移动
}修复方法
rust
▶ Runfn main() {
let s = String::from("hello");
let vec = vec![s.clone()]; // 克隆
println!("{}", s); // ✅ 正确
println!("vec = {:?}", vec);
}性能分析
移动 vs 克隆的性能基准测试
rust
▶ Runuse 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");
}运行结果示例
=== 性能基准测试 ===
移动耗时: 1ns
克隆耗时: 500μs
性能差异: 500000 倍
=== 内存使用分析 ===
移动后:
• 堆内存:1 个 1MB 字符串
• 栈内存:24 字节(指针结构)
克隆后:
• 堆内存:2 个 1MB 字符串
• 栈内存:48 字节(两个指针结构)
• 总内存:2MB + 48B最佳实践
✅ 推荐做法
- 默认使用移动:不需要保留原值时,让所有权自动转移
- 函数参数优先引用:函数不需要所有权时使用
&String或&str - 返回所有权显式标注:返回值明确所有权转移
- 大数据谨慎克隆:克隆大数据会显著影响性能
❌ 避免做法
- 不要过度克隆:大量克隆会导致性能问题
- 不要忽略所有权:理解所有权转移避免编译错误
- 不要在循环中移动:循环中移动会导致第一次后就失败
选择建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 函数参数(只读) | &str | 更通用,接受 String 和字面量 |
| 结构体字段 | String | 拥有所有权,生命周期独立 |
| 字符串字面量 | &'static str | 编译时已知,零成本 |
| 需要修改 | String | 可增长、可修改 |
| 函数返回 | String | 转移所有权给调用者 |
| 临时使用 | &str 或 &String | 借用,避免所有权转移 |
小结
本章核心知识点
| 概念 | 关键字/语法 | 核心要点 |
|---|---|---|
| String | String::from() | 堆分配,24字节栈结构 |
| &str | "literal", &s[0..n] | 借用,16字节栈结构 |
| 移动 | let s2 = s1; | 转移所有权,不复制堆数据 |
| 克隆 | s.clone() | 完整复制,包含堆数据 |
学习检查清单
- [ ] 理解 String 的内存布局(ptr、len、capacity)
- [ ] 掌握 String 与 &str 的区别
- [ ] 理解移动语义的工作原理
- [ ] 知道何时使用克隆
- [ ] 能够分析所有权转移的性能影响
下一章
下一章将学习函数中的所有权传递和 Copy trait。
➡️ 函数与 Copy