参考:什么是所有权? - Rust 程序设计语言 简体中文版 (kaisery.github.io)
在 Rust 中,Rust 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查,如果违反了这些规则, 则程序无法通过编译,同时,在运行时,所有权系统的任何功能都不会减慢程序。在开始详细介绍所有权之前,我们先简单介绍下为什么需要所有权
堆和栈
在计算机系统中,堆(heap)和栈(stack)都是程序运行时常见的可供使用的内存,但是他们的结构不同:
栈内存
栈以放入值的顺序存储值并以相反的顺序取出值,这种行为被称为后进先出(last in,first out),其中添加数据的操作被称为进栈(push onto the stack),移出数据叫做出栈(popping off the stack)。栈的所有数据都必须占用已知且固定的大小。
堆内存
在编译大小未知或大小可能变化的数据,要改为存储在堆上,堆是缺乏组织的内存空间,当向堆放入数据时,需要请求一定大小的空间,内存分配器(memory allocator)根据请求的空间大小在堆上寻找到一块足够大小的空位,并且将其标记为已使用,并且将表示这块空间位置的指针(pointer)返回到程序中。这个过程被称为分配(allocating on the heap)。因为内存分配器返回的指针大小是固定的,因此该指针可以被存储到栈上,不过当需要访问实际的数据时,需要根据指针访问堆上对应的数据。
差异
- 入栈比在堆上分配内存要快,因为入栈时直接放到栈顶即可,而堆需要通过内存分配器寻找合适的空间并分配。
- 访问堆上的数据比栈上的数据要慢,因为需要先访问栈上的指针,再根据指针访问对应的内存数据。
- 栈上的数据会在函数结束时被自动释放掉,但是堆上的数据无法被自动释放
变量作用域
作用域(Scope)指的是一个项(Item)在程序中有效的范围,我们假设有这样一个变量:
let s = "hello";
变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的,这个变量从声明的点开始知道当前作用域结束都是有效的。
{
// s 在这里无效,因为它尚未声明
let s = "hello"; // 从此时起,s 是有效的
// 使用 s
} // 此作用域已结束,s 不再有效
也就是说:
- 当 s 进入作用域时,它就是有效的
- 在 s 离开作用域为止,他都是有效的
内存与分配
String 类型
Rust
提供了 String
类型,这个类型可以管理被分配到堆上的数据,所以可以存储编译时未知大小的文本,可以使用 from
函数基于字符串字面量来创建 String
,如下:
let s = String::from("Hello");
我们可以尝试修改这个字符串:
let mut s = String::from("hello");
s.push_str(", world"); // push_str() 在字符串的后面追加字面值
println!("{s}"); // print ‘hello, world’
String 内存分配
对于 String
类型来说,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时位置大小的内容来存放内容,这就意味着:
- 必须在运行时向内存分配器请求内存
- 需要一个当我们处理完
String
时将内存返还给分配器的方法(释放内存)
第一部分由我们完成,当我们调用 String::from
时,它的实现(implementation)请求其所需的内存,这在编程语言中是非常通用的。
然而,在第二部分实现起来就各有区别了,在有垃圾回收(garbage collector,GC)的语言中,GC记录并且清除不再使用的内存,而我们并不需要要关心它。
在大部分没有GC的语言中,识别出不再使用的内存并调用代码显示释放就是我们的责任了,跟请求内存的时候一样。如果我们忘记回收了,则会导致内存的浪费,并且最终会导致内存泄漏,如果过早回收了,将会出现无效变量,如果重复回收,则会产生二次释放(double free) 问题,因此我们需要精确地为一个 allocate
配置一个 free
。
Rust 采用了一个不同的策略:内存在拥有它的变量离开作用域后就会被自动释放。
{
let s = String::from("hello"); // s 开始有效
// 使用 s
} // s 作用域结束,s 被释放变得不再有效
当 s 离开作用域的时候,Rust 为我们调用一个特殊的函数drop
,在这里 String
的作者可以放置释放内存的代码,Rust 在结尾的 }
处自动调用 drop
。
变量与数据交互的方式(一):移动
在 Rust 中,多个变量可以采取不同的方式与同一数据进行交互。
let x = 5;
let y = x;
上面的这两行代码实现了:将 5 绑定到 x ,接着生成一个值 x 的拷贝并且绑定放到 y,现在我们有了两个变量 x、y,因为整数是已知固定大小的简单值,所以这两个 5 被放入了栈中。
let s1 = String::from("Hello");
let s2 = s1;
这个看起来和上面的代码非常类似,但是因为 String 类型其实是堆上的内存,因此实际上这段代码和上一段代码并不完全一致。
在第一行代码中,将 String 绑定到 s1 实际上是完成了这样的一个操作:
当我们将 s1 赋值给 s2 ,在 C++ 这类的语言下,会是这样的:
s2 仅仅拷贝 s1 的指针信息而不拷贝指针指向的信息,这种被称为浅拷贝,前面我们提到过, 当变量离开作用域后,Rust 自动调用 drop 函数并且清理堆内存,但是,如果按照上图的方式拷贝,那么 s1 和 s2 会指向同一个内存区域,当执行 drop 函数的时候,会导致二次释放错误,两次释放(相同)内存会导致内存污染,他可能会导致潜在的安全漏洞(第一次释放以后内存被分配给其他程序,第二次释放以后其他程序丢失数据)
为了确保内存安全,在 let s2 = s1;
之后,Rust 认为 s1
不再有效,因此 Rust 不需要在 s1
离开作用域后清理任何东西。
如果你在其他语言中听过说浅拷贝(shallow copy)和深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝,但是在因为rust同时使第一个变量无效了,这个操作被称为移动(move),而不是叫浅拷贝。类似下图:
另外,需要注意的是,Rust 永远不会自动创建数据的“深拷贝”,因此,任何自动的复制可以被认为对运行时性能影响较小。
变量与数据交互的方式(二):克隆
如果我们确实需要深度复制 String
堆上的数据,而不仅仅是栈上的数据,可以使用一个叫 clone 的函数:
let s1 = String::from("Hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
只在栈上的数据:拷贝
我们来看下这个代码:
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
这段代码依旧是有效的,尽管没有调用 clone,不过 x 依然有效并且没有被移动到 y 中。原因是像政协这样的在编译时一致到校的类型被整个存储在栈上,所以拷贝其实际的值是快速的,这意味着没有理由在创建变量y后使x无效,换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
所有权与函数
将值传递给函数与给变量赋值的原理类似,向函数传递至可能会移动或者复制,就像复制语句一样
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,
// 所以在后面可继续使用 x
} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 没有特殊之处
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
// 占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处
返回值与作用域
返回值也可以转移所有权
fn main() {
// gives_ownership 将返回值转移给 s1
let s1 = gives_ownership();
// s2 进入作用域
let s2 = String::from("hello");
// s2 被移动到 takes_and_gives_back 中 它也将返回值移给 s3
let s3 = takes_and_gives_back(s2);
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 离开作用域并被丢弃
// gives_ownership 会将返回值移动给调用它的函数
fn gives_ownership() -> String {
// some_string 进入作用域。
let some_string = String::from("yours");
// 返回 some_string 并移出给调用的函数
some_string
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String {
// a_string 进入作用域
// 返回 a_string 并移出给调用的函数
a_string
}
出处:https://grimoire.cn/rust/rust-ownership.html
版权:本文《【Rust】笔记:什么是所有权》版权归作者所有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任