19 KiB
Rust 所有权练习题
建议先手动写出每道题的答案,再运行代码验证。
一、基础题:判断正误
判断以下代码能否通过编译。如果可以,说明原因;如果不能,指出错误原因。
题目 1-1
fn main() {
let s = String::from("hello");
let t = s;
println!("{}", s);
}
答: 不能
题目 1-2
fn main() {
let x = 42;
let y = x;
println!("x = {}, y = {}", x, y);
}
答: 能
题目 1-3
fn main() {
let s = String::from("rust");
let t = s.clone();
println!("s = {}, t = {}", s, t);
}
答: 能
题目 1-4
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = &s1;
println!("{} {} {}", s1, s2, s3);
}
答: 能
题目 1-5
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
答: 不能
题目 1-6
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}, {}, {}", r1, r2, r3);
}
[批注] 原答"能"错误 —— 该代码无法通过编译。 r1、r2 是不可变引用,r3 是可变引用。Rust 不允许在已有不可变借用的作用域内再创建可变借用。
println!同时使用了三者,借用范围重叠,编译器报错:error[E0502]: cannot borrowsas mutable because it is also borrowed as immutable
二、填空题:补充代码
补全下列代码使其能通过编译。
题目 2-1:转移所有权
fn main() {
let s = String::from("rustacean");
takes_ownership(________); // 填空,使 s 的所有权进入函数
// println!("{}", s); // 此行若取消注释会报错
}
fn takes_ownership(s: String) {
println!("{}", s);
}
答: s
题目 2-2:借用的生命周期
fn main() {
let s = String::from("hello");
let len = get_length(________); // 填空:传递引用取得长度
println!("'{}' 的长度是 {}", s, len);
}
fn get_length(s: &String) -> usize {
s.len()
}
答: &s
题目 2-3:可变引用
fn main() {
let ________ s = String::from("hello"); // 填空:声明可变变量
append_world(________); // 填空:传递可变引用
println!("{}", s); // 期望输出:hello, world!
}
fn append_world(s: &mut String) {
s.push_str(", world!");
}
答: mut, &mut s
题目 2-4:作用域技巧
fn main() {
let mut s = String::from("hello");
{ // 进入新作用域
let r1 = ________; // 填空:创建不可变引用
println!("{}", r1);
} // r1 离开作用域
let r2 = ________; // 填空:现在可以创建可变引用了
r2.push_str(" world");
println!("{}", r2);
}
答: &s, &mut s
三、找出并修复错误
以下每段代码都有编译错误,请指出错误并写出修正后的代码。
题目 3-1
fn main() {
let s1 = String::from("hello");
let s2 = s1;
print_both(s1, s2);
}
fn print_both(a: String, b: String) {
println!("{} and {}", a, b);
}
[批注] 原错误分析有误: 错误原因是
let s2 = s1;将 s1 的所有权移动给了 s2,此后 s1 已失效,不能继续用于print_both(s1, s2)。原答写的修复代码语法也有问题(fn print_both(a: String, &b: String)不是合法签> 名)。正确修复:clone 一份。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
print_both(s1, s2);
}
fn print_both(a: String, b: String) {
println!("{} and {}", a, b);
}
题目 3-2
fn main() {
let s = dangle();
println!("{}", s);
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
[批注] 原错误分析不精确。 真正原因:
dangle()返回&String,但引用指向的是函数内的局部变量s。函数结束时s被释放,返回的引用变成悬垂引用(dangling reference)。Rust 编译器在编译期阻止了这种行为。修复方法:直接返回String让所有权移出。
fn main() {
let s = dangle();
println!("{}", s);
}
fn dangle() -> String {
let s = String::from("hello");
s
}
题目 3-3
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
let r2 = &mut s;
r2.push_str("!");
println!("{}", r1);
println!("{}", r2);
}
[批注] 原分析"同时只能存在一个可变引用"基本正确,但未给出修复代码。 由于 r1 在
println!("{}", r1)之后不再使用,Rust 的 NLL(Non-Lexical Lifetimes)可在该点结束 r1 的借用。修复:将 println! 移到 r2 创建之前。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1); // r1 在此之后不再使用,NLL 结束其借用
let r2 = &mut s; // 此时 r1 已失效,可以创建新的可变引用
r2.push_str("!");
println!("{}", r2);
}
题目 3-4
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
s.clear(); // 清空字符串
println!("first = {}", first);
}
[批注] 原分析不精确,原修复代码
let first = s[0..5];仍是切片引用,与s.clear()的&mut self冲突,同样无法编译。 真正原因:first是对s内容的不可变引用,而clear()需要&mut self(可变引用),两者不能同时存在。正确修复:先使用 first,再 clear。
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
println!("first = {}", first); // 先使用 first
let mut s = s; // 重新绑定为可变的
s.clear(); // 此时 first 的借用已结束
}
四、编程题
题目 4-1:计算单词数量
编写一个函数 count_words(s: &str) -> usize,计算字符串中的单词数量(以空格分隔)。
fn count_words(s: &str) -> usize {
// 你的代码
return s.split_whitespace().count();
}
fn main() {
let s = String::from("hello world rust");
let count = count_words(&s);
println!("'{}' 有 {} 个单词", s, count); // 期望输出:3
// 验证 s 的所有权未被取走
println!("原始字符串仍然可用: {}", s);
}
题目 4-2:获取首尾单词
编写函数 first_and_last(s: &str) -> (&str, &str),返回字符串的首单词和尾单词。
fn first_and_last(s: &str) -> (&str, &str) {
// 你的代码
let words: Vec<&str> = s.split_whitespace().collect();
return (words[0], words[words.len() - 1]);
}
fn main() {
let s = String::from("Rust is a systems programming language");
let (first, last) = first_and_last(&s);
println!("首单词: '{}', 尾单词: '{}'", first, last);
// 期望输出:首单词: 'Rust', 尾单词: 'language'
println!("原字符串: {}", s);
}
题目 4-3:字符串处理
编写一个函数,接收一个字符串,返回一个新的字符串,将其中的每个单词首字母大写,其余字母小写。
fn title_case(s: &str) -> String {
// 你的代码
// 提示:可以使用 split_whitespace、to_uppercase、to_lowercase、collect 等方法
> **[批注] 原代码未完成。** 只 collect 了 words 但没有做大小写转换。补全如下:
```rust
fn title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn main() {
let s = String::from("hello WORLD rust");
let result = title_case(&s);
println!("输入: {}", s);
println!("输出: {}", result);
// 期望输出:Hello World Rust
// 验证 s 的所有权未被取走
println!("原字符串: {}", s);
}
题目 4-4:参数所有权设计
请合理设计以下函数的参数类型(传值 / 传引用 / 传可变引用):
// fn capitalize(s: ???) // 1. 只读取,不修改,调用后原变量还要用
// fn consume_and_print(s: ???) // 2. 拿走所有权,调用后原变量不再使用
// fn append_exclamation(s: ???)// 3. 修改原字符串
fn capitalize(s: &str) -> String {
let mut result = s.to_string(); // &str -> String
if let Some(c) = result.get_mut(0..1) {
c.make_ascii_uppercase();
}
result
}
fn consume_and_print(s: String) { // [批注] 原写了 str,str 是 DST 不能直接作参数
println!("消费了: {}", s);
}
fn append_exclamation(s: &mut String) { // [批注] 原写了 &mut str,应为 &mut String
s.push_str("!");
}
fn main() {
let s = String::from("hello");
let capitalized = capitalize(&s); // 填空:调用 capitalize
println!("{}", s); // s 仍可用
consume_and_print(s); // 填空:调用 consume_and_print,传入 s
// println!("{}", s); // 若取消注释会报错
let mut t = String::from("hello");
append_exclamation(&mut t); // 填空:调用 append_exclamation
println!("{}", t); // 期望输出:hello!
}
题目 4-5:数组切片操作
编写函数实现以下功能:
// 返回数组前 n 个元素的和
fn sum_of_first_n(arr: &[i32], n: usize) -> i32 {
// 你的代码
// 提示:使用切片 &arr[..n]
return arr[..n].iter().sum();
}
// 判断一个切片是否包含目标值
fn contains(arr: &[i32], target: i32) -> bool {
// 你的代码
return arr.contains(&target);
}
fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("前 3 个元素的和: {}", sum_of_first_n(&numbers, 3)); // 期望:60
println!("包含 30 吗? {}", contains(&numbers, 30)); // 期望:true
println!("包含 99 吗? {}", contains(&numbers, 99)); // 期望:false
}
五、综合思考题
题目 5-1
以下代码为什么不能通过编译?提出两种修复方法,并说明各自的优缺点。
fn main() {
let mut data = vec![1, 2, 3];
let first = &data[0];
data.push(4);
println!("first = {}", first);
}
答:
题目 5-2
分析以下代码,回答:
- 写出每一步所有权的状态变化
- 最终输出是什么?
fn main() {
let mut s = String::from("rust");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1, r2 此后不再使用
let r3 = &mut s;
r3.push_str(" is great");
println!("{}", r3);
// r3 此后不再使用
let s2 = s; // 移动 s
// println!("{}", s); // 若取消注释会怎样?
println!("{}", s2);
}
[批注] 原答最终输出写错了。 r3 通过
push_str(" is great")修改了 s 的内容,s 此时已经是"rust is great"。之后 s 移动给 s2,s2 也是"rust is great"。正确的逐步分析:
let mut s = String::from("rust")— s 拥有 "rust"let r1 = &s— 不可变借用,s 仍持有所有权let r2 = &s— 第二个不可变借用(允许多个)println!("{} and {}", r1, r2)— 输出rust and rust,此后 r1、r2 不再使用let r3 = &mut s— 可变借用(r1、r2 已失效,允许)r3.push_str(" is great")— 通过可变引用修改 sprintln!("{}", r3)— 输出rust is greatlet s2 = s— s 的所有权移动到 s2println!("{}", s2)— 输出rust is great最终输出:
rust and rust rust is great rust is great
题目 5-3
以下代码在 Rust 中是否合法?为什么?
fn main() {
let x = 5;
let y = &x;
let z = &x;
println!("{} {} {}", x, y, z);
}
[批注] 原分析对引用的理解有严重错误:引用(
&)不会转移所有权!对于
i32:y = &x创建的是指向 x 的引用,并没有发生 Copy 或所有权转移。x始终拥有那个5,y和z只是借用它。代码合法,因为多个不可变引用可以共存,且原值本身也可以在引用存在时使用(前提是x实现了Copy或本身没有被移动)。这里println!读取x也是合法的,因为 i32 是 Copy,会自动复制。对于
String: 同样,y = &x和z = &x创建的是不可变引用,没有转移所有权。x仍然持有该 String。代码合法,因为多个不可变引用可以同时存在。原答说"所有权被转移给y和z,x不再有变量所有权"是完全错误的——引用和所有权转移(move)是两回事。
如果将 x 的类型从 i32 改为 String,同样的结构是否仍然合法?
fn main() {
let x = String::from("hello");
let y = &x;
let z = &x;
println!("{} {} {}", x, y, z);
}
[批注] 同上一问,引用不会转移所有权。 String 版本同样合法。x 持有 String 的所有权,y 和 z 只是不可变借用。但注意这里
println!中直接使用x与 String 的情况,由于x不可变,println!只需要&T,所以编译器会自动将x视为&x,不会发生移动。如果此处是let y = x;(不带 & 的移动),那 x 的所有权才真正被转移,编译才会失败。
题目 5-4
Rust 的所有权系统与 C++ 的 RAII、Java 的垃圾回收各自有什么优缺点?请简要对比分析。
[批注] 原答过于简略且有概念偏差:"C++ RAII和Java的垃圾回收在运行时检查"不够准确。
- C++ RAII:所有权由程序员显式管理(构造/析构、拷贝/移动语义),C++11 引入
std::unique_ptr等智能指针提供了类似 Rust 的部分保护,但编译器不强制检查,野指针、重复释放、use-after-move 等错误仍可能静默通过编译。优点:灵活性极高,零运行时开销。- Java GC:完全无所有权概念,所有对象在堆上分配,由 GC 自动追踪引用并回收。程序员完全不用操心释放。缺点:运行时开销(GC 暂停),无法在编译期保证资源及时释放,内存占用不可预测。
- Rust 所有权:编译期静态检查所有权、移动、借用,零运行时开销。内存安全和数据竞争在编译期得到保证。缺点:学习曲线陡峭,某些合法但需要复杂生命周期的模式难以表达。
参考答案
请独立完成再查看答案。
点击展开答案
一、基础题
1-1:❌ 编译错误。s 的所有权已移动到 t,s 已失效。
1-2:✅ 通过编译。i32 实现了 Copy,赋值时自动复制。
1-3:✅ 通过编译。clone 创建了深拷贝,两个变量独立。
1-4:✅ 通过编译。不可变引用可以同时存在多个。
1-5:❌ 编译错误。同一作用域不能同时存在两个可变引用。
1-6:❌ 编译错误。已有不可变引用时不能创建可变引用。
二、填空题
2-1:takes_ownership(s);
2-2:let len = get_length(&s);
2-3:
let mut s = String::from("hello");
append_world(&mut s);
2-4:
let r1 = &s;
let r2 = &mut s;
三、修复错误
3-1:s1 已被移动给 s2,不能再传给函数。修复:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 克隆一份
print_both(s1, s2);
}
3-2:返回了局部变量的引用,生命周期不够。修复:
fn dangle() -> String { // 返回 String 而非 &String
let s = String::from("hello");
s // 所有权移出
}
3-3:r1 和 r2 的可变借用时间重叠。修复:将两次借用限制在各自的作用域内,或使用 Non-Lexical Lifetimes 特性(Rust 2018+ 已默认启用,但此处 r1 在 r2 创建后仍被使用)。修复方法:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1);
// r1 在 println! 后不再使用
let r2 = &mut s;
r2.push_str("!");
println!("{}", r2);
}
3-4:clear() 需要一个可变引用,但 first 是一个不可变引用,违反了借用规则。修复:先使用 first,再 clear:
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
println!("first = {}", first); // 先使用
let mut s = s; // 重新绑定
s.clear();
}
四、编程题
4-1:
fn count_words(s: &str) -> usize {
s.split_whitespace().count()
}
4-2:
fn first_and_last(s: &str) -> (&str, &str) {
let words: Vec<&str> = s.split_whitespace().collect();
(words[0], words[words.len() - 1])
}
4-3:
fn title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
}
})
.collect::<Vec<String>>()
.join(" ")
}
4-4:
fn capitalize(s: &str) -> String { /* ... */ }
fn consume_and_print(s: String) { /* ... */ }
fn append_exclamation(s: &mut String) { /* ... */ }
// 调用方式:
let capitalized = capitalize(&s); // 传引用
consume_and_print(s); // 传值,所有权移入
append_exclamation(&mut t); // 传可变引用
4-5:
fn sum_of_first_n(arr: &[i32], n: usize) -> i32 {
arr[..n].iter().sum()
}
fn contains(arr: &[i32], target: i32) -> bool {
arr.contains(&target)
}
五、综合思考题
5-1:first 是对 data 元素的不可变引用,push 需要一个可变引用,违反借用规则。
- 方法一:调整顺序,先 push 再取引用 —— 简单但有时逻辑不允许
- 方法二:使用索引
let first = data[0](需要 Copy 类型)—— 避免了借用但只适用于 Copy 类型
5-2:
- 步骤:创建
s→ 两次不可变借用(r1, r2)→ 打印后 r1, r2 释放 → 一次可变借用(r3)→ 打印后 r3 释放 → 移动s到s2 - 输出:
rust and rust/rust is great/rust is great - 取消注释会报错:
s已移动
5-3:两者都合法。创建引用不会转移 x 的所有权,无论 x 是什么类型。只要引用和原值不冲突即可(此处都是不可变引用)。
5-4(简要):
- C++ RAII:灵活、性能极高,但依赖程序员自觉,容易出现野指针、重复释放
- Java GC:使用方便,不会出现悬挂指针,但存在运行时开销、不可预测的停顿
- Rust 所有权:编译期零成本抽象,保证内存安全/线程安全,但学习曲线陡峭