文章目录
- Rust所有权
- 所有权规则
- 作用域
- 内存和分配
- 移动与克隆
- 栈空间
- 堆空间
- 关于函数的所有权机制
- 作为参数
- 作为返回值
- 引用与租借
- 垂悬引用
Rust所有权
C/C++中我们对于堆内存通常需要自己手动管理,手动申请和释放,即便有了智能指针,对于效率的影响和安全性问题也没有完全解决
Rust为了高效的使用和管理内存,以及对安全性的考量,提出了所有权的概念以及一系列规则
所有权规则
所有权有三条核心规则
- Rust中的每个值都有一个隐含的变量,称为所有者
- 一个值 同一时刻只能有一个所有者
- 当所有者离开作用域时,值会被丢弃(调用drop函数释放资源)
fn main() {let s1 = String::from("Hello"); // s1 是 "Hello" 的所有者let s2 = s1; // s1 的所有权被转移给 s2,s1 失效// println!("{}", s1); // ❌ 编译错误:s1 已经失去所有权println!("{}", s2); // ✅ s2 仍然是 "Hello" 的所有者
} // s2 离开作用域,"Hello" 被释放
作用域
这里的作用域和C/C++的作用域基本类似
{// 在声明以前,变量 s 无效let s = "runoob";// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
内存和分配
Rust是静态语言,这意味着我们无法像C++那样运行时扩容,例如vector会在满时进行扩容
在C++中=
是赋值的意思,理解就是将一个变量的值,赋值给一个新的变量,这是一种拷贝语义(Copy)
但是在Rust中,变量和数据交互的方式主要是移动(Move)和克隆(Clone)
移动与克隆
如果你学过C++11,那你一定知道移动语义,例如移动拷贝或者移动赋值
这实际上是一种所有权的转移,主要是避免频繁申请和释放堆空间
但是有一些情况,我们并不希望只是所有权的转移,而是真的创建一个副本进行操作,这就需要使用clone()
方法
fn main() {let s1 = String::from("hello");let s2 = s1.clone(); // 深拷贝println!("s1: {}, s2: {}", s1, s2); // ✅ 仍然可以使用 s1
}
栈空间
在栈空间内,Rust变量“移动”的方式其实就是复制,因为栈空间内基本上都是基本数据类型的,通常占用空间和复制时间不会很久,就会是直接复制,这时候两个变量都是可以使用的
let x = 1;let y = x;println!("{x}, {y}");
"基本数据"类型有这些:
- 所有整数类型,例如 i32 、 u32 、 i64 等。
- 布尔类型 bool,值为 true 或 false 。
- 所有浮点类型,f32 和 f64。
- 字符类型 char。
- 仅包含以上类型数据的元组(Tuples)。
堆空间
String对象的hello
存储的位置可就不是栈空间了而是堆空间
例如
let s1 = String::from("hello");
let s2 = s1;
当执行到第二步时,s1就会把自己对hello字符串对所有权移交给s2,此时s1,就会失效
因此在移动之后,继续使用s1会报错
关于函数的所有权机制
作为参数
当一个变量作为参数传递给函数时,所有权应该怎么处理
fn main() {let s = String::from("hello");// s 被声明有效takes_ownership(s);// s 的值被当作参数传入函数,相当于s的所有权移交给函数了// 所以可以当作 s 已经被移动,从这里开始已经无效let x = 5;// x 被声明有效makes_copy(x);// x 的值被当作参数传入函数// 但 x 是基本类型,依然有效// 在这里依然可以使用 x 却不能使用 s} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放fn takes_ownership(some_string: String) { // 一个 String 参数 some_string 传入,有效println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放fn makes_copy(some_integer: i32) { // 一个 i32 参数 some_integer 传入,有效println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放
作为返回值
fn main() {let s1 = gives_ownership();// gives_ownership 移动它的返回值到 s1let s2 = String::from("hello");// s2 被声明有效let s3 = takes_and_gives_back(s2);// s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.fn gives_ownership() -> String {let some_string = String::from("hello");// some_string 被声明有效return some_string;// some_string 被当作返回值移动出函数
}fn takes_and_gives_back(a_string: String) -> String { // a_string 被声明有效a_string // a_string 被当作返回值移出函数
}
引用与租借
这里的引用和C++中的引用是类似的,如果不了解C++的引用,也可以认为是一种指针
例如
fn main() {let s1 = String::from("hello");let s2 = &s1;println!("s1 is {}, s2 is {}", s1, s2);
}
按照原先的理解,s1内部会存一个指向"hello"的指针,s2内部其实也是一个指向"hello"的指针,但是s2是后来的,我们就认为s2是s1的一个引用,也就是别名
- 引用可以认为是单独的一种类型
- 引用不会获得值的所有权
- 引用只能租借(Borrow)值的所有权
- 当一个值被移动时,原先的引用会失效,必须重新租借所有权
fn main() {let s1 = String::from("hello");let mut s2 = &s1;let s3 = s1;s2 = &s3; // 重新从 s3 租借所有权println!("{}", s2);
}
一般的租借引用是不允许修改数据内容的,除非原先的数据是mut的,引用时也是mut引用,才允许修改
let mut s1 = String::from("hello");
let s2 = &mut s1;
此时s2允许修改s1的内容
可变引用和不可变引用除了权限不同以外,可变引用不允许多重引用(多个变量引用同一个值),但是不可变引用是允许的
这样做主要是为了避免同时有多个使用者可以进行写操作
垂悬引用
这个其实就是对应着空指针的概念(已经释放资源的指针也算),例如
fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}
s是在函数里申请的,函数结束自动释放,但是返回了s的引用,被main函数接收到了,这里就相当于得到了一个空指针