Skip to content
On this page

所有权

一般来讲,所有的语言都需要管理自己的内存,一般来讲有两种模型用于管理内存:

首先是经典的使用垃圾回收机制,其次是要求程序员手动挡自己分配释放内存,然而 Rust 是第三种:使用所有权系统来管理内存。

知识基础:堆与栈

栈是一种后进先出的内存空间,想象成放盘子,放盘子放在最上边,抽盘子要抽最下边,但是你没法从中间插入或者抽出盘子。术语上,添加数据叫入栈,移除数据叫出栈。

所有存储在栈中的数据必须有已知且固定的大小,对那些编译期间无法确定数据大小的数据,我们只能用堆了。

堆则宽松很多,如果希望存储数据,那就请求特定大小的空间,操作系统会根据你的请求在堆里找到一块足够大的可用空间,并吧指向这片空间的指针返回给我们,这一过程叫做堆分配。就好比去餐厅聚餐,你告诉服务员要几个座位,然后给你分配一个足够大的桌子。即使有聚餐的人来迟了,也可以通过询问位置来找到你。

因为多了指针跳转的缘故,堆的效率肯定是不如栈的。

内存与分配

对于字符串字面量而言,由于我们在编译时就知道内容,所以这部分硬编码的文字被直接嵌入到了最终的可执行文件里,然而我们没法把未知大小的文本在编译器放入二进制文件中,而且这些文本可能在程序运行中被改变。

为了存放一个未知大小的文本类型,我们需要在堆上分配一块未知大小的内存来存放数据,这同时也意味着:

  1. 我们使用的内存是操作系统在运行时动态分配出来的
  2. 使用完 String 后,我们需要用某种方式把内存还给操作系统

大部分语言都是由程序员来发起堆内存的分配请求,比如 String::from 这类操作。

然而之后内存的回收就差别很大了,有些语言是使用垃圾回收机制,GC 代替程序员记录清除不再使用的内存。而一些没有 GC 的语言,需要用代码显式的释放,为了保证程序的文档运行,我们必须把分配和释放严格的一一对应起来。

与这些语言不同,Rust 的方案是内存会自动在拥有它的变量离开作用域后进行释放。

变量与数据交互的方式

移动

Rust 的多个变量可以采用一种独特的方式与同一数据进行交互:移动。

rust
let a = 1;
let b = a;

一般来讲,我们可以猜到这段代码的执行效果:讲整数值绑定给 a,然后创建一个 a 值的拷贝绑定到 b 上,整数是已知固定大小的简单值,因此两个值会被推入的栈中。

然后我们看看 String 版本的表现:

rust
fn main() {
    let s1 = String::from("1");
    let s2 = s1;
    println!("{}", s1)
}

然而,编译器报错了,第三行的报错情有可原,但是第四行的报错则令人困惑:

text
borrow of moved value: a value borrowed here after move

实际上两段代码的运行方式完全不一致,String 的内存布局实际上由三部分构成:一个指向存放字符串的堆内存的指针,一个长度和一个容量,这三个数据被存储在了栈中。

我们将 s1 赋值到 s2 的时候,我们复制了栈上的指针、长度、容量字段,所以我们实际上并没有复制指针指向的堆数据,用前端的话来说,这叫浅拷贝。

如果此时 Rust 用对待之前那两个数字变量的方式对待这两个 String,那么此时如果 s1 离开它的作用域,那么 Rust 会回收它,然而 s2 并不该被回收,但是 s1 被回收时,指针指向的堆内存也被释放了,然而 s2 指向的堆跟 s1 是同一个,于是这就造成了臭名昭著的“二次释放”,这是著名的内存错误之一。

所以 Rust 为了保证内存安全,它会在 s1 赋值给 s2 的时候废弃 s1 变量,这样二次释放问题就得到了解决,或者说被规避了。

克隆

既然前面有浅拷贝,那肯定也有深拷贝,直接上代码吧:

rust
fn main() {
    let a = String::from("1");
    let b = a.clone();
    println!("{},{}", a, b)
}

所有权与函数

作为函数参数

rust
fn main() {
    let a = String::from("123");
    take_over(a);
    println!("{}", a);
}

fn take_over(s: String) {
    println!("{}", s);
}

这段代码会报错,因为函数实际上带走了 a 的所有权,所以在 main() 最后一行调用 a 是不合法的。

函数传参使用了变量与数据交互的方式中的:移动。

引用与借用

如果一直这样复制、转移,我们会产出面条一样的代码,所以我们可以使用引用作为参数而不是直接转移所有权。

使用起来非常简单,就在变量前面加上 & 就变成了对变量的引用。& 代表的是引用语义,它允许我们在不获取所有权的情况下使用值。我们一般称引用传递参数给函数的方法为借用,在现实生活中我们借用别人的东西,之后就会还给别人。

与变量类似,引用默认是不可变的,Rust 不允许修改引用指向的值。

可变引用

就在传入参数的时候显式的指定 &mut,然后在函数参数上也显式的指定 &mut

rust
fn main() {
    let mut str = String::from("Hello");
    modify(&mut str);
    println!("{}", str)
}

fn modify(str: &mut String) {
    str.push_str(", Rust!");
}

但是可变引用有一个限制:在特定作用域里只能声明一个,否则就会编译错误,这让导致数据竞争的代码编译检查都无法通过,但是我们仍旧可以轻松的绕过限制,只需要通过 {} 来创建一个新的作用域就行了。

Released under the MIT License.