Rust 学习笔记之内存管理与生命周期

本文介绍了Rust编程语言的内存管理机制,包括所有权、作用域、Move和Copy语义、借用规则以及智能指针。Rust通过所有权规则确保内存安全,防止内存泄漏和越界访问等问题。文章详细阐述了不可变和可变借用、生命周期、引用、原生指针以及各种智能指针类型如Box、Rc、Cell和RefCell的使用,强调了Rust如何在编译阶段避免内存问题,提供高效且安全的代码。

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

内存管理是理解低级语言(和硬件相关的)的基础概念。低级语言没有提供自动内存管理的解决方案,例如内置垃圾回收器。它要求程序员自己在程序中管理内存。理解内存何时何地被创建和释放可以使得程序员构建出一个高效、安全的软件。然而,低级语言的大量错误也正是因为程序员使用内存不当造成的。

C 和 C++ 因为容易出现大量的内存漏洞而一直被人们诟病,因此不少人选择使用 Java 这种自带垃圾回收的语言,既可以提高安全性,又省去内存管理的心智负担。不过,无论使用哪种垃圾回收方案,都无法避免世界暂停现象,垃圾回收器会使得程序中断,接着进行回收内存的操作。换句话说,这带来了性能的开销。

Rust 选择在编译阶段解决内存管理的问题,尽可能通过良好的编程实践,阻止程序员编写出糟糕的代码,通过编译器的指导程序员编写出安全、高效使用内存的软件。

内存安全

内存安全指的是程序不会碰到不该碰到的内存位置(例如越界),程序里的指针不能指向无效的内存(NULL),在整个代码中相关的内存都应该是有效的。换句话说,安全意味着程序指针始终是有效引用,进行指针操作的时候不会出现未定义的行为,例如访问未初始化的数组,编译器没有说明这种情况下会发生什么:

#include <stdio.h>

int main()
{
   
   
    int values[5];
    for (int i = 0; i < 5; i++)
    {
   
   
        printf("%d ", values[i]);
    }
}

编译执行这段代码,输出的都是随机值。这种情况最好是可以立即崩溃,像这样表面看起来很正常,很可能日后就成为一颗定时炸弹,在某些情况下导致错误的输出。

还有一种很常见的内存安全问题就是迭代器失效

#include <iostream>
#include <vector>

int main()
{
   
   
    std::vector<int> v{
   
   1, 5, 10, 15};
    for (auto it = begin(v); it != end(v); it++)
    {
   
   
        if ((*it) == 5)
            v.push_back(-1);
    }

    for (auto it = begin(v); it != end(v); it++)
    {
   
   
        std::cout << (*it) << " ";
    }

    return 0;
}

能够正常编译这段代码,并且在某些情况下是能正常运行,但是某些情况下却会导致段错误。原因在于 push_back 有可能引发内存的重新分配(扩容),导致原有的迭代器失效(原本迭代器指向的内存已经被复制后删除了)。那么循环继续访问失效的内存,就会产生错误。

另外一种常见的内存安全问题就是数组越界

int main()
{
   
   
    char buf[3];
    buf[0] = 'a';
    buf[1] = 'b';
    buf[2] = 'c';
    buf[3] = 'd';
}

这种内存安全问题很容易导致缓冲区溢出攻击。现在的 gcc 版本中会使用 SIGABRT(中止)信号阻止程序。

还有另外一种内存问题就是内存泄漏Rust 并不能完全解决这类问题。内存泄漏相对而言危害性不如上面几种,但如果需要一个长时间运行的系统,内存泄漏问题是值得关注的。内存日积月累的泄漏,长期运行下来,总会把内存占满,最终可能导致宕机。

所有权

所有者的概念不同语言有所不同。我们这里使用资源(resource)来统称堆或者栈上的任何变量,包括持有打开文件描述符、数据库连接、网络套接字等等。从他们存在到程序使用时,均占用一些内存。作为资源的所有者,一个重要指责就是在适当的时候释放掉内存。

