生命周期

野指针

C/C++ 编程出现的野指针是非常恼人的东西,而且还非常难以处理。

为什么会有野指针

野指针的出现,主要有以下原因:

  1. 指针变量没有被合理的初始化。C/C++的指针若不被指定,那么其值就是随机的。
  2. 指针被free或者delete之后,仍继续使用该指针。要知道free和delete只是用来释放内存的,可是指针变量依旧存在。
  3. 函数返回函数内栈上声明变量的指针值。函数已经退出,栈上的内存已经被回收,函数返回指针所指向的内容是无效的。

使用以上指针会导致程序出现不可预防的错误,非常危险。

通常常用的解决方案是:

  • 对于情况1:必须初始化,或直接初始化为NULL。
  • 对于情况2:程序进行if (p != NULL)进行预防。
  • 对于情况3:不让这种情况出现。

即便是有以上的一些简单的处理方案,程序中的指针与内存依然非常的难以控制。

理解Rust的生命周期

Rust的生命周期与野指针有着微妙的联系。理解野指针可以帮助我们理解为什么Rust要这样设计,为什么需要生命周期的概念。

Rust使用<'lifetime_name>来标示一个生命周期。其中lifetime_name可以为a,b,c...等任意的名称。

Rust更推荐使用<'a>,<'b>,<'c>这种简单的符号。因为这些标志,代码中实际没有作用,它只是用于向编译器提供信息。

struct中的生命周期

C/C++中可能出现这样一类代码:

struct Face {    // 每个脸都有眼镜。
    Eye *p_eye;
}

在程序中Face的eye指向一个Eye。可是,一旦这个Eye被free。该p_eye就会变成一个野指针。

可以知道,当结构体的成员失效,结构体本身也应该是失效了。
也就是说,必须是结构体实例先被释放,然后成员才能被释放。

来看Rust的例子:

struct Face {
    eye:&Eye;
}

可是如果只这样写,代码无法通过编译。编译器会报"missing lifetime specifier",即声明周期缺失。

因为Face的属性eye借用了Eye。一旦Eye被释放,Face实例是否需要释放呢?编译器不知道。那么这件事应该由程序员来告知编译器。而编译器得知这些信息后,会帮我们检验代码,防止“野指针”的出现。

Rust中的struct若使用了借用,则必须声明生命周期。

代码需要修改为:

struct Face<'a> {
    eye:&'a Eye;
}

Rust在编译过程中会分析结构体实例与其成员的生命周期是否符合生命周期标记。

例:以下是一段编译错误的代码。我们主动让eye成员的实际生命周期小于Face实例。

#[derive(Debug)]
struct Eye;

#[derive(Debug)]
struct Face<'a> {
    eye : &'a Eye
}

fn main() {
    // face的作用域是整个main方法,可是eye0的作用域是{ }。
    let face:Face;
    {
        let eye0 = Eye;
        face = Face {eye: &eye0};
        println!("{:?}",face);
    }
}

eye0先于face被回收(在C/C++中出现野指针),Rust编译器报错。

这段代码编译器报出
error: `eye0` does not live long enough

若结构体成员中存在多个借用,则必须声明多个生命周期。
例:

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

fn main() {
    let a = &5;
    let b = &8;
    let f = Foo { x: a, y: b };

    println!("{}", f.x + f.y);
}

函数的生命周期

若一个函数返回指针。该指针指向只能是以下几种情况:

  • 指针指向全局内存数据。
  • 指针指向参数指向的内存。
  • 指针指向堆内存。但函数外必须释放该指针。

Rust没法返回指针,Rust的函数只能返回“借用”,对一块内存数据的借用。Rust返回借用后,外部变量无法得知该部分内存什么时候应该释放。

所以程序员需要告诉编译器,返回的变量应该什么时候释放内存。编译器无法判断得知。程序员需要告诉编译器,返回的变量的生命周期是和哪个参数绑定在一起的。

对于以下函数,编译器会报错。

fn foo(x: &str, y: &str) -> &str {
    if true {
        x
    } else {
        y
    }
}

编译器不做语句的判断,不执行函数,所以编译器也不知道函数到底返回x还是y。

编译器无法知道返回的借用该何时释放。所以必须声明生命周期。

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

若声明多个生命周期,则编译会出错。

fn foo<'a,'b>(x: &'a str, y: &'b str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

必须声明b生命周期小于a生命周期。声明形式:'b:'a

fn foo<'a,'b:'a>(x: &'a str, y: &'b str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}

results matching ""

    No results matching ""