所有权与借用

c3n1g / 2024-10-11 / 原文

RAII

目前来说主流的资源管理有三种方式:

  1. 手动管理:C语言、Zig语言
  2. 垃圾回收:Java语言、Go语言
  3. RAII/所有权机制:C++、Rust

RAII模式全称是Resource Acquisition Is Initialization(资源获取即初始化),是由C++之父提出的一种编程思想,它是一种介于手动管理和垃圾回收之间的一种方法,在创建对象是时就申请好资源,在销毁对象时自动回收资源,以此达到即有垃圾回收一样的资源安全,也有手动管理一样的高利用率。而所有权是基于RAII实现的一种机制,所以要熟悉所有权,首先要知道RAII思想。

按照过去的手动管理方式创建如下代码

class Car {};

void example() {
    Car* car = new Car;
    func_maybe_throw();
    if(!should_continue()) return;
    delete car;
}

可以很明显的看出如果func_maybe_throw或者should_continue出现问题,将不会删除car所占内存,这就会导致内存泄露。或者如果有多个Car指针指向同一个Car对象,那么就会造成所有者不明的情况,清理内存时就不知道根据哪个所有者来释放内存。

如果采取了RAII思想,则代码如下

class CarManager {
    private:
    	Car *p;
    public:
    	CarManager(Car* p): p(p) {}
    	~CarManager() {
            delete p;
        }
}

class Car {};

void example() {
    CarManager car = CarManger(new Car);
    func_maybe_throw();
    if(!should_continue()) return;
}

Car对象在CarManager构造函数中初始化完成,由于CarManager分配的是栈内存,在函数结束时会调用CarManager的析构函数来释放内存,所以不管func_maybe_throw和should_continue是否出问题,都会调用析构函数来释放内存。

当然C++标准库已经提供了更加方便的智能指针来实现RAII,例如unique_ptr和shared_ptr,unique_ptr指针独自拥有指向堆内存的所有权,shared_ptr指针则是通过引用计数器来实现多个对象拥有堆内存所有权。

void example() {
    unique_ptr<Car> car = make_unique<Car>();
    // unique_ptr<Car> car2 = car;	这就会导致错误,因为unique所有权只能有一个
    unique_ptr<Car> car2 = move(car);	// 这样是将car所有权给了car2
    func_maybe_throw();
    if(!should_continue()) return;
}
void example() {
    shared_ptr<Car> car = make_shared<Car>();
    shared_ptr<Car> car2 = car;	// 共享一个对象
    func_maybe_throw();
    if(!should_continue()) return;
}

所有权

Rust采用的所有权机制来管理内存,其实所有权机制和C++的RAII实现类似,只不过Rust在语言层面实现了RAII,例如

struct Car {}

fn example() {
	let car = Box::new(Car {});	// Box智能指针在堆分配内存
    let car2 = car;	// 相当于let car2 = move(car)
    func_maybe_panic();
    if(!should_continue()) { return; }
}

这段代码和使用unique_ptr代码效果一样,car获取了Car对象的所有权,然后再把所有权转移给了car2,Rust代码看起来比C++更加的简便,是因为它在语言的层面实现了unique_ptr、move等东西

与shared_ptr对应的rust代码可以写为

struct Car {}

fn example() {
	let car = Rc::new(Car {});	// Rc智能指针,使用计数器管理
    let car2 = car.clone();	// clone方法是生成一个新的智能指针指向相同的堆,并将引用计数加一
    func_maybe_panic();
    if(!should_continue()) { return; }
}

规则

所有权有三条规则需要谨记:

  1. Rust中每个值都有一个称其为所有者的变量
  2. 一次只能有一个所有者
  3. 当超出所有者范围,值就会被释放

原始数据类型(布尔值、字符、整数、浮点数)不遵循以上规则,因为它是直接将值复制,而不是转移所有权。

以下是一些所有权错误例子

fn main() {
    let s1 = String::from("hello world");
    let s2 = s1;	// 所有权转移
    println!("s1 is: {s1}");	// 违反第二个规则
}
fn main() {
	{
    	let s1 = String::from("hello world");
	}	// s1超出范围,内存释放
    println!("s1 is: {s1}");	// 违反第三个规则 
}
fn show(s: String) -> String {
    println!("s is: {s}");
    String::from("haha")	// 返回新string的所有权
}	// 取得了s的所有权,超出范围释放资源

fn main() {
    let s1 = String::from("hello world");
	show(s1);	// 所有权转移
    println!("s1 is: {s1}");	// 违反第二个规则 
}

借用

在上面例子中,当把变量传递给函数后就转移了所有权,但是如果不想在函数里释放所有权,那么有两种方式来实现,第一种是对变量进行复制

fn show(s: String) {
    println!("s is: {s}");
}

fn main() {
    let s1 = String::from("hello world");
	show(s1.clone());	// 复制了s1,传递进函数
    println!("s1 is: {s1}");
}

还有一种方式就是借用,简单理解就是获取变量的临时所有权,在超出范围后并不会释放所有权,因为它不具有其实际所有权

fn show(s: &String) {
    println!("s is: {s}");
}

fn main() {
    let s1 = String::from("hello world");
    let s2: &String = &s1;
	show(s2);	// 传递s1的借用
    println!("s1 is: {s1}");
}

借用对比于复制来说,效率更高,消耗资源更少

规则

借用规则有两条:

  1. 在任何给定时间,你可以拥有一个可变引用任意多个不可变引用
  2. 所有引用都必须有效

第一条规则是为了防止多线程情况下,一个可变引用改变了数据,导致其他可变引用或者不可变引用发生数据改变。第二条规则是为了防止悬挂指针。

可变借用例子:

fn add(mut s: &String) {
    s.push_str("haha");
}

fn main() {
    let mut s1 = String::from("hello world");	// 可变引用需要申明变量为可变
    let s2: &mut String = &s1;
	show(s2);	// 传递可变引用
    println!("s1 is: {s1}");
}

错误例子:

fn add(mut s: &String) {
    s.push_str("haha");
}

fn show(s: &String) {
    println!("s is: {s}");
}

fn main() {
    let mut s1 = String::from("hello world");
    let s2: &mut String = &s1;
    let s3: &String = &s1;
	show(s3);	// 出错,违反第一条规则,同时有可变变量s2和不可变变量s3
    add(s2);
    println!("s1 is: {s1}");
}

解决方法

fn add(mut s: &String) {
    s.push_str("haha");
}

fn show(s: &String) {
    println!("s is: {s}");
}

fn main() {
    let mut s1 = String::from("hello world");
    let s3: &String = &s1;
	show(s3);	// rust会意识到到这里为止,s3就已经用完了
    let s2: &mut String = &s1;	// 从这里开始范围就没有s3定义了
    add(s2);
    println!("s1 is: {s1}");
}

错误例子2:

fn ret() -> &String {
    let s = String::new("hello world");
    &s
}	// 违反第二条规则,s所有权在函数结束时就已经释放了,但是它还返回一个借用,这个借用指向不确定的地方