一、堆和栈
栈和堆都是程序运行时使用的内存,但是它们的结构不同。
1.栈
栈,英文是stack。是内存的一段区域。
栈是后进先出形式的。就像薯片桶,先放进去的一片只能后拿出来。
栈上存储的数据大小必须是已知且固定的。也就是说如果一个变量或数据要放到栈上,那么它的大小在编译是就必须是明确的。
例如,类型为i32的变量,它的大小是固定4个字节。
2.堆
堆,英文是heap。是内存的另一段区域。堆内存也叫做资源。
堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针。这个过程称作在堆上分配内存。因为指针大小是已知且固定的,所以可以将该指针存储在栈上,不过当需要实际数据时,必须去指针指向的内存读取数据。就像一个围棋棋盘,你可以把一枚棋子放到任意可以放得下的位置。
在编译时大小未知或大小可能变化的数据,要存储在堆上。
堆不受系统管理,由用户自己管理,因此,使用不当,内存溢出的可能性就大大增加了。
Rust与其他语言的区别
(1)指针超出作用域会自动释放堆内存:
对于简单类型的栈内存(如int)超出作用域后自动释放,这个功能各个语言都有。而对于new出来的堆内存,在c/c++中要手动释放,在java中要委托垃圾回收释放。而垃圾回收不是实时的,会影响性能,手动释放又总会有人忘记释放。而Rust对栈内存和堆内存一视同仁,超出作用域一律自动释放,相当于自动delete指针。所以rust避免了内存泄漏。
在作用域结束时释放资源的模式称作资源获取即初始化(Resource Acquisition Is Initialization (RAII))。
rust强制使用raii,所以任何对象在离开作用域时,它的析构函数就被调用,然后它占有的资源就被释放。
这避免了资源泄漏,所以你再也不用手动释放内存或者担心内存泄漏。
(2)所有权:
某段内存只能被最后的变量名所有,前面声明过的变量都作废,这样一段内存就只有一个所有者,只有所有者可以释放这块内存。这有效避免了被多个变量释放的问题,而且该操作是在编译期的,这可在编译期就能避免空指针问题。比如c++中,a和b指向同一块内存,如果delete a之后,再delete b就会出错,而rust中不会出现这种问题。
二、所有权
(一)所有权是什么
所有权是指对内存资源的控制权和管理权。
在Rust中,每个值都有一个唯一的所有者。定义一个变量就是声明这个值由这个变量所有。所有的值最终都要存储在一块内存上,变量拥有这个值其实是拥有这块内存。栈内存所有者就是声明时的变量,而堆内存所有者是分配返回的指针。
只有所有者才能释放这块内存,其他人不能释放这块内存。
当所有者超出作用域时,会自动释放这块内存。
所有权的规则:
1.Rust中的每一个值都有一个所有者。
2.值在任一时刻有且只有一个所有者。
3.当所有者离开作用域,这个值将被丢弃。
比如,
现在可以把Box当成一个指针,后面章节再讲解Box用法。
fn create_box() {
let _box1 = Box::new(3i32);// 在堆上分配一个整型数据
// `_box1` 在这里被销毁,内存得到释放。如果是c++,就得手动delete,否则就会内存泄漏。这就是区别。
}
fn main() {
let _box2 = Box::new(5i32);// 在堆上分配一个整型数据
// 嵌套作用域:
{
let _box3 = Box::new(4i32);// 在堆上分配一个整型数据
// `_box3` 在这里被销毁,内存得到释放
}
// 创建一大堆 box 完全不需要手动释放内存!
for _ in 0u32..1000 {
create_box();
}
// `_box2` 在这里被销毁,内存得到释放
}
(二)转让所有权
所有权可以转让。转让所有权也叫move。
就是由新变量拥有内存,旧变量变成无效的。
s1转让给s2,就像下图所示。s2拥有堆内存,s1变无效。
Rust语言中转让所有权的方式有以下几种:
1.把一个变量赋值给另一个变量。
2.把变量值传递给函数作为参数。
3.函数中返回一个变量作为返回值。
1.把一个变量赋值给另一个变量
fn main(){
let a = Box::new(5i32);// a 是一个指向堆分配的整数的指针
let b = a;// 移动a到b,把a的指针地址(而非数据)复制到b。现在是b拥有堆内存,a变无效。
//println!("a contains: {}", a);// 报错!a无效,因为它不再拥有那部分堆上的内存。
}
2.把变量值传递给函数作为参数。
值传递方式,值的所有权也会发生变更
fn destroy_box(c: Box<i32>) {
println!("Destroying a box that contains {}", c);
// c 被销毁且内存得到释放
}
fn main() {
let a = Box::new(5i32);// a 是一个指向堆分配的整数的指针
destroy_box(a);// a的所有权转移给函数形参,a变无效
//println!("a contains: {}", a);// 报错!a无效
}
3.函数中返回一个变量作为返回值
函数的形参获得的所有权将在离开函数后就失效了。失效的数据就再也访问不到了。
为了解决所有权失效的问题,我们可以让函数将所有者返回给调用者。
fn destroy_box(c: Box<i32>) ->Box<i32> {
println!("Destroying a box that contains {}", c);
c
}
fn main() {
let mut a = Box::new(5i32);// a 是一个指向堆分配的整数的指针
a = destroy_box(a);// a的所有权转移给函数形参,a变无效
println!("b contains: {}", a);// 报错!a无效
}
(三)复刻
复刻,英文是clone。也叫深复制或深拷贝。
有时候,我们需要创建一个值的完全独立的副本,而不是转让所有权。在这种情况下,可以使用复刻。
创建s1的副本s2,就像下图所示。s1和s2都拥有了独立的所有权。
示例:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{} {}", s1, s2); // 正常打印 "hello hello"
}
创建了字符串"hello"的副本,赋值给s2,因此s1和s2都拥有了独立的所有权。
fn main() {
let a = Box::new(5i32);
let b = a.clone();
println!("{}", a);
println!("{}", b);
}
栈上的数据
看下例
fn main() {
let a = 50;
let b = a;
println!("{}", a);
println!("{}", b);
}
这段代码能编译通过。按照所有权转让规则的话,它应该编译错误才对,可是为什么能编译通过?
因为像整型这样的类型完全存储在栈上,并不需要占用那么大的内存,所以拷贝它的值是很快的。没有理由在创建变量b后使a无效。这里没有深浅拷贝的区别,所以这里不管是否调用clone,效果都一样。
Rust有一个Copy trait,可以用于存储在栈上的类型。如果一个类型实现了Copy trait,那么就不使用所有权转让,而是使用复刻。
那么哪些类型实现了Copy trait呢?
Rust不允许自身或其部分实现了Drop trait的类型使用Copy trait。
任何简单标量值的组合都可以实现Copy,任何不在堆上分配内存的类型都可以实现Copy。
如下是一些Copy的类型:
1.布尔类型,bool
2.数字类型,包括整数和浮点数,比如 u32,f64。
3.字符类型,char
4.元组,当且仅当,其包含的类型都实现Copy的时候。比如,(i32, i32) 实现了Copy,但 (i32, String) 就没有。