拉不拉稀肚拉稀 发表于 2026-4-27 12:48:34

Rust内存管理与安全:告别内存走漏和空指针

Rust内存管理与安全:告别内存走漏和空指针

   后端转 Rust 的萌新,ID "第一步伐员"——名字大,人很菜(暂时)。正在跟全部权和生命周期死磕,一样寻常记载 Rust 学习路上的踩坑履历和"啊哈时间",代码片断包管能跑。保持学习,保持输出。接待大佬们轻喷,也接待同好一起进步。
媒介

作为一个从后端转 Rust 的萌新,我最被 Rust 吸引的地方就是它的内存安全性。从前写 C++ 时,总是担心内存走漏、空指针引用、悬垂指针等标题,每次调试都要花许多时间排查这些标题。而 Rust 的全部权体系和借用查抄器从根本上办理了这些标题,让我可以更专注于业务逻辑的实现。本日就来分享一下我学习 Rust 内存管理与安全的心得,渴望能帮到和我一样的萌新们。
Rust 内存管理的根本概念

1. 栈和堆

和其他语言一样,Rust 也使用栈和堆来管理内存:


[*]栈:用于存储固定巨细的数据,如根本范例、函数参数、局部变量等。栈的使用速率快,由编译器自动管理。
[*]堆:用于存储动态巨细的数据,如字符串、向量等。堆的使用速率较慢,必要手动申请和开释。
2. 全部权体系

Rust 的全部权体系是其内存安全的核心,包罗以下规则:


[*]每个值都有一个全部者:一个变量拥有它所绑定的值。
[*]同一时间只能有一个全部者:当值被赋值给另一个变量时,原变量就失去了全部权。
[*]当全部者离开作用域时,值会被自动烧毁:编译器会在适当的位置插入整理代码。
示例:
fn main() {
    let s1 = String::from("hello"); // s1 是 "hello" 的所有者
    let s2 = s1; // s1 的所有权转移给 s2,s1 不再有效
    println!("{}", s1); // 编译错误:s1 已经失去所有权
    println!("{}", s2); // 正常:s2 是当前所有者
}
3. 借用

为了克制全部权转移带来的未便,Rust 提供了借用机制:


[*]不可变借用:使用 &T 范例,可以读取但不能修改值。
[*]可变借用:使用 &mut T 范例,可以读取和修改值。
借用规则:


[*]同一时间只能有一个可变借用或多个不可变借用:防止数据竞争。
[*]借用的生命周期不能高出全部者的生命周期:防止悬垂引用。
示例:
fn main() {
    let mut s = String::from("hello");
   
    let r1 = &s; // 不可变借用
    let r2 = &s; // 可以有多个不可变借用
    // let r3 = &mut s; // 编译错误:不能同时有可变和不可变借用
   
    println!("{} and {}", r1, r2); // 不可变借用结束
   
    let r3 = &mut s; // 现在可以进行可变借用
    r3.push_str(", world");
    println!("{}", r3);
}
Rust 的内存安全包管

1. 空指针安全

Rust 中没有空指针,全部引用都必须指向有用的内存。编译器会在编译时查抄引用的有用性,防止空指针解引用。
示例:
fn main() {
    let mut x = Some(5);
    if let Some(value) = x {
      println!("Value: {}", value);
    }
    // 不需要检查空指针,因为 Option 类型会强制我们处理 None 的情况
}
2. 悬垂指针安全

Rust 的借用查抄器会确保借用的生命周期不高出全部者的生命周期,防止悬垂指针。
示例:
fn main() {
    let r;
    {
      let x = 5;
      r = &x; // 编译错误:x 的生命周期短于 r
    }
    println!("r: {}", r);
}
3. 数据竞争安全

Rust 的借用规则确保同一时间只能有一个可变借用或多个不可变借用,防止数据竞争。
示例:
use std::thread;

fn main() {
    let mut data = vec!;
   
    // 编译错误:不能在多个线程中同时可变借用同一数据
    let handle = thread::spawn(|| {
      data.push(4); // 可变借用
    });
   
    println!("{:?}", data); // 不可变借用
   
    handle.join().unwrap();
}
内存走漏的防范

固然 Rust 的全部权体系会自动管理内存,但在某些情况下仍然大概发生内存走漏。以下是一些常见的内存走漏场景和防范步伐:
1. 循环引用

使用 Rc 和 RefCell 时,如果形成循环引用,会导致内存走漏。
示例:
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