像 Python 这样的动态语言中,可以有多个所有者或者别名指向类似 list 这样的对象。不需要关心和释放对象的问题,因为 GC 会处理这些问题。对于编译型语言,比如 C 或者 C++,由于没有清楚的界定所有权,很多时候内存该由库来释放还是由用户来释放是不清楚的,需要进一步的了解才能确定。C/C++ 语言允许多个指针指向同一个内存,这也是很多内存问题的根源,只要其中有一个释放掉内存,其余的指针就不能再使用了(当然,RAII 惯用法可以极大程度上解决这个问题)。

Rust 则希望给所有权定下明确的规则:

  • 使用 let 创建资源并分配给变量后,这个变量就是该资源的所有者;
  • 但该变量被重新绑定到另外一个变量后,所有权会转移到新的变量上,原有变量不能再访问;
  • 变量会在作用域结束时被释放;

简而言之,Rust 规定了在同一时间里只能有一个所有者

#[derive(Debug)]
struct Foo(u32);

fn main()
{
   
   
    let foo = Foo(2048);
    let bar = foo;
    println!("Foo is {:?}", foo);
    println!("Bar is {:?}", bar);
}

使用 rustc 编译这段代码,会发现报错:
单一所有权

代码中,我们创建了两个变量,foobar。一开始 foo 拥有了 Foo 对象的所有权,随后重新绑定到了 bar 上,那么原来的 foo 变量就不再具有 Foo 资源的所有权了。也就是说,Rust 不允许在这个作用域内拥有两个可以修改资源的变量。

作用域

Rust 的作用域和大部分语言一样,同样支持 {} 建立块级作用域。作用域之间可以互相嵌套,但是内部作用域可以访问父作用域的环境变量,反之则不行。

fn main()
{
   
   
    let level_0_str = String::from("foo");
    {
   
   
        let level_1_number = 9;
        {
   
   
            let mut level_2_vector = vec![1, 2, 3];
            level_2_vector.push(level_1_number);
        } // level_2_vector 作用域结束,被释放

        level_2_vector.push(4); // 不存在了!!!
    } // level_1_number 作用域结束
} // level_0_str 作用域结束

进行所有权推理的时候,作用域的范围是需要记住的重要属性。作用域结束的时候,会自动隐式调用 Drop 方法来释放内存。

Move 和 Copy 语义

Rust 中默认是是移动语义。C++ 默认则是 Copy语义,直到 C++ 11 才引入 Move 移义。复制语义意味着得到是值的副本,两个变量之间其实并没有联系。至于移动语义,并不进行拷贝,而是进行了所有权的转移。Rust 由于它的类型系统是仿射类型系统,默认具有移动语义。仿射类型系统的一个突出的特点就是值或者资源只能用一次。

如果 Rust 只有移动语义,有时就有一定的局限性。因此,Rust 提供了 Copy trait 来实现 Copy 语义。

#[derive(Copy, Clone, Debug)]
struct Dummy;

fn main() {
   
   
    let a = Dummy;
    let b = a;
    println!("{:?}", a);
    println!("{:?}", b);
}

对于简单的原生类型,例如 u32,Rust 默认实现了 Copy,但对于稍微复制的类型,一般都是移动语义,从而避免复制内存的开销。

Copy

Copy 通常用来实现完全由堆栈表示的类型,也就是在 C/C++ 中一般不用 mallocnew 的类型。换句话说,没有内存是在堆上的。如果类型是在堆上,Copy 一般就会是性能较差的操作,因为实现了 Copy 意味着从一个变量到另一个变量的赋值会隐式的复制数据,这个操作就很类似 C 语言中的 memcpy

Rust 中,Vec<T>String,可变引用都默认没有实现 Copy。要复制这些值,我们需要更加明确的使用 Clone trait。

Clone

Clone trait 是用于显式复制的,它带有一个 clone 方法。Clone trait 的定义类似这样:

pub trait Clone {
   
   
    fn clone(&self) -> Self;
}

可以看到它接收一个不可变的引用,并且返回一个相同类型的值。不像 Copy 可以隐式调用,使用 Clone 必须显式调用 clone 方法。

clone 方法是更加通用的复制机制,Copy 只是它的特例,Copy 总是按位复制。智能指针的类型也实现了 Clone trait,但它只需要复制指针和额外的元数据,例如引用计数。

#[derive(Clone, Debug)]
struct Dummy {
   
   
    items: u32
}

fn main() {
   
   
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷雨の梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值