目录
第二章 所有权系统
2.1 什么是所有权
所有权(Ownership)是Rust最独特的特性,它使得Rust能够在没有垃圾回收器(GC)的情况下保证内存安全。理解所有权是理解Rust的关键。
栈与堆
在深入了解所有权之前,需要理解栈(Stack)和堆(Heap)的区别:
栈(Stack):
- 后进先出(LIFO)的数据结构
- 存储已知固定大小的数据
- 入栈和出栈速度快
- 数据必须拥有已知且固定的大小
堆(Heap):
- 用于存储编译时大小未知或可能变化的数据
- 在堆上分配内存称为分配(allocating)
- 访问堆数据比栈慢,需要指针寻址
- 灵活性更高,但管理更复杂
所有权规则:
1. Rust中的每个值都有一个所有者(owner) 2. 值在任一时刻有且只有一个所有者 3. 当所有者离开作用域,值将被丢弃
2.2 变量作用域
作用域(Scope)是程序中一个项在有效性的范围:
{ // s 在这里无效,它尚未声明 let s = "hello"; // s 从这里开始有效 // 使用 s println!("{}", s); } // 作用域结束,s 不再有效 // println!("{}", s); // 错误!s已失效
关键点:
- 当变量进入作用域,它就是有效的
- 一直有效直到离开作用域
- 作用域由大括号{}界定
2.3 String类型与内存分配
字符串字面量 vs String
let s = "hello"; // 字符串字面量,硬编码进程序,不可变 let mut s = String::from("hello"); // String类型,可变的 s.push_str(", world!"); // 追加字符串
为什么String可变而字符串字面量不可变?
- 字符串字面量:编译时已知内容,直接硬编码到最终可执行文件,快速高效
- String:在堆上分配,编译时大小未知,可以存储在运行时修改的文本
内存分配方式
String的内存布局:
let s = String::from("hello");
内存中的表示:
- 指向堆上内容的指针
- 长度(当前内容使用的字节数)
- 容量(从分配器获得的内存总量)
2.4 所有权转移(Move)
变量与数据交互的方式
1. 移动(Move):
let s1 = String::from("hello"); let s2 = s1; // s1的所有权移动到s2 // println!("{}", s1); // 错误!s1不再有效 println!("{}", s2); // 正确
当s1赋值给s2时,String的数据被复制了,包括:
- 指向堆数据的指针
- 长度
- 容量
但堆上的数据没有被复制!为了避免双重释放(double free)错误,Rust让s1失效。
图示:
赋值前:
s1 +--------+ +---------+ | 指针 | ---> | hello | | 长度 5 | +---------+ | 容量 5 | +--------+
赋值后(s1失效):
s2 +--------+ +---------+ | 指针 | ---> | hello | | 长度 5 | +---------+ | 容量 5 | +--------+
克隆(Clone)
如果需要深拷贝堆数据,使用clone方法:
let s1 = String::from("hello"); let s2 = s1.clone(); // 深拷贝 println!("s1 = {}, s2 = {}", s1, s2); // 都有效
注意:clone操作可能很昂贵,Rust通过显式调用来表明这一点。
只在栈上的数据:Copy trait
某些类型在赋值后仍然可用,因为它们实现了Copy trait:
let x = 5; let y = x; // Copy,不是Move println!("x = {}, y = {}", x, y); // 都有效
实现Copy trait的类型:
- 所有整数类型(i32, u64等)
- 布尔类型(bool)
- 浮点类型(f32, f64)
- 字符类型(char)
- 仅包含Copy类型的元组(如(i32, i32),但(i32, String)不行)
2.5 所有权与函数
将值传递给函数
fn main() { let s = String::from("hello"); // s进入作用域 takes_ownership(s); // s的值移动到函数里 // println!("{}", s); // 错误!s不再有效 let x = 5; // x进入作用域 makes_copy(x); // x被Copy到函数里 println!("x = {}", x); // 正确!x仍然有效 } fn takes_ownership(some_string: String) { println!("{}", some_string); } // some_string离开作用域,调用drop,内存释放 fn makes_copy(some_integer: i32) { println!("{}", some_integer); } // some_integer离开作用域,无需特殊处理
返回值与作用域
fn main() { let s1 = gives_ownership(); // gives_ownership将返回值转移给s1 let s2 = String::from("hello"); // s2进入作用域 let s3 = takes_and_gives_back(s2); // s2被移动到函数,返回值给s3 println!("s1 = {}, s3 = {}", s1, s3); } // s3离开作用域被丢弃,s2已移动(不发生),s1被丢弃 fn gives_ownership() -> String { let some_string = String::from("yours"); some_string // 返回并转移所有权 } fn takes_and_gives_back(a_string: String) -> String { a_string // 返回并转移所有权 }
2.6 引用与借用
什么是引用
为了避免所有权转移带来的不便,可以使用引用:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // 传递引用,不转移所有权 println!("'{}'的长度是{}", s1, len); // s1仍然有效 } fn calculate_length(s: &String) -> usize { s.len() } // s离开作用域,但它没有所有权,所以不释放内存
借用(Borrowing):将创建一个引用的行为称为借用。
引用与指针的区别:
- 引用总是有效的(编译器保证)
- 不能有空引用
- 不能有多重引用导致的问题
可变引用
默认情况下引用是不可变的,需要可变引用才能修改:
fn main() { let mut s = String::from("hello"); change(&mut s); println!("{}", s); // "hello, world" } fn change(some_string: &mut String) { some_string.push_str(", world"); }
可变引用的限制:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // 错误!不能同时有两个可变引用 println!("{}, {}", r1, r2);
好处: 在编译期防止数据竞争(data race)。
数据竞争条件:
1. 两个或更多指针同时访问同一数据 2. 至少有一个用于写入 3. 没有同步访问机制
混合引用规则
let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 let r3 = &mut s; // 大问题!
规则总结:
- 可以有任意数量的不可变引用
- 或者只有一个可变引用
- 不能同时存在可变和不可变引用
2.7 悬垂引用(Dangling References)
Rust编译器保证引用永远不会悬垂:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s // 错误!s将在函数结束时被释放 } // s离开作用域,内存被释放
编译器错误信息:
error[E0106]: missing lifetime specifier
解决方案:返回所有权
fn no_dangle() -> String { let s = String::from("hello"); s // 转移所有权 }
2.8 Slice类型
字符串Slice
字符串slice是String中一部分值的引用:
let s = String::from("hello world"); let hello = &s[0..5]; // 从0开始到5(不包括5) let world = &s[6..11]; // 从6开始到11(不包括11)
语法:
- [开始索引..结束索引]:从开始到结束(不包括结束)
- [..2]:从0开始到2
- [3..]:从3开始到结尾
- [..]:整个字符串
注意: slice的索引必须在有效的UTF-8字符边界。
字符串字面量就是slice
let s = "Hello, world!"; // &str类型
字符串字面量被直接存储在二进制文件中,&str是指向该位置的slice。
数组Slice
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // &[i32]类型,包含[2, 3]
2.9 生命周期(初步介绍)
生命周期是Rust的另一个重要概念,将在第十四章深入讲解。这里简要介绍其必要性:
fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
这段代码会报错,因为编译器不知道返回的引用与哪个参数相关。需要使用生命周期标注:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
练习题
练习题2.1:理解所有权转移
预测以下代码的输出:
fn main() { let s = String::from("hello"); let mut s2 = s; s2.push_str(" world"); println!("{}", s2); // println!("{}", s); // 能编译吗? }
练习题2.2:借用练习
修复以下代码中的错误:
fn main() { let s = String::from("hello"); let r1 = &s; let r2 = &mut s; // 错误! println!("{}, {}", r1, r2); }
修复方案:
fn main() { let mut s = String::from("hello"); { let r1 = &s; println!("{}", r1); } // r1在这里离开作用域 let r2 = &mut s; // 现在可以了 println!("{}", r2); }
练习题2.3:实现第一个单词提取
编写函数返回字符串中的第一个单词:
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); let word = first_word(&my_string); println!("第一个单词是:{}", word); }
练习题2.4:计算字符串字节长度
fn string_length(s: &String) -> usize { s.len() } fn main() { let s = String::from("hello"); let len = string_length(&s); println!("长度:{}", len); }
本章小结
本章深入讲解了Rust的所有权系统:
- 所有权规则:每个值有且只有一个所有者,所有者离开作用域值被丢弃
- Move语义:赋值时所有权转移,原变量失效
- Copy trait:基本类型实现Copy,赋值时复制而非移动
- 借用:通过引用访问数据而不获取所有权
- 可变引用:同一作用域只能有一个可变引用
- 生命周期:编译器通过生命周期确保引用总是有效
所有权系统是Rust内存管理的核心,也是Rust能够提供内存安全保证的基础。掌握所有权系统是成为Rust程序员的关键一步。
在下一章中,我们将详细学习Rust的基本数据类型。
