1.什么是所有权

所有权是Rust的核心特性,它让Rust不需要GC(垃圾收集器)就可以保证内存安全。

所有的程序在运行时都必须管理它们使用计算机内存的方式。有些语言(C#,java)有垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存;有些语言(C、C++),程序员必须显式分配和释放内存。

Rust采用第三种方式:内存通过一个所有权管理系统管理,其中包含一组编译器在编译时检查的规则。当程序运行时,所有权特性不会减缓程序运行速度,因为Rust把内存管理相关工作提前到编译时。

1.1 Stack vs Heap(栈内存vs堆内存)

对于Rust,一个值存储在栈上还是堆上,对语言行为和你要做的某些决定有更大的影响。

在代码运行时,stack和heap都是可用的内存,但是它们的结构不一样。

  • 存储数据

stack后进先出,LIFO,添加数据叫压入栈,移除叫弹出栈。

所有存储在stack上的数据必须拥有已知的固定的大小,编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上。

当你把数据放入heap时,你会请求一定数量的空间;操作系统在heap里找到足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址。这个过程叫做在heap上进行分配,有时仅仅称为“分配”。

把值压在stack上就不叫分配。因为操作系统不需要寻找用来存储新数据的空间,那个位置永远在stack的顶端。把数据压在stack上要在heap上分配快的多。在heap上分配空间需要做更多的工作,操作系统首先要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配。

  • 访问数据

访问heap上的数据要比访问stack上的数据慢,因为需要通过指针才能找到heap中的数据。如果指令在内存中跳转次数越少,那么速度越快。

stack上数据存放距离比较近,处理起来快一些。

heap上数据之间距离比较远,处理速度慢一些。此外在heap上分配大量空间也需要时间。

  • 函数调用

当代码调用函数时,值被传入到函数(包括指向heap的指针)。函数本地的变量被压到stack上。函数结束后,这些值会从stack上弹出。

  • 所有权存在的原因

管理heap数据是所有权存在的原因,这有助于解释它为什么会这样工作。

所有权解决的问题:

  1. 跟踪代码的哪些部分正在使用heap的哪些数据;
  2. 最小化heap上的重复数据量;
  3. 清理heap上未使用的数据避免空间不足。

这样就可以不经常去想stack和heap了。

1.2 所有权规则

  1. 每个值都有一个变量,这个变量是这个值的所有者;
  2. 每个值同时只能有一个所有者;
  3. 当所有者超出作用域(scope)时,该值将被删除;
  • 变量作用域

scope就是程序中的一个项目的有效范围,这一点和其他语言都类似。

fn main()
{ //s不可用
let s = "hello";//s可用
//可以对s进行相关操作
}//s作用域到此结束,s不再可用
  • string类型

string存储在heap上,比基础标量数据类型更复杂。

在Rust中有字符串字面值,即程序里手写的字符串,它们是不变的,为了编译未知数量的文本,Rust采用string类型。

可以使用from函数从字符串字面值创建出string类型

let s = String::from("hello");
//::表示from是String类型下的函数

这类字符串是可以被修改的。

1.3 内存与分配

字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里。速度快、高效,是因为其不可变性

String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容。当用完String后,需要某种方式将内存返回给操作系统,这一步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存,没有GC就要我们识别内存何时不再使用,并调用代码将它返回。如果忘了,就浪费内存;如果提前做了,变量就变得非法;如果做了两次,就是double free bug,必须一次分配对应一次释放。

Rust采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动交还给操作系统,走出作用域时调用drop函数。

fn main()
{
let mut s = String::from("hello");
s.push_str(",world");
println!("{}",s);
}
  • 变量和数据交互的方式:移动(move)

多个变量可以与同一个数据使用同一种独特的方式来交互,如

let x = 5;
let y = x;

整数是已知大小且固定大小的简单的值,这两个5被压到stack中。

如果类型是String,它由三个内容组成,一个指向存放字符串内容的内存的指针;一个长度len,就是存放字符串内容所需的字节数;一个容量capacity,值String从操作系统中总共获得内存的总字节数。具体如下。

String类型组成

左边存放在stack,右边存放在heap。

当出现如下情况:

let s1 = String::from("hello");
let s2 = s1;

对于一般情况,String的数据会被复制一份。在stack上复制一份指针、长度、容量,并没有复制heap上的数据。当变量离开作用域时,Rust自动调用drop函数,释放heap内存,这会导致doouble free bug。

s1赋值给s2

为了保证内存安全,Rust没有尝试复制被分配的内存,Rust让s1失效,s1离开作用域时,Rust不需要释放任何东西。这里,我们用一个新的术语:移动(move)。

这里隐含一个设计原则:Rust不会自动创建数据的深拷贝,就运行时的性能而言,任何自动赋值的操作都是廉价的。

  • 变量和数据交互的方式:克隆(clone)

如果真想实现深拷贝,就要用克隆,如下:

fn main()
{
let s1 = String::from("Hello");
let s2 = s1.clone();

println!("{},{}",s1,s2);
}

clone

比较消耗资源。

  • stack上的数据:复制

Rust提供copy trait(可以理解为一个接口),可以用于像整数这样完全存放在stack上面的类型。如果一个类型实现了copy trait,那么旧的变量在赋值后仍然可用。但是一个类型或者该类型的一部分实现了drop trait,那么Rust不允许让他再去实现copy trait了。

一些拥有copy trait的类型:

所有整数类型,如u32

bool

char

所有浮点类型,如f64

任何需要分配内存或某种资源的都不是copy的