所有权规则
1.每个值都有一个变量,这个变量是该值的所有者
2.每个值同时只能有一个所有者
3.当所有者超出作用域的时候,该值将被删除
变量作用域
作用域就是程序中一个项目的有效范围,例子:
String类型
为了演示一些所有权的相关规则,需要一个稍微复杂一点的数据类型,因为String类型比之前那些基础标量数据类型复杂一些,这里只讲String与所有权相关的部分
字符串字面值就是程序里写死的字符串值,虽然方便,但是不能满足所有的需求场景,原因之一就是因为字符串字面值是不可变的,另一个原因就是所有的字符串值都能在编写代码时确定,就比如获取用户的输入并保存,相对这种情况,Rust 提供了第二种字符串类型:String类型
String类型会在堆上来分配自己所需的存储空间,而且能存储在编译时未知大小的文本。
创建String类型的值
可以使用from函数从字符串字面值创建出String类型
如:let s = String::from("hello");
两个冒号“::”表示from时String类型下的函数,这类字符串是可以被修改的,例子:
为什么String类型的值可以修改,而字符串字面值不能修改,因为它们处理内存的方式不同
内存和分配
字符串字面值,在编译时就知道它的内容,其文本内容直接被硬编码到最终的可执行文件里,因为不可变,所以速度快、高效。
String类型为了支持可变性,需要在堆上分配内存来保存编译时未知的文本内容,所以操作系统必须在运行时来请求内存,这一步是通过调用String::from来实现
当用完String之后,需要使用某种方式将内存返回给操作系统,在拥有GC的语言中,GC会跟踪并清理不再使用的内存,比如JAVA,C#。没有GC的话,就需要我们去识别内存何时不再使用,并调用代码将它们返回,如果忘了,那就浪费内存,如果提前做了,那变量会非法,如果做了两次,就会出现严重的bug,针对没有GC的语言,都必须每做一次分配,对应一次释放
Rust采用了不同的方式,对于某个值来说,当拥有它的变量走出作用域时,内存会立即自动交还给操作系统
drop函数
当变量走出作用域后,Rust会自动调用drop函数来释放
变量和数据交互的方式:移动(Move)
多个变量可以与同一个数据使用一种独特的方式进行交互,如: let x = 5; let y = x; 整数是已知且固定大小的简单的值,这两个5会被压到栈中
let s1 = String::from("hello"); let s2 = s1; 这个虽然跟上面的例子很相似,但它们的运行方式是完全不一样的
一个String由3个部分组成:
ptr:一个指向存放字符串内容的内存的指针
len:长度,就是存放字符串内容所需的字节数
capacity:容量,是指String从操作系统总共获得内存的总字节数
这些东西是放在栈中,而存放字符串内容的部分则是在堆上
当把s1赋给s2时,String的数据被复制了一份:在栈上复制了一份指针。长度。容量。但并没有复制指针所指向的堆上的数据
变量离开作用域时Rust会自动调用drop函数,并将变量使用的堆内存释放,当s1和s2离开作用域的时候,它们都会尝试释放相同的内存,就会引起严重的bug:二次释放
为了保证内存安全,Rust没有尝试复制被分配的内存,Rust的做法是让s1失效,这时,就算s1离开作用域的时候,Rust也不需要释放任何东西
如果当s2创建后,再使用s1的话,会报错:
可能你会将复制指针、长度、容量视为浅拷贝,但由于Rust让s1失效了,所以这里用一个新的术语:移动(Move),相当于s1移动到了s2,既然只有s2是有效的,那就只有s2离开自己的作用域时释放内存空间,就避免了二次释放,这就体现了Rust的安全性
克隆
如果真想对堆上面的String数据进行深度拷贝,而不仅仅时栈上的数据,可以使用clone克隆方法,这个以后会细说,现在只是举一个简单的例子: