学习rust第二天--所有权

所有权

在rust中最核心的功能之一就是所有权(ownership),一些语言有垃圾回收机制(比如java、golang),在程序运行时有规律的寻找不在使用的内存;而在另一些语言中,程序员必须亲自分配和释放内存(比如c),rust通过所有权系统管理内存,编译器在编译时,会根据所有权规则进行一系列检查,如果违反了这些规则,则程序则不能成功编译。在运行时,所有权的任何功能都不会减慢程序。

堆和栈

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 **后进先出**(_last in, first out_)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 **进栈**(_pushing onto the stack_),而移出数据叫做 **出栈**(_popping off the stack_)。栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。 堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 **指针**(_pointer_)。这个过程称作 **在堆上分配内存**(_allocating on the heap_),有时简称为 “分配”(allocating)。(将数据推入栈中并不被认为是分配)。因为指向放入堆中数据的指针是已知的并且大小是固定的,你可以将该指针存储在栈上,不过当需要实际数据时,必须访问指针。想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。

入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。

访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。

当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的主要目的就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
所有权规则
1、rust中每个值都有一个所有者(owner)。
2、值在任意时刻有且只有一个所有者。
3、当所有者离开值的作用域,值将会被丢弃。
变量作用域
fn main() {

	let x = 10;

		{
			let y = 5; 
			println!("y is {}",y);
			println!("x is {}",x);
		}
	
	println!("x is {}",x);

}
在上述代码中,y的作用域在{}中,而x的作用域则是在整个程序。若在{}外打印y的值,则会编译错误。

也就是,有两个关键时间点:
1、变量进入作用域,该变量就是有效的
2、这个过程一直持续到变量离开作用域
rust如何清理离开作用域的数据
1、string数据类型

String数据类型是一种在编译时存储未知大小的字符串类型。
String类型有三部分组成:一个指向存放字符串内容内存的指针、一个长度、一个容量。存放到栈上。长度表示字符串使用了多大的内存,容量表示系统分配了多大的内存。

创建string类型的数据
let mut s = String::from("hello");

就字面量字符串来说,在编译时,我们就知道其内容是什么,编译时字符串是硬编码到最终的可执行文件中。这使得字面量字符串快速且高效。但是我们不能在编译时将一个大小未知的内存放入到二进制文件中,有可能该大小会随这程序的运行而改变。

对于String类型,为了支持一个可变的,可增长的文本片段类型,需要在堆上分配一块大小未知的内存存储内容,需要两个方法:
1、在程序运行时,我们需要像内存分配器请求内存
2、当我们处理完String类型的数据后,将内存返回给内存分配器的方法
第一部分由我们完成,我们使用String::from,它实现了请求所需要的内存。
第二部分,在其他有垃圾回收机制(GC)的语言中,GC记录并清理不再使用的内存,我们并不需要关心它。正确处理内存是一件历史性的难题,如果回收过早,可能会产生无效变量。如果忘记回收,则会浪费资源。如果重复回收,可能会造成资源污染。所以在rust中我们需要对每一个内存分配器配对一个释放内存的功能。
在rust中,处理内存使用了一个新的策略:内存在拥有他的变量离开作用域后就会自动被释放,下面是一个例子:

fn main() {
    {
        let s = String::from("hello"); // 从此处起,s 是有效的

        // 使用 s
    }                                  // 此作用域已结束
                                       // s 不再有效
}

在上述代码中,当s离开作用域时,系统会自动调用一个特殊的函数,他的名字叫drop,在‘}’处自动调用drop函数实现释放s所占用的内存空间。

2、变量的数据交互方式(一):移动
我们可以看一段代码示例:
fn main() {
    let x = 5;
    let y = x;
}

在上述代码中,将5绑定到了x上,然后再将x的值进行拷贝并绑定到y上,这就产生了两个简单变量,所以这两个变量x,y被放到了栈上。

再看一个String的版本:
fn main(){
	let s1 = String::from("hello");
	let s2 = s1;
}

这份代码与上面的数值类型的代码类似,但是存放在内存上的逻辑相差深远。
当定义s1时,s1会像系统申请内存,将数据内容存储到堆上,然后将自己的数据结构放到栈上,然后由指针指向堆上的内存空间。
当定义s2时,s2并不会向系统申请内存,而是将s1放在栈上的数据结构拷贝一份,并且同样指向s1指针所指向的堆上的内存空间。而不是再为s2重新申请一份在堆上的内存空间,再由s2的指针指向该内存空间。
但是这样做会出现一个问题,就是在s1,s2离开作用域的时候,会有对堆上的内存空间进行两次回收清理的释放过程,会释放掉两次相同的内存地址,从而产生二次释放的错误。
在rust中,为了确保内存安全,在代码 let s2 = s1后,系统会进行识别,自动将s1判定为无效,自然也就不会在离开作用域的时候释放内存空间。
如下代码,若系统判定s1无效之后,再对s1进行使用的话,会出现错误:禁止无效的引用。

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

在rust中,由拷贝操作而使第一个变量无效了,这种操作被称为移动

3、变量与数据交互的方式(二):克隆

在上面的字符串拷贝代码中,如果我们确实需要将s1的堆内存拷贝给s2,而不仅仅是栈上的数据,rust提供了一个克隆机制函数,下面是一个克隆函数的用例:

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

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

这段代码就是将s1的堆栈信息都拷贝到s2上,并且s1并没有无效处理。

所有权与函数

rust中,在将值传递给函数时与变量赋值的过程类似,也会出现移动或克隆的过程,如下例子:

fn main() {

let s = String::from("hello ownership");

takes_ownership(s); // 因为s是String类型,s移动到函数中 在函数后s将变得无效

let x = 5;
 
makes_copy(5); // 因为x是长度固定的数值类型 x会拷贝一份数值到函数内 在函数后x依然可以使用

println!("x is {}", x);
}


// 移动

fn takes_ownership(some_string: String) {

println!("{}", some_string);

} // s离开作用域 系统调用drop方法 释放内存


// 克隆
fn makes_copy(some_integer: i32) {

println!("{}", some_integer);

} // x离开作用域

在调用takes_ownership函数后,再次使用s,系统会出现错误:borrow of moved value: s,用来说明变量s已经移动,不能再使用。

返回值与作用域

函数的返回值可以转移所有权,如下代码所示:

fn main() {

let s1 = gives_ownership(); // some_string将作用域移动到s1

  

let s2 = takes_and_gives_back(s1); // s1的作用域再进入takes_and_gives_back函数并移动到a_string

// a_string再将值移动到s2

println!("s2 is {}",s2); // 打印s2

} // s1、s2离开作用域被丢弃;


fn gives_ownership() -> String{

let some_string = String::from("hello return_ownership"); // string 进入作用域

some_string // 返回some_string
}    // some_string无效

fn takes_and_gives_back(a_string: String) -> String{

a_string // 返回a_string

}    // a_string无效
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值