所有权与借用
RAII
目前来说主流的资源管理有三种方式:
- 手动管理:C语言、Zig语言
- 垃圾回收:Java语言、Go语言
- 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; }
}
规则
所有权有三条规则需要谨记:
- Rust中每个值都有一个称其为所有者的变量
- 一次只能有一个所有者
- 当超出所有者范围,值就会被释放
原始数据类型(布尔值、字符、整数、浮点数)不遵循以上规则,因为它是直接将值复制,而不是转移所有权。
以下是一些所有权错误例子
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}");
}
借用对比于复制来说,效率更高,消耗资源更少
规则
借用规则有两条:
- 在任何给定时间,你可以拥有一个可变引用或任意多个不可变引用
- 所有引用都必须有效
第一条规则是为了防止多线程情况下,一个可变引用改变了数据,导致其他可变引用或者不可变引用发生数据改变。第二条规则是为了防止悬挂指针。
可变借用例子:
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所有权在函数结束时就已经释放了,但是它还返回一个借用,这个借用指向不确定的地方