Rust 核心内存安全机制——所有权、借用与生命周期
Rust 通过所有权、借用和生命周期三大机制保障内存安全。所有权规定每个值有唯一所有者,离开作用域自动释放,支持 Move 转移或 Clone 复制。借用允许获取引用而不转移所有权,遵循不可变引用可共存、可变引用独占且不与不可变引用共存的规则。生命周期标注引用有效范围,防止悬垂引用。文章结合字符串查找、数组排序及链表实现等案例,演示如何应用这些机制编写无 GC、高性能且安全的系统级代码,并解决了悬垂引用、数据竞争等常见问题。

Rust 通过所有权、借用和生命周期三大机制保障内存安全。所有权规定每个值有唯一所有者,离开作用域自动释放,支持 Move 转移或 Clone 复制。借用允许获取引用而不转移所有权,遵循不可变引用可共存、可变引用独占且不与不可变引用共存的规则。生命周期标注引用有效范围,防止悬垂引用。文章结合字符串查找、数组排序及链表实现等案例,演示如何应用这些机制编写无 GC、高性能且安全的系统级代码,并解决了悬垂引用、数据竞争等常见问题。

💡 三大核心难点:
⚠️ 三大高频错误点:
所有权机制是 Rust 的核心内存安全保障,它通过编译器检查而非运行时 GC(垃圾回收)来管理内存,杜绝了空指针、野指针、内存泄漏等 C/C++ 常见的安全问题。
✅ 每个值都有且仅有一个所有者(变量)
✅ 所有者离开作用域时,值会被自动释放
✅ 值的所有权可以转移(Move),但不能复制(除非实现 Copy trait)
当一个值被赋值给另一个变量、传递给函数或从函数返回时,值的所有权会发生转移,原变量将无法再访问该值。
⌨️ 所有权转移示例:
// 1. 赋值操作中的所有权转移
let s1 = String::from("Hello, Rust!");
let s2 = s1;
// s1 的所有权转移到 s2
// println!("s1: {}", s1); // 编译错误:s1 不再是所有者
// 2. 函数参数传递中的所有权转移
fn takes_ownership(s: String) {
println!("函数内部:{}", s);
}
let s3 = String::from("World");
takes_ownership(s3);
// println!("函数外部:{}", s3); // 编译错误:s3 的所有权已转移
// 3. 函数返回值中的所有权转移
fn gives_ownership() -> String {
let s = String::from("Rust is safe");
s // 返回值是表达式,所有权转移到调用方
}
let s4 = gives_ownership();
println!("s4: {}", s4);
// 输出:Rust is safe
如果需要复制一个值而不转移所有权,可以使用 clone() 方法,克隆会在堆内存上复制一份新的数据。
⌨️ 克隆示例:
let s1 = String::from("Hello");
let s2 = s1.clone();
// 克隆一份新数据
println!("s1: {}, s2: {}", s1, s2);
// 输出:Hello, Hello
对于简单的栈内存数据类型(如整数、浮点数、布尔值、字符、数组长度≤32 字节的数组等),Rust 默认实现了 Copy trait,赋值操作会在栈内存上复制一份新的数据,而不是转移所有权。
⌨️ Copy trait 示例:
let x = 5;
let y = x; // 栈内存复制,x 仍可访问
println!("x: {}, y: {}", x, y);
// 输出:5, 5
let z = [1, 2, 3];
let w = z; // 栈内存复制,z 仍可访问
println!("z: {:?}, w: {:?}", z, w);
// 输出:[1,2,3], [1,2,3]
⚠️ Copy trait 的限制:
如果我们不想转移值的所有权,可以使用借用(Borrow),也就是获取值的引用(Reference)。
不可变引用是最常用的引用类型,同一时间同一数据可以有多个不可变引用,但不能有可变引用。
⌨️ 不可变引用示例:
let s = String::from("Rust");
// 同一时间同一数据有多个不可变引用
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("r1: {}, r2: {}, r3: {}", r1, r2, r3);
// 输出:Rust, Rust, Rust
可变引用允许修改值的内容,同一时间同一数据只能有一个可变引用,且不能有不可变引用。
⌨️ 可变引用示例:
let mut s = String::from("Rust");
// 同一时间同一数据只有一个可变引用
let r1 = &mut s;
// let r2 = &mut s; // 编译错误:同一时间同一数据只能有一个可变引用
// let r3 = &s; // 编译错误:同一时间同一数据不能同时有可变和不可变引用
r1.push_str(", safe and fast!");
println!("r1: {}", r1);
// 输出:Rust, safe and fast!
✅ 引用的生命周期不能超过所有者的生命周期
✅ 引用的作用域必须在所有者的作用域内
⌨️ 悬垂引用示例(编译错误):
fn returns_dangling_reference() -> &String {
// 编译错误:返回值引用没有标注生命周期
let s = String::from("Dangling reference");
&s // 所有者 s 离开作用域时会被释放,返回的引用是无效的
}
fn main() {
let r = returns_dangling_reference();
println!("r: {}", r);
}
生命周期(Lifetime)用于标注引用的有效范围,确保引用的生命周期不超过所有者的生命周期,防止出现悬垂引用。
生命周期参数用撇号(')开头,后面跟一个标识符(如'a、'b、'static),生命周期参数只用于标注引用的有效范围,不改变引用的实际生命周期。
当函数有多个引用参数时,需要标注它们的生命周期参数,编译器会根据标注的生命周期参数推断返回值的生命周期。
⌨️ 函数参数的生命周期标注示例:
// 函数接受两个字符串引用,返回较长的那个引用
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
// 'a 标注参数和返回值的生命周期相同
if s1.len() > s2.len() { s1 } else { s2 }
}
fn main() {
let s1 = String::from("Hello");
let result;
{
let s2 = String::from("World!");
result = longest(s1.as_str(), s2.as_str());
// result 的生命周期由 s1 决定(s1 的作用域更广)
println!("result: {}", result);
// 输出:World!(s2 的作用域还没结束)
}
// println!("result: {}", result); // 编译错误:result 的生命周期与 s2 相同,s2 已离开作用域
}
如果一个结构体包含引用类型的字段,需要标注该字段的生命周期参数,编译器会确保结构体的生命周期不超过字段引用的所有者的生命周期。
⌨️ 结构体的生命周期标注示例:
// 包含引用类型字段的结构体
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str, // 'a 标注 part 字段的生命周期
}
impl<'a> ImportantExcerpt<'a> {
// 实现结构体时也需要标注生命周期参数
fn level(&self) -> i32 {
// &self 的生命周期与结构体的生命周期 'a 相同
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
// 返回值的生命周期与 &self 相同
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
println!("i: {:?}", i);
// 输出:ImportantExcerpt { part: "Call me Ishmael" }
}
'static 生命周期表示引用的有效范围是整个程序的运行期,通常用于字符串字面量(存储在静态内存上)。
⌨️ 'static 生命周期示例:
// 字符串字面量的生命周期是 'static
let s: &'static str = "Hello, static life!";
// 可以将 'static 生命周期的引用赋值给任何生命周期的变量
fn longest<'a>(s1: &'a str, s2: &'static str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
// 'static 生命周期比 'a 长,所以可以返回
}
为了简化代码,Rust 编译器提供了生命周期省略规则,在某些情况下可以不写生命周期参数。
✅ 三个省略规则:
⌨️ 生命周期省略规则示例:
// 规则 1:每个参数都是单独的生命周期参数
fn print_two_strings(s1: &str, s2: &str) {
// 编译器自动标注为 fn print_two_strings<'a, 'b>(s1: &'a str, s2: &'b str)
println!("s1: {}, s2: {}", s1, s2);
}
// 规则 2:如果只有一个引用参数,返回值的生命周期与该参数相同
fn get_first_word(s: &str) -> &str {
// 编译器自动标注为 fn get_first_word<'a>(s: &'a str) -> &'a str
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// 规则 3:如果是方法,&self 的生命周期会被用作返回值的生命周期
#[derive(Debug)]
struct Person<'a> {
name: &'a str,
}
impl<'a> Person<'a> {
fn get_name(&self) -> &str {
// 编译器自动标注为 fn get_name<'b>(&'b self) -> &'b str('b 与 'a 相关)
self.name
}
}
fn main() {
let name = "张三";
let person = Person { name };
(, person);
(, person.());
}
💡 场景分析:需要编写一个自定义的字符串查找函数,接受两个字符串引用,返回第一个字符串中第一个出现第二个字符串的起始索引。
⌨️ 代码示例:
// 自定义字符串查找函数
fn find_substring<'a>(haystack: &'a str, needle: &str) -> Option<usize> {
let haystack_bytes = haystack.as_bytes();
let needle_bytes = needle.as_bytes();
let haystack_len = haystack_bytes.len();
let needle_len = needle_bytes.len();
if needle_len == 0 {
return Some(0);
}
if haystack_len < needle_len {
return None;
}
for i in 0..=haystack_len - needle_len {
let mut j = 0;
while j < needle_len && haystack_bytes[i + j] == needle_bytes[j] {
j += 1;
}
if j == needle_len {
return Some(i);
}
}
None
}
fn main() {
let haystack = "Hello, Rust! This is a test.";
let needle1 = "Rust";
= ;
= ;
(, needle1, haystack);
(index) = (haystack, needle1) {
(, index);
} {
();
}
(, needle2, haystack);
(index) = (haystack, needle2) {
(, index);
} {
();
}
(, needle3, haystack);
(index) = (haystack, needle3) {
(, index);
} {
();
}
}
💡 场景分析:需要编写一个自定义的数组排序函数,接受一个整数数组的可变引用,使用冒泡排序算法对其进行排序。
⌨️ 代码示例:
// 自定义冒泡排序函数
fn bubble_sort(arr: &mut [i32]) {
let len = arr.len();
for i in 0..len {
for j in 0..len - 1 - i {
if arr[j] > arr[j + 1] {
arr.swap(j, j + 1); // 交换两个元素
}
}
}
}
fn main() {
let mut numbers = [5, 3, 8, 1, 2, 9];
println!("排序前:{:?}", numbers);
bubble_sort(&mut numbers);
println!("排序后:{:?}", numbers);
// 输出:[1, 2, 3, 5, 8, 9]
}
💡 场景分析:需要编写一个自定义的单向链表数据结构,支持添加节点、删除节点、查找节点、遍历链表等操作。
⌨️ 代码示例:
// 定义链表节点
#[derive(Debug)]
struct Node<T> {
data: T,
next: Option<Box<Node<T>>>,
}
// 定义链表结构
#[derive(Debug)]
struct LinkedList<T> {
head: Option<Box<Node<T>>>,
}
impl<T> LinkedList<T> {
// 创建新的链表
fn new() -> Self {
LinkedList { head: None }
}
// 在链表头部添加节点
fn push_front(&mut self, data: T) {
let new_node = Box::new(Node { data, next: self.head.take() });
self.head = Some(new_node);
}
// 在链表尾部添加节点
fn push_back(&mut self, data: T) {
let new_node = Box::new(Node { data, next: None });
if self.head.is_none() {
self.head = Some(new_node);
return;
}
let mut current = self.head.().();
current.next.() {
current = current.next.().();
}
current.next = (new_node);
}
(& ) <T> {
.head.().(|node| {
.head = node.next;
node.data
})
}
(&, target: &T) <>
T: ,
{
= .head.();
= ;
(node) = current {
node.data == *target {
(index);
}
current = node.next.();
index += ;
}
}
(&) Iter<T> {
Iter { next: .head.() }
}
}
<, T> {
next: <& Node<T>>,
}
<, T> <, T> {
= & T;
(& ) <::Item> {
.next.(|node| {
.next = node.next.();
&node.data
})
}
}
() {
= LinkedList::();
list.();
list.();
list.();
list.();
();
list.() {
(, item);
}
();
(index) = list.(&) {
(, index);
} {
();
}
();
(data) = list.() {
(, data);
} {
();
}
();
(data) = list.() {
(, data);
} {
();
}
}
问题现象:引用的生命周期超过了所有者的生命周期,导致编译错误或运行时崩溃。
解决方案:确保引用的作用域在所有者的作用域内,或者使用 Box 等智能指针来延长数据的生命周期。
⌨️ 使用 Box 延长数据的生命周期示例:
fn returns_valid_reference() -> Box<String> {
let s = String::from("Valid reference");
Box::new(s) // 使用 Box<T> 将数据转移到堆内存上,返回值是 Box<T>,不是引用
}
fn main() {
let r = returns_valid_reference();
println!("r: {}", r);
// 输出:Valid reference
}
问题现象:同一时间同一数据存在多个可变引用,导致编译错误(数据竞争)。
解决方案:确保同一时间同一数据只有一个可变引用,或者使用锁(如 Mutex)来实现线程安全的共享可变数据。
⌨️ 使用 Mutex 实现线程安全的共享可变数据示例:
use std::sync::Mutex;
fn main() {
let data = Mutex::new(0);
// 线程 1
let thread1 = std::thread::spawn(move || {
let mut guard = data.lock().unwrap();
*guard += 1;
});
// 线程 2
// 注意:thread1 已经获取了 data 的所有权,这里重新创建一个 Mutex<T> 以便演示
let data2 = Mutex::new(0);
let thread2 = std::thread::spawn(move || {
let mut guard = data2.lock().unwrap();
*guard += 1;
});
thread1.join().unwrap();
thread2.join().unwrap();
}
问题现象:同一时间同一数据存在可变引用和不可变引用,导致编译错误(数据竞争)。
解决方案:确保同一时间同一数据只有可变引用或多个不可变引用。
⌨️ 避免可变引用与不可变引用同时存在的示例:
let mut s = String::from("Rust");
// 先使用不可变引用,再使用可变引用
let r1 = &s;
println!("r1: {}", r1);
drop(r1); // 显式释放不可变引用
let r2 = &mut s;
r2.push_str(", safe!");
println!("r2: {}", r2);
✅ 理解了所有权机制的三个核心规则:每个值都有且仅有一个所有者,所有者离开作用域时值会被自动释放,值的所有权可以转移但不能复制(除非实现 Copy trait)
✅ 掌握了借用的规则:同一时间同一数据可以有多个不可变引用或一个可变引用,不能同时有可变和不可变引用
✅ 精通了生命周期参数的标注方法:包括函数参数、返回值、结构体的生命周期标注,以及省略规则
✅ 结合真实场景编写了三个实用的代码案例:自定义字符串查找函数、自定义数组排序函数、自定义链表数据结构
✅ 学习了常见问题的解决方案:包括悬垂引用、多次可变引用、可变引用与不可变引用同时存在
下一篇文章,我们将深入学习 Rust 的结构体与枚举的高级用法,包括泛型、trait、关联类型、模式匹配的高级应用(如枚举的嵌套匹配、结构体的字段匹配),通过这些知识我们将能够编写更灵活、可复用的代码。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online