0%

Rust所有权

Rust的核心功能之一 ownership ,运行的程序都需要使用计算机管理内存的方式,比如Java 具备垃圾回收,还有一些语言需要手动去释放内存。而Rust则是第三种方式,通过所有权管理内存,编译器在编译时会根据一些列规则检查,在运行时所有权系统的任务功能都不会减慢程序。

  • 所有权和生命周期是 Rust 和其它编程语言的主要区别,也是 Rust 其它知识点的基础。

所有权

  • 栈是程序运行的基础。每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存被称为帧(frame)。

  • 栈以放入值的顺序存储并以相反的顺序取出值。这也被称为 后进先出 (last in , first out) 。添加数据的时候加 进栈 (pushing anto the stack) ,而移出数据叫 出栈 (poping off th stack)。

  • 在调用的过程中,一个新的帧会分配足够的空间存储寄存器的上下文。在函数里使用到的通用寄存器会在栈保存一个副本,当这个函数调用结束,通过副本,可以恢复出原本的寄存器的上下文,就像什么都没有经历一样。此外,函数所需要使用到的局部变量,也都会在帧分配的时候被预留出来。

  • 栈的操作时非常快的,这主要得益于它存取数据的方式,数据的存取位置总时在栈顶,而不需要重新寻找一个位置去存放或者读取。另一个属性就是,栈中所有的数据都必须占据已知且固定的大小。

  • 但是工作中我们依然要避免大量数据存放栈中,避免栈溢出(stack overfow),运行程序调用栈超出了系统运行的最大空间,就无法创建新的。就会出现溢出现象

在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上。

  • 堆主要存放大小未知,可能大小变化的数据,堆事缺乏组织的。当向堆中存放数据时,要申请一个空间,操作系统在堆的某处足够大的空间,把它标记为已经使用。并返回一个指针(pointer),这个过程叫做堆上分配内存(allocating on the heap)。
  • 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。当代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
案例一
1
2
3
4
5
fn say(name : String){
println("name {}" , name);
}
say("tomcat".to_string());
say("jetty".to_string());
  • 字符串的数据结构,在编译器是不确定的,运行期才代码才知道大小。比如tomcatjetty,当say方法执行的时候才知道参数的具体长度。
案例二
1
2
3
let mut arr = Vec::new();
arr.push(1);
arr.push(2);
  • 列表初始化长度是4,而不是2。在堆上内存分配会使用libc提供的mallo函数。系统调用的代价是昂贵的,所以我们要避免频繁地 malloc()。
内存泄漏

如果手工管理堆内存的话,堆上内存分配后忘记释放,就会造成内存泄漏。一旦有内存泄漏,程序运行得越久,就越吃内存,最终会因为占满内存而被操作系统终止运行。

堆越界

如果堆上内存被释放,但栈上指向堆上内存的相应指针没有被清空,就有可能发生使用已释放内存(use after free)的情况。

GC垃圾回收处理
  • 比如Java采用垃圾回收(GC),来自动管理内存,定期标记(mark)找出不在被引用的对象,然后清理(sweep)掉。
  • 但是GC是不确定的,可能引起STW(Stop The World)。
ARC自动引用计数处理方式
  • 在编译时,它为每个函数插入 retain/release 语句来自动维护堆上对象的引用计数,当引用计数为零的时候,release 语句就释放对象。

栈与堆

  • 栈上存放的数据是静态的,固定大小,固定生命周期;堆上存放的数据是动态的,不固定大小,不固定生命周期。

所有权规则

  • Rust中每一个值都有一个被称为其所有者的变量。
  • 值有且只有一个所有者。当所有者(变量)离开作用域,这个值将被丢弃。

作用域

变量作用域

1
2
3
4
{ // 作用域开启, var无效,未声明。
let var = "hello";
// 使用var
} // 作用域结束, var不再有效。
  • 当变量var离开作用域的时候,Rust会调用一个特殊的函数drop自动处理。

    在 C++ 中,这种 item 在生命周期结束时释放资源的模式有时被称作 资源获取即初始化(*Resource Acquisition Is Initialization (RAII)*)。

内存分配

定义某个变量且赋值,在编译期间就知道了具体内容。

案例一
1
2
let x = 5;	
let y = x;
  • x的值赋给y,这时候有了两个变量且值都等于5。此时栈中会存放两个5。
案例二
1
2
3
4
5
6
7
fn main() {
println!("Hello, world!");
let s1 = String::from("hi");
let s2 = s1;
println!("s1 value {}",s1);
println!("s2 value {}",s2);
}
  • Console
1
2
3
4
5
6
7
8
--> src/main.rs:5:28
|
3 | let s1 = String::from("hi");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 | let s2 = s1;
| -- value moved here
5 | println!("s1 value {}",s1);
| ^^ value borrowed here after move
  • 编译器会提示两个错误,将s1 赋值给 s2 后 s1 就不能被访问了。
  • 如果还想让s1 被访问就需要 clone复制一份堆数据出来。
1
2
3
4
5
6
7
fn main() {
println!("Hello, world!");
let s1 = String::from("hi");
let s2 = s1.clone();
println!("s1 value {}",s1);
println!("s2 value {}",s2);
}
  • Console
1
2
3
Hello, world!
s1 value hi
s2 value hi

Copy

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
    所有浮点数类型,比如 f64。
  • 字符类型,char。
  • Tuple元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。

所有权与函数

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

fn main() {
let s = String::from("hi"); // s 进入作用域
hello(s);
// s 到这里不再有效
let x = 5; // x 进入作用域
hello_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
println!("x {}", x);
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn hello(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 移出作用域并调用 `drop` 方法。占用的内存被释放

fn hello_copy(some_integer: i32) { //
println!("{}", some_integer);
}