析构函数

在介绍堆时,我们提到你需要负责释放你分配的内存。在介绍借用检查器时,我们也说过,在Rust中你很少需要直接管理内存。

这两个陈述起初可能看似矛盾。让我们通过引入作用域析构函数来看看它们是如何结合在一起的。

作用域

变量的作用域是指Rust代码中该变量有效或存活的区域。

变量的作用域从其声明开始。当以下之一发生时结束:

  1. 变量被声明的代码块(即{}之间)结束
    fn main() { // `x` is not yet in scope here let y = "Hello".to_string(); let x = "World".to_string(); // <-- x's scope starts here... let h = "!".to_string(); // | } // <-------------- ...and ends here
  2. 变量的所有权转移到其他人(例如,函数或其他变量)
    fn compute(t: String) { // Do something [...] } fn main() { let s = "Hello".to_string(); // <-- s's scope starts here... // | compute(s); // <------------------- ..and ends here // because `s` is moved into `compute` }

析构函数

当一个值的所有者超出作用域时,Rust会调用其析构函数。析构函数试图清理该值使用的资源,尤其是它分配的内存。

你可以通过将值传递给std::mem::drop来手动调用析构函数。这就是为什么Rust开发者常会说某个值已经被丢弃,以此来表达一个值已经超出作用域且其析构函数已被调用。

可视化解构点

我们可以通过插入显式的drop调用来“拼写”出编译器为我们所做的操作。回到之前的例子:

fn main() { let y = "Hello".to_string(); let x = "World".to_string(); let h = "!".to_string(); }

这等同于:

fn main() { let y = "Hello".to_string(); let x = "World".to_string(); let h = "!".to_string(); // Variables are dropped in reverse order of declaration drop(h); drop(x); drop(y); }

来看第二个例子,其中s的所有权被转移到compute

fn compute(s: String) { // Do something [...] } fn main() { let s = "Hello".to_string(); compute(s); }

它等同于:

fn compute(t: String) { // Do something [...] drop(t); // <-- Assuming `t` wasn't dropped or moved // before this point, the compiler will call // `drop` here, when it goes out of scope } fn main() { let s = "Hello".to_string(); compute(s); }

注意区别:尽管在main中调用computes不再有效,但在main中并没有sdrop(s)。 当你将值的所有权转移到函数时,你也正在转移清理它的责任

这确保了一个值的析构函数至多被调用一次,设计上防止了双重释放漏洞

丢弃后的使用

如果你尝试在值被丢弃后使用它会发生什么?

#![allow(unused)] fn main() { let x = "Hello".to_string(); drop(x); println!("{}", x); }

如果你尝试编译这段代码,你会收到错误:

#![allow(unused)] fn main() { error[E0382]: use of moved value: `x` --> src/main.rs:4:20 | 3 | drop(x); | - value moved here 4 | println!("{}", x); | ^ value used here after move }

Drop消耗掉它被调用的对象,意味着调用后该对象不再有效。因此,编译器会阻止你使用它,避免了释放后使用漏洞

引用的丢弃

如果变量包含引用会怎样? 例如:

#![allow(unused)] fn main() { let x = 42i32; let y = &x; drop(y); }

当你调用drop(y)...什么也没发生。如果你真的尝试编译这段代码,你会收到警告:

warning: calls to `std::mem::drop` with a reference instead of an owned value does nothing --> src/main.rs:4:5 | 4 | drop(y); | ^^^^^-^ | | | argument has type `&i32` |

这回到了我们之前所说:我们只想调用一次析构函数。你可能对同一个值有多个引用——如果我们中的一个超出作用域时就调用它们指向的值的析构函数,其他引用会怎么样? 它们会指向一个不再有效的内存位置:一个所谓的悬挂指针,是释放后使用漏洞的近亲。Rust的所有权制度从设计上排除了这类漏洞。

参考资料

  • 本节练习位于 exercises/03_ticket_v1/11_destructor
1

Rust不保证析构函数一定会执行。例如,如果你选择故意泄露内存,它们就不会执行。