fn main() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: None }));
   
    // 形成循环引用
    a.borrow_mut().next = Some(b.clone());
    b.borrow_mut().next = Some(a.clone());
   
    // 即使离开作用域,a 和 b 也不会被销毁,因为它们互相引用
}
办理方案:使用 Weak 指针冲破循环引用。
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
    prev: Option<Weak<RefCell<Node>>>, // 使用 Weak 指针
}

fn main() {
    let a = Rc::new(RefCell::new(Node { value: 1, next: None, prev: None }));
    let b = Rc::new(RefCell::new(Node { value: 2, next: None, prev: None }));
   
    a.borrow_mut().next = Some(b.clone());
    b.borrow_mut().prev = Some(Rc::downgrade(&a)); // 降级为 Weak 指针
   
    // 现在不会有循环引用,内存会被正确释放
}
2. 无穷递归

无穷递归会导致栈溢出,固然不是内存走漏,但也会导致步伐瓦解。
示例:
fn infinite_recursion() {
    infinite_recursion(); // 无限递归
}

fn main() {
    infinite_recursion();
}
办理方案:确保递归有克制条件,大概使用迭代取代递归。
3. 长时间运行的线程

如果创建了长时间运行的线程,而且线程持有资源的引用,那么这些资源不会被开释。
示例:
use std::thread;
use std::time::Duration;

fn main() {
    let data = vec!;
   
    thread::spawn(|| {
      loop {
            thread::sleep(Duration::from_secs(1));
            println!("Running...");
      }
    });
   
    // data 会一直存在,直到线程结束
    println!("Main thread exiting");
}
办理方案:使用 Arc 和 Mutex 来管理共享资源,大概确保线程可以大概正常竣事。
不安全代码的使用

固然 Rust 的安全机制非常强盛,但在某些情况下,我们必要使用不安全代码来实现一些特别功能。
1. 什么是不安全代码

不安全代码是教唆用 unsafe 关键字标志的代码块,在这些代码块中,Rust 的安全查抄会被禁用。
2. 何时使用不安全代码



[*]与 C 语言交互:调用 C 函数或使用 C 布局体时。
[*]实现底层数据布局:如自界说的智能指针。
[*]性能优化:在某些情况下,不安全代码可以进步性能。
3. 不安全代码的留意事项



[*]始终最小化不安全代码的范围:只在须要的地方使用 unsafe。
[*]提供安全的抽象:在不安全代码之上提供安全的 API。
[*]详细文档:阐明为什么必要使用不安全代码,以及怎样安全地使用它。
示例:
use std::ptr;

fn main() {
    let mut v = vec!;
   
    let ptr = v.as_mut_ptr();
    let len = v.len();
   
    // 使用不安全代码直接操作指针
    unsafe {
      for i in 0..len {
            *ptr.add(i) *= 2;
      }
    }
   
    println!("{:?}", v); //
}
内存优化本领

1. 使用 Box 存储大对象

对于大对象,使用 Box 将其存储在堆上,克制栈溢出。
示例:
fn main() {
    // 大数组,存储在堆上
    let big_array = Box::new();
    println!("Array size: {}", big_array.len());
}
2. 使用 String 和 Vec 管理动态内存

String 和 Vec 是 Rust 中管理动态内存的重要范例,它们会自动处置处罚内存的分配和开释。
示例:
fn main() {
    let mut s = String::new();
    s.push_str("Hello");
    s.push_str(", world!");
    println!("{}", s);
   
    let mut v = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);
    println!("{:?}", v);
}
3. 使用 Rc 和 Arc 实现共享全部权



[*]Rc:用于单线程情况下的共享全部权。
[*]Arc:用于多线程情况下的共享全部权。
示例:
use std::rc::Rc;

fn main() {
    let value = Rc::new(42);
   
    let a = value.clone();
    let b = value.clone();
   
    println!("a: {}, b: {}, value: {}", a, b, value);
    println!("Reference count: {}", Rc::strong_count(&value));
}
4. 使用 Cell 和 RefCell 实现内部可变性



[*]Cell:用于实现 Copy 范例的内部可变性。
[*]RefCell:用于实现非 Copy 范例的内部可变性。
示例:
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(42);
   
    {
      let mut mutable_data = data.borrow_mut();
      *mutable_data = 100;
    } // 可变借用结束
   
    println!("Data: {}", *data.borrow());
}
实际案例分析

案例 1:实现一个安全的链表

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

pub struct LinkedList<T> {
    head: Option<Rc<RefCell<Node<T>>>>,
    tail: Option<Weak<RefCell<Node<T>>>>,
    length: usize,
}

struct Node<T> {
    value: T,
    next: Option<Rc<RefCell<Node<T>>>>,
    prev: Option<Weak<RefCell<Node<T>>>>,
}

