Rust是如何实现内存安全的--理解RAII/所有权机制/智能指针/引用

本文围绕Rust语言展开,介绍其内存管理,如栈和堆内存分配、智能指针及RAII机制;阐述内存不安全的例子,如空指针、野指针等,并说明Rust解决内存安全问题的方法;还讲解了所有权系统,包括所有权机制、转移、借用等,助力开发者掌握Rust。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

不带自动内存回收(Garbage Collection)的内存安全是Rust语言最重要的创新,是它与其他语言最主要的区别所在,是Rust语言设计的核心。

Rust希望通过语言的机制和编译器的功能,把程序员易犯错、不易检查的问题解决在编译期,避免运行时的内存错误。

1. rust的内存管理

采用虚拟内存空间在栈和堆上分配内存,这是诸多编程语言通用的内存管理基石,Rust当然也不例外。然而,与C/C++语言不同的是,Rust不需要开发者显式地通过malloc/new或free/delete 之类的函数去分配和回收堆内存。

栈内存的生命周期是短暂的,会随着栈展开(常见的是函数调用)的过程而被自动清理。而堆内存是动态的,其分配和重新分配并不遵循某个固定的模式,所以需要使用指针来对其进行跟踪。

Rust受现代C++的启发,同样引入了智能指针来管理堆内存。智能指针在堆上开辟内存空间,并拥有其所有权,通过存储于栈中的指针来管理堆内存。智能指针的 RAII 机制利用栈的特点,在栈元素被自动清空时自动调用析构函数,来释放智能指针所管理的堆内存空间。

函数的局部变量

在函数中定义的局部变量都会被默认存储到栈中。这和C/C++语言,甚至更多的语言行为都一样,但不同的是,Rust编译器可以检查未初始化的变量,以保证内存安全。

Rust编译器会对代码做基本的静态分支流程分析。

当函数调用完毕时,栈帧会被释放,局部变量会被清空。如果变量指向堆内存,那么Rust会自动清空其指向的已分配堆内存。这就是智能指针。

Rust中的指针

Rust中的指针大致可以分为三种:引用、原生指针(裸指针)和智能指针。

  • 原生指针可以在 unsafe 块下任意使用,不受 Rust 的安全检查规则的限制
  • 引用则必须受到编译器安全检查规则的限制。
  • 智能指针是对指针的一层封装,提供了一些额外的功能,比如自动释放堆内存。
    智能指针区别于常规结构体的特性在于,它实现了Deref和Drop 这两个trait(可理解为接口)。Deref提供了解引用能力,Drop提供了自动析构的能力,正是这两个trait让智能指针拥有了类似指针的行为。比如String和Vec类型就是一种智能指针(和C++中的String和Vector很相似)。

RAII(构造和析构)

RAII的机制来源于C++,RAII的设计目标就是替代GC,防止内存泄漏。RAII使用构造函数来初始化资源,使用析构函数来回收资源。

RAII和GC最大的不同在于,RAII将资源托管给创建堆内存的指针对象本身来管理,并保证资源在其生命周期内始终有效,一旦生命周期终止,资源马上会被回收。

GC是由第三方只针对内存来统一回收垃圾的,这样就很被动。

Rc引用计数智能指针

注意,在多个指针指向同一块内存的情况下,RAII机制是无法避免内存泄露的。这个时候Rust提供了智能指针Rc<T>,它的名字叫引用计数(referencecounting)智能指针。Rc<T>内部维护着一个引用计数器,每clone一次,计数器加1,当它们离开main函数作用域时,计数器会被清零,对应的堆内存也会被自动释放。

2. rust的内存安全

内存不安全的例子

  • 空指针
    解引用空指针是不安全的。这块地址空间一般是受保护的,对空指针解引用在大部分平台上会产生segfault。
  • 野指针
    野指针指的是未初始化的指针。它的值取决于它这个位置以前遗留下来的是什么值。所以它可能指向任意一个地方。对它解引用,可能会造成segfault,也可能不会,纯粹凭运气。但无论如何,这个行为都不会是你预期内的行为,是一定会产生bug的。
  • 悬空指针
    悬空指针指的是内存空间在被释放了之后,继续使用。它跟野指针类似,同样会读写已经不属于这个指针的内容。
  • 使用未初始化内存
    不只是指针类型,任何一种类型不初始化就直接使用都是危险的,造成的后果我们完全无法预测。
  • 非法释放内存
    分配和释放要配对。如果对同一个指针释放两次,会制造出内存错误。如果指针并不是内存分配器返回的值,对其执行释放操作,也是危险的。
  • 缓冲区溢出
    指针访问越界了,结果也是类似于野指针,会读取或者修改临近内存空间的值,造成危险。

