跳到主要内容

所有权

问题

Rust 的所有权机制是什么?为什么说它是 Rust 内存安全的基石?

答案

所有权(Ownership)是 Rust 管理内存的核心机制。与 C/C++ 手动管理内存不同,也与 Java/Go 的垃圾回收不同,Rust 通过编译期的所有权规则在零运行时开销下保证内存安全。

三条铁律

  1. Rust 中每个值都有一个所有者(owner)
  2. 同一时间只能有一个所有者
  3. 当所有者离开作用域(scope),值被自动释放

作用域与 Drop

当变量离开作用域时,Rust 自动调用 drop 函数释放内存,类似 C++ 的 RAII:

fn main() {
{
let s = String::from("hello"); // s 在此处生效,分配堆内存
println!("{}", s);
} // s 离开作用域,Rust 自动调用 drop,释放堆内存
// 此处 s 不再可用
}

Move 语义(移动)

对于堆上分配的数据(如 StringVec),赋值操作会转移所有权,而非复制数据:

fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 移动到 s2

// println!("{}", s1); // ❌ 编译错误:value used here after move
println!("{}", s2); // ✅ s2 是当前所有者
}

Move 的内存示意图:

为什么要 Move 而不是浅拷贝?

如果两个变量同时拥有同一块堆内存的指针,当两个变量都离开作用域时,就会双重释放(double free)——这是 C/C++ 中著名的内存安全问题。Move 语义让同一时间只有一个所有者,从根源上避免了这个问题。

Copy 语义(复制)

对于栈上分配的简单类型,赋值时自动按位复制,不会发生 Move:

fn main() {
let x = 42; // i32 实现了 Copy trait
let y = x; // 按位复制,x 依然有效
println!("x = {}, y = {}", x, y); // ✅ 两个都可以用
}

实现了 Copy trait 的类型:

类型说明
所有整数类型i8i128u8u128isizeusize
浮点数f32f64
布尔bool
字符char
元组当所有元素都是 Copy 时,如 (i32, f64)
数组当元素是 Copy 时,如 [i32; 5]
共享引用&T(注意 &mut T 不是 Copy)
Copy 与 Drop 互斥

一个类型如果实现了 Drop trait(自定义析构逻辑),就不能同时实现 Copy。因为 Copy 意味着按位复制后两份数据都有效,但 Drop 意味着离开作用域要清理资源——两份数据各自 Drop 会导致资源被释放两次。

Clone 语义(显式深拷贝)

当你确实需要复制堆上数据时,使用 clone() 方法进行显式深拷贝

fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 显式深拷贝,堆上数据也被复制一份

// 两个变量各自拥有独立的堆内存
println!("s1 = {}, s2 = {}", s1, s2); // ✅ 两个都有效
}
Copy vs Clone
  • Copy:隐式的、按位复制,零开销,只适用于栈上简单数据
  • Clone:显式的、可以自定义深拷贝逻辑,可能有性能开销

所有 Copy 类型都自动实现了 Clone(Copy 本身就是一种 Clone)。

函数与所有权

将值传入函数,所有权规则同样适用——传参等价于赋值:

fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权移动到函数中
// println!("{}", s); // ❌ s 已经被 move,不能再使用

let x = 42;
makes_copy(x); // i32 是 Copy,只是复制了一份
println!("x = {}", x); // ✅ x 依然可用
}

fn takes_ownership(s: String) {
println!("{}", s);
} // s 离开作用域,内存被释放

fn makes_copy(x: i32) {
println!("{}", x);
} // x 是 Copy 的副本,离开作用域简单弹出栈

返回值与所有权转移

函数返回值也会转移所有权:

fn create_string() -> String {
let s = String::from("hello");
s // 所有权转移给调用者
}

fn main() {
let s1 = create_string(); // 获得返回值的所有权
println!("{}", s1); // ✅
}

如果每次调用函数都要转移所有权再返回,会非常繁琐。这就是为什么 Rust 引入了引用和借用——在不转移所有权的情况下访问数据:

fn calculate_length(s: &String) -> usize {
s.len()
} // s 是引用,离开作用域不会释放底层数据

fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传递引用,不转移所有权
println!("'{}' 的长度是 {}", s, len); // ✅ s 依然可用
}

详细内容请阅读 借用与引用

自定义类型的 Move/Copy/Clone

通过 derive 宏为自定义类型实现 Copy 或 Clone:

// 所有字段都是 Copy 的,可以 derive Copy + Clone
#[derive(Debug, Copy, Clone)]
struct Point {
x: f64,
y: f64,
}

// 包含 String(非 Copy),只能 derive Clone,不能 derive Copy
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
}

fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // Copy,p1 依然可用
println!("{:?}", p1); // ✅

let u1 = User { name: String::from("Alice"), age: 30 };
let u2 = u1.clone(); // 必须显式 clone
// let u3 = u1; // Move,u1 不再可用
println!("{:?}", u2);
}

部分移动(Partial Move)

结构体中的某些字段可以被单独移动,此时整个结构体不能再使用,但未移动的字段仍可独立访问:

fn main() {
let user = User {
name: String::from("Alice"),
age: 30,
};

// 只移动 name 字段
let name = user.name;

// println!("{:?}", user); // ❌ user 整体已不可用
println!("age: {}", user.age); // ✅ 未被移动的字段仍可访问
println!("name: {}", name);
}

常见面试问题

Q1: Rust 的所有权机制解决了什么问题?

答案