impl<T> LinkedList<T> {
    pub fn new() -> Self {
      Self {
            head: None,
            tail: None,
            length: 0,
      }
    }
   
    pub fn push_front(&mut self, value: T) {
      let new_node = Rc::new(RefCell::new(Node {
            value,
            next: self.head.take(),
            prev: None,
      }));
      
      if let Some(old_head) = &new_node.borrow().next {
            old_head.borrow_mut().prev = Some(Rc::downgrade(&new_node));
      } else {
            self.tail = Some(Rc::downgrade(&new_node));
      }
      
      self.head = Some(new_node);
      self.length += 1;
    }
   
    pub fn push_back(&mut self, value: T) {
      let new_node = Rc::new(RefCell::new(Node {
            value,
            next: None,
            prev: self.tail.clone(),
      }));
      
      let new_node_weak = Rc::downgrade(&new_node);
      
      if let Some(old_tail) = self.tail.take() {
            if let Some(old_tail_rc) = old_tail.upgrade() {
                old_tail_rc.borrow_mut().next = Some(new_node.clone());
            }
      } else {
            self.head = Some(new_node.clone());
      }
      
      self.tail = Some(new_node_weak);
      self.length += 1;
    }
   
    pub fn pop_front(&mut self) -> Option<T> {
      self.head.take().map(|old_head| {
            if let Some(new_head) = old_head.borrow_mut().next.take() {
                new_head.borrow_mut().prev = None;
                self.head = Some(new_head);
            } else {
                self.tail = None;
            }
            self.length -= 1;
            Rc::try_unwrap(old_head).ok().unwrap().into_inner().value
      })
    }
   
    pub fn len(&self) -> usize {
      self.length
    }
   
    pub fn is_empty(&self) -> bool {
      self.length == 0
    }
}

fn main() {
    let mut list = LinkedList::new();
   
    list.push_back(1);
    list.push_back(2);
    list.push_back(3);
    list.push_front(0);
   
    println!("Length: {}", list.len());
   
    while let Some(value) = list.pop_front() {
      println!("Popped: {}", value);
    }
   
    println!("Is empty: {}", list.is_empty());
}
案例 2:实现一个线程安全的计数器

use std::sync::{Arc, Mutex};
use std::thread;

struct Counter {
    value: Arc<Mutex<u32>>,
}

impl Counter {
    fn new() -> Self {
      Self {
            value: Arc::new(Mutex::new(0)),
      }
    }
   
    fn increment(&self) {
      let mut value = self.value.lock().unwrap();
      *value += 1;
    }
   
    fn get(&self) -> u32 {
      *self.value.lock().unwrap()
    }
}

fn main() {
    let counter = Counter::new();
    let mut handles = vec![];
   
    for _ in 0..10 {
      let counter_clone = Counter {
            value: Arc::clone(&counter.value),
      };
      
      let handle = thread::spawn(move || {
            for _ in 0..1000 {
                counter_clone.increment();
            }
      });
      
      handles.push(handle);
    }
   
    for handle in handles {
      handle.join().unwrap();
    }
   
    println!("Final count: {}", counter.get());
}
总结

通过本文的学习,我们相识了 Rust 内存管理与安全的核心概念和实践方法:


[*]Rust 的栈和堆内存管理
[*]全部权体系和借用规则
[*]内存安全包管(空指针安全、悬垂指针安全、数据竞争安全)
[*]内存走漏的防范步伐
[*]不安全代码的使用场景和留意事项
[*]内存优化本领
[*]实际案例分析
Rust 的内存管理体系是其最大的特色之一,它通过全部权、借用和生命周期等概念,在编译时就包管了内存安全,克制了运行时的内存错误。固然刚开始学习时会以为有些复杂,但一旦把握了这些概念,你会发现 Rust 是一门非常安全、高效的语言。
保持学习,保持输出!本日的 Rust 内存管理与安全文章就到这里,渴望对各人有所资助。接待在批评区分享你的履历和标题,我们一起进步!
参考资料



[*]Rust 官方文档 - 全部权
[*]Rust 官方文档 - 借用与引用
[*]Rust 官方文档 - 生命周期
[*]Rust 官方文档 - 不安全 Rust
[*]Rust onomicon
   后端转 Rust 的萌新,ID "第一步伐员"——名字大,人很菜(暂时)。正在跟全部权和生命周期死磕,一样寻常记载 Rust 学习路上的踩坑履历和"啊哈时间",代码片断包管能跑。保持学习,保持输出。接待大佬们轻喷,也接待同好一起进步。
页: [1]
查看完整版本: Rust内存管理与安全:告别内存走漏和空指针