Rust是如何解决内存安全问题的?

  • 使用未定义内存
    Rust中的变量必须初始化以后才可使用,否则无法通过编译器检查。
  • 空指针
    开发者没有任何办法去创建一个空指针。Rust中使用Option类型来代替空指针,Option实际是枚举体,包含两个值:Some(T)和None,分别代表两种情况,有和无。这就迫使开发者必须对这两种情况都做处理,以保证内存安全。
  • 悬空指针
    悬空指针指的是内存空间在被释放了之后,继续使用。Rust通过所有权和借用机制解决这个问题。
  • 缓冲区溢出
    Rust编译器在编译期就能检查出数组越界的问题,从而完美地避免了缓冲区溢出。
  • 非法释放未分配的指针或已经释放过的指针。
    Rust中不会出现未分配的指针,所以也不存在非法释放的情况。同时,Rust的所有权机制严格地保证了析构函数只会调用一次,所以也不会出现非法释放已释放内存的情况。

3. 所有权系统

现代C++的RAII机制解决了无GC自动管理内存的基本问题,但并没有解决全部问题,还存在着很多安全隐患。

空指针解引用

#include <iostream>
#include <memory>
using namespace std;
int main(){
    unique_ptr<int> orig(new int(5));
    cout << *orig << endl;
    auto stolen = move(orig);
    cout << *orig << endl;
}

上面这段C++代码,使用了move函数,将原来的unique_ptr指针赋予了stolen,并转让了所有权。原来的 orig 则变为了空指针,而对空指针解引用是很不安全的,所以该C++代码运行时就会抛出段错误(segmentationfault)。

fn main(){
    let orig = Box::new(5);
    println!("{}", *orig);
    let stolen = orig;
    println!("{}", *orig);
}

这段Rust代码和上面的C++代码作用一样,但是在Rust中,错误会在编译期间爆出来。

Rust中的所有权是系统性概念

可以看到Rust并没有显式地使用任何类似现代C++中的move函数来转移所有权,却拥有和现代C++一样的效果。

现代C++中的RAII机制虽然也有所有权的概念,但其作用范围非常有限,仅智能指针有所有权,并且现代C++编译器也并没有依据所有权进行严格检查,所以才会出现代码那样的解引用空指针的运行时错误。

而在Rust中,所有权是系统性的概念,是Rust语言中的基础设施。Rust中的每个值都必定有一个唯一控制者,即,所有者。所有权的转移都是按系统性的规则隐式地自动完成的,这也是代码清单5-2如此简洁的原因。

所有权系统是Rust的核心,可以说掌握了所有权系统,就等于掌握了Rust。

所有权机制

Rust中分配的每块内存都有其所有者,所有者负责该内存的释放和读写权限,并且每次每个值只能有唯一的所有者。这就是 Rust的所有权机制(OwnerShip)。

转移所有权

  • 对于可以安全地在栈上进行按位复制的类型,就只需要按位复制,也方便管理内存。

  • 对于在堆上存储的数据,因为无法安全地在栈上进行按位复制,如果要保证内存安全,就必须进行深度复制。深度复制需要在堆内存中重新开辟空间,这会带来更多的性能开销。如果堆上的数据不变,只需要在栈上移动指向堆内存的指针地址,不仅保证了内存安全,还可以拥有与栈复制同样的性能。(这就叫转移所有权)

引用与所有权借用

引用(Reference)是 Rust 提供的一种指针语义。引用是基于指针的实现,它与指针的区别是,指针保存的是其指向内存的地址,而引用可以看作某块内存的别名(Alias),使用它需要满足编译器的各种安全检查规则。

引用也分为不可变引用和可变引用。使用&符号进行不可变引用,使用&mut符号进行可变引用。

在所有权系统中,引用&x也可称为x的借用(Borrowing),通过&操作符来完成所有权租借。既然是借用所有权,那么引用并不会造成绑定变量所有权的转移。但是借用所有权会让所有者(owner)受到如下限制:

  • 在不可变借用期间,所有者不能修改资源,并且也不能再进行可变借用。
  • 在可变借用期间,所有者不能访问资源,并且也不能再出借所有权。

引用在离开作用域之时,就是其归还所有权之时。使用借用,与直接使用拥有所有权的值一样自然,而且还不需要转移所有权。

智能指针与所有权转移

智能指针和普通引用的区别之一就是所有权的不同。智能指针拥有资源的所有权,而普通引用只是对所有权的借用。

对于智能指针Box<T>类型来说,如果包含的类型T属于复制语义,则执行按位复制;如果属于移动语义,则移动所有权。

进一步学习Rust

欢迎加入我的知识星球,我会提供一些学习资料(书籍/视频)以及解答一些问题。
大家一起学习Rust 😃
rust

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值