MENU

【Rust】笔记:什么是所有权

October 24, 2023 • Rust

参考:什么是所有权? - 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)。因为内存分配器返回的指针大小是固定的,因此该指针可以被存储到栈上,不过当需要访问实际的数据时,需要根据指针访问堆上对应的数据。

差异

  1. 入栈比在堆上分配内存要快,因为入栈时直接放到栈顶即可,而堆需要通过内存分配器寻找合适的空间并分配。
  2. 访问堆上的数据比栈上的数据要慢,因为需要先访问栈上的指针,再根据指针访问对应的内存数据。
  3. 栈上的数据会在函数结束时被自动释放掉,但是堆上的数据无法被自动释放

变量作用域

作用域(Scope)指的是一个项(Item)在程序中有效的范围,我们假设有这样一个变量:

let s = "hello";

变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的,这个变量从声明的点开始知道当前作用域结束都是有效的。

{    
    // s 在这里无效,因为它尚未声明
    
    let s = "hello"; // 从此时起,s 是有效的
    
    // 使用 s
} // 此作用域已结束,s 不再有效

也就是说:

  1. 当 s 进入作用域时,它就是有效的
  2. 在 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 类型来说,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时位置大小的内容来存放内容,这就意味着:

  1. 必须在运行时向内存分配器请求内存
  2. 需要一个当我们处理完 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
}
作者:NorthCity1984
出处:https://grimoire.cn/rust/rust-ownership.html
版权:本文《【Rust】笔记:什么是所有权》版权归作者所有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任