Skip to content

17.1 内存泄漏

首先,我们设计一个 Node 类型,它里面包含一个指针,可以指向其他的 Node 实例:


rust
struct Node {
    next : Box<Node>
}

接下来我们尝试一下创建两个实例,将它们首尾相连:


rust
fn main() {
    let node1 = Node { next : Box::new(...) }
}

到这里写不下去了,Rust 中要求,Box 指针必须被合理初始化,而初始化 Box 的时候又必须先传入一个 Node 实例,这个 Node 的实例又要求创建一个 Box 指针。这成了“鸡生蛋蛋生鸡”的无限循环。

要打破这个循环,我们需要使用“可空的指针”。在初始化 Node 的时候,指针应该是“空”状态,后面再把它们连接起来。我们把代码改进,为了能修改 node 的值,还需要使用 mut:


rust
struct Node {
    next : Option<Box<Node>>
}

fn main() {
    let mut node1 = Box::new (Node { next : None });
    let mut node2 = Box::new (Node { next : None });

    node1.next = Some(node2);
    node2.next = Some(node1);
}

编译,发生错误:“error:use of moved value:`node2`”。

从编译信息中可以看到,在 node1.next=Some(node2);这条语句中发生了 move 语义,从此句往后,node2 变量的生命周期已经结束了。因此后面一句中使用 node2 的时候发生了错误。那我们需要继续改进,不使用 node2,换而使用 node1.next,代码改成下面这样:


rust
fn main() {
    let mut node1 = Box::new (Node { next : None });
    let mut node2 = Box::new (Node { next : None });

    node1.next = Some(node2);
    match node1.next {
        Some(mut n) => n.next = Some(node1),
        None => {}
    }
}

编译又发生了错误,错误信息为:“error:use of partially moved value:`node1`”。

这是因为在 match 语句中,我们把 node1.next 的所有权转移到了局部变量 n 中,这个 n 实际上就是 node2 的实例,在执行赋值操作 n.next=Some(node1)的过程中,编译器认为此时 node1 的一部分已经被转移出去了,它不能再被用于赋值号的右边。

看来,这是因为我们选择使用的指针类型不对,Box 类型的指针对所管理的内存拥有所有权,只使用 Box 指针没有办法构造一个循环引用的结构出来。于是,我们想到使用 Rc 指针。同时,我们还用了 Drop trait 来验证这个对象是否真正被释放了:


rust
use std::rc::Rc;

struct Node {
    next : Option<Rc<Node>>
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    let mut node1 = Node { next : None };
    let mut node2 = Node { next : None };
    let mut node3 = Node { next : None };

    node1.next = Some(Rc::new(node2));
    node2.next = Some(Rc::new(node3));
    node3.next = Some(Rc::new(node1));
}

编译依然没有通过,错误信息为:“error:partial reinitialization of uninitialized structure`node2`”,还是没有达到目的。继续改进,我们将原来“栈”上分配内存改为在“堆”上分配内存:


rust
use std::rc::Rc;

struct Node {
    next : Option<Rc<Node>>
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    let mut node1 = Rc::new(Node { next : None });
    let mut node2 = Rc::new(Node { next : None });
    let mut node3 = Rc::new(Node { next : None });

    node1.next = Some(node2);
    node2.next = Some(node3);
    node3.next = Some(node1);
}

编译再次不通过,错误信息为:“error:cannot assign to immutable field”。通过这个错误信息,我们现在应该能想到,Rc 类型包含的数据是不可变的,通过 Rc 指针访问内部数据并做修改是不可行的,必须用 RefCell 把它们包裹起来才可以。继续修改:


rust
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    next : Option<Rc<RefCell<Node>>>
}

impl Node {
    fn new() -> Node {
        Node { next : None}
    }
}
impl Drop for Node {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn alloc_objects() {
    let node1 = Rc::new(RefCell::new(Node::new()));
    let node2 = Rc::new(RefCell::new(Node::new()));
    let node3 = Rc::new(RefCell::new(Node::new()));

    node1.borrow_mut().next = Some(node2.clone());
    node2.borrow_mut().next = Some(node3.clone());
    node3.borrow_mut().next = Some(node1.clone());
}

fn main() {
    alloc_objects();
    println!("program finished.");
}

因为我们使用了 RefCell,对 Node 内部数据的修改不再需要 mut 关键字。编译通过,执行,这一次屏幕上没有打印任何输出,说明了析构函数确实没有被调用。

至此,终于实现了使用 Rc 指针构造循环引用,制造了内存泄漏。

本节花费这么多笔墨一步步地向大家演示如何构造内存泄漏,主要是为了说明,虽然构造循环引用非常复杂,但是可能性还是存在的,Rust 无法从根本上避免内存泄漏 。通过循环引用构造内存泄漏,需要同时满足三个条件:1)使用引用计数指针;2)存在内部可变性;

3)指针所指向的内容本身不是'static 的。

当然,这个示例也说明,通过构造循环引用来制造内存泄漏是比较复杂的,不是轻而易举就能做到的。构造循环引用的复杂性可能也刚好符合我们的期望,毕竟从设计原则上来说:鼓励使用的功能应该设计得越易用越好;不鼓励使用的功能,应该设计得越难用越好。

Easy to Use,Easy to Abuse.

对于上面这个例子,要想避免内存泄漏,需要程序员手动把内部某个地方的 Rc 指针替换为 std::rc::Weak 弱引用来打破循环。这是编译器无法帮我们静态检查出来的。

Released under the MIT License