所有权机制从根源上解决了手动内存管理语言(C/C++)中的常见内存安全问题:

问题传统语言Rust 所有权方案
双重释放两个指针指向同一内存,各自 freeMove 语义确保只有一个所有者
使用已释放内存指针指向已 free 的内存编译器追踪所有权,move 后禁止使用
内存泄漏忘记调用 free离开作用域自动 drop
空指针解引用null pointer dereference没有 null,用 Option<T> 代替

同时不需要 GC——零运行时开销,性能与 C/C++ 持平。

Q2: Move 和 Copy 的区别是什么?什么时候会发生 Move?

答案

  • Move:所有权转移,原变量不再可用。适用于堆上分配的复杂类型(StringVec<T>Box<T> 等)
  • Copy:按位复制,原变量仍然可用。适用于栈上的简单类型(整数、浮点数、boolchar 等)

发生 Move 的场景:

  1. 变量赋值let s2 = s1;
  2. 函数传参foo(s1);
  3. 函数返回值let s = create();
  4. 模式匹配解构let (a, b) = tuple;(当元素类型不是 Copy 时)

判断规则:如果类型实现了 Copy trait,则复制;否则 Move

Q3: Clone 和 Copy 有什么关系?

答案

  • CopyClone 的子 trait:所有实现 Copy 的类型必须也实现 Clone
  • Copy隐式的、按位复制,没有额外开销
  • Clone显式的、调用 .clone() 方法,可以有自定义逻辑(如深拷贝堆数据)
  • 实现了 Drop 的类型不能实现 Copy(但可以实现 Clone

Q4: 为什么 String 不能实现 Copy

答案

String 内部包含一个指向堆内存的指针、长度和容量。如果 String 实现了 Copy(按位复制),会出现两个 String 指向同一块堆内存的情况。当两者都离开作用域时,堆内存会被释放两次(double free),这是未定义行为

此外,String 实现了 Drop(用于释放堆内存),而 CopyDrop 是互斥的——Rust 编译器禁止一个类型同时实现这两个 trait。

Q5: 所有权转移后,原始变量的内存怎么处理?

答案

Move 之后,原始变量在栈上的元数据(指针、长度、容量)还在,但编译器标记它为"已移动",不允许再被使用。这些栈上的元数据会在函数返回时随栈帧一起被回收。

Move 不涉及任何堆内存操作——只是把栈上的元数据(通常是 24 字节的 ptr+len+cap)复制到新位置,并告诉编译器旧位置不再有效。这也是为什么 Move 是零成本的。

Q6: Box<T> 的所有权规则是什么?

答案

Box<T> 是 Rust 最简单的智能指针,它在堆上分配内存并拥有该数据:

fn main() {
let b1 = Box::new(42); // 在堆上分配一个 i32
let b2 = b1; // 所有权 move 到 b2(Box 没有实现 Copy)
// println!("{}", b1); // ❌ b1 已被 move
println!("{}", b2); // ✅ 输出 42
} // b2 离开作用域,堆内存被释放

Box<T> 遵循标准的所有权规则:赋值时 Move,离开作用域时 Drop(释放堆内存)。它实现了 Deref,可以自动解引用访问内部的 T

Q7: 什么是"部分移动"(Partial Move)?

答案

当只移动结构体中的某个字段时,发生部分移动。此时:

  • 整个结构体不能再整体使用
  • 未被移动的字段仍可独立访问
struct Config {
name: String,
retries: u32,
}

fn main() {
let config = Config {
name: String::from("app"),
retries: 3,
};

let name = config.name; // 部分移动:name 被 move
// println!("{:?}", config); // ❌ 整体不可用
println!("{}", config.retries); // ✅ retries 是 Copy,未被影响
println!("{}", name); // ✅
}

避免部分移动的方式:使用 clone()引用来访问字段。

Q8: Rust 如何处理循环引用导致的内存泄漏?

答案

所有权系统可以防止大部分内存安全问题,但循环引用仍然可能导致内存泄漏——这不是"不安全"(不会导致未定义行为),但是不理想的。

典型场景:用 Rc<T> + RefCell<T> 创建循环引用。解决方案是使用 Weak<T>(弱引用)打破循环:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // ← 弱引用,不增加引用计数
children: RefCell<Vec<Rc<Node>>>,
}

更多智能指针内容请参阅 智能指针

Q9: 所有权在多线程场景下有什么作用?

答案

所有权与 Send/Sync trait 结合,在编译期消除数据竞争:

  • Move 到线程中std::thread::spawn 要求闭包是 'static + Send,通常用 move 闭包将数据的所有权移入线程
  • 编译器保证:如果数据被 Move 到某个线程,其他线程无法再访问——从根源上杜绝了数据竞争
use std::thread;

fn main() {
let data = vec![1, 2, 3];

let handle = thread::spawn(move || {
// data 的所有权已 move 进来,主线程不能再用
println!("{:?}", data);
});

// println!("{:?}", data); // ❌ 编译错误:data 已被 move
handle.join().unwrap();
}

更多内容请参阅 Send 与 Sync

Q10: 如何选择 Move、Clone 还是借用?

答案

场景选择原因
不再需要原始数据Move零开销,转移所有权
需要保留原始数据,且只读借用 &T零开销,多处共享读
需要保留原始数据,且修改可变借用 &mut T零开销,独占写
需要独立副本Clone有开销,但获得独立所有权
栈上小数据Copy(自动)编译器自动处理

优先级:借用 > Move > Clone。只在确实需要独立副本时才 Clone。

相关链接