什么是指针(pointer)
指针是一个保存内存地址的变量的通用的概念, 学过C的都知道是怎么回事, 就不画图了, 在C的笔记中画过了
什么是智能指针(smtart pointers)
智能指针是Rust中特殊的指针, 用法类似指针, 但是拥有额外的元数据和功能
Rust 标准库中不同的智能指针提供了多于引用的额外功能, 比如自动解引用, 引用计数等
在 Rust 中, 普通引用和智能指针是有区别的
- 引用是一类只借用数据的指针
- 智能指针拥有它们指向的数据
标准库中最常用的一些智能指针:
- Box<T>: 用于在堆上分配数据
- Rc<T>/Arc<T>: 一个引用计数类型, 他的数据可以有多个所有者
- RefCell<T>/Mutex<T> : 修改 Rust 默认的内部可变性规则
另外可能会涉及 内部可变性
(interior mutability)模式, 这是不可变类型暴露出改变其内部值的 API, 我们也会讨论 引用循环(reference cycles)会如何泄漏内存,以及如何避免
指针指针的特点:
指针指向堆上分配内存空间(如: Box), 普通指针(如:&str) 是引用栈上的数据
实现 Deref 和 Drop 特性
- Deref 自动解引用
- Drop 超出生命周期自动清理(类似垃圾回收机制)
指针与内存
Rust 程序有3个存放数据的内存区域: 数据内存
栈内存
堆内存
用C语言来举例类比, 方便理解
数据内存
对于固定大小和静态(生命周期为程序的整个运行期间)的数据, 这个msg的字节是只读的, 因此它位于数据内存区域中, 编译器对这类数据做了许多优化, 由于内存位置是已知且固定的, 所以编译器使用起来非常快
// 静态变量和常量
const SLOGAN: &str = "Rust is best language";
static APP_SLOGAN: &str = "Hello Rust";
fn main() {
println!("slogan:{}", SLOGAN);
println!("app_slogan:{}", APP_SLOGAN);
}
#include <stdio.h>
static const SLOGAN: char* msg = "C is best language";
int main() {
printf("%s\n", SLOGAN);
return 0;
}
栈内存
对于在函数中声明为变量的数据, 在函数调用期间, 内存的位置不会改变, 以为编译器可以优化代码, 所以栈内存中的数据使用起来也比较快
fn main() {
let num = 10; // 局部变量存在栈中
println!("num={}", num);
}
#include <stdio.h>
int main() {
int num = 10; // 局部变量存在栈中
printf("num=%d\n", num);
return 0;
}
堆内存
对于在程序运行时创建的数据, 此区域中的数据可以添加、移动、删除、调整大小等, 由于它的动态特性, 通常认为它使用起来比较慢, 但是它允许更多创造性的内存使用, 当数据添加到该区域时, 我们称其为分配, 从本区域中删除 数据后,我们将其称为释放
fn main() {
let ptr = Box::new(5); // 这个数据是存在堆上的
println!("ptr={}, num={}", ptr, *ptr); // 智能指针会自动解引用
}
#include <stdio.h>
#include <stdlib.h>
int main() {
// 手动分配内存的数据存在堆上
int *ptr = malloc(sizeof(int));
*ptr = 10;
printf("ptr=%p,num=%d \n", ptr, *ptr);
free(ptr);
ptr = NULL;
}
Box<T>
- Box<T> 是最简单的智能指针, 他允许你在堆内存中存储数据
- 没有额外的性能开销, 但是也没有额外的功能
- 实现了
Deref
和Drop
特性(trait)
适用场景:
- 在编译时, 某个类型的大小无法确定, 但是使用该类型时, 上下文却需要知道他确切的大小
- 当有大量的数据, 想要移交所有权, 但需要确保在操作时数据不会被复制
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
使用 Box 在堆上存储数据
fn main () {
let n = Box::new(5); // 类似C语言的 malloc 函数
println!("n = {}", n);
}
可以使用 Box 创建递归类型
比如创建一个单向链表 LinkedList
struct Node<T> {
value: T,
next: Option<Box<Node<T>>>
}
struct LinkedList<T> {
head: Option<Box<Node<T>>>
}
Deref 特性
通过实现 Deref
这个特性, 就能够重载* 解引用运算
(de-reference operator), 注意, 这个 *
不是乘法的意思, 和 C 语言的解引用是一样的: 获取指针对应内存地址空间的值
解引用获取指针(引用)的值
fn main () {
let num = 5;
let num_ref = # // 指针指向栈上的内存空间
// 用C表示: int* num_ref = #
assert_eq!(num, 5); // 直接取值对比
assert_eq!(*num_ref, 5); // 需要解引用, 获取到引用(指针)对应空间的值然后对比
}
Box 也可以直接解引用
fn main() {
let x = 15;
let xbox_ref = Box::new(x);
// Box::new 指针和上面的指针是不一样的, 堆内存上空间需要
// 手动 malloc 分配, 手动 free 释放
// 用 C 表示: int * xbox_ref = (int *)malloc(sizeof(int));
assert_eq!(x, 15);
assert_eq!(*xbox_ref, 15); // Box 智能指针也可以直接解引用获取值
}
自定义智能指针
手动实现一个 Box, 了解智能指针的原理
初步实现
#[derive(Debug)]
struct MockBox<T>(T);
impl<T> MockBox<T> {
fn new(value: T)-> Self {
Self(value)
}
}
fn main () {
let y = 25;
let mbox_ref = MockBox::new(y);
assert_eq!(y, 25);
assert_eq!(*mbox_ref, 25);// 编译器错误: type `MockBox<{integer}>` cannot be dereferenced
// 默认情况下, 如果一个 struct 实例不是一个智能指针, 它就无法被直接解引用
// 获取到内存中对应的值, 如果需要让这个 struct 变成一个智能指针(也就是可以直接解引用)
// 那么就需要实现 std::ops::Deref 这个特性(也就是: trait)
}
发现不能直接解引用, 需要实现 Deref 特性
use std::ops::Deref;
#[derive(Debug)]
struct MockBox<T>(T);
impl<T> MockBox<T> {
fn new(value: T)-> Self {
Self(value)
}
}
impl<T> Deref for MockBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main () {
let y = 25;
let mbox_ref = MockBox::new(y);
assert_eq!(y, 25);
assert_eq!(*mbox_ref, 25);
// 实现 std::ops::Deref 这个特性以后, 就可以像Box那样, 直接解引用
// 那么换句话说, 我们就实现了一个自己的智能指针 MockBox, 功能和 Box 一样
}
函数/方法的隐式解引用转换
什么是隐式解引用转换
英文: deref coercions
当将一个与函数定义的参数类型不匹配的指针(引用), 作为 参数传函数去调用, Rust 编译器会尝试: 自动将这个指针转换为一个与函数定义 的参数类型匹配的指针(引用), 这个过程就叫做: 隐式解引用转换(Deref Coercion)
- 解引用转换是将一种类型(A)隐式的(也就是不用手动操作的)转换为另外一种类型(B)的引用
- 因为 A 类型实现了 Deref trait, 并且其关联类型是 B 类型(意思是: A 解引用之后, 得到的数据类型是 B 类型)
比如: 解引用转换
可以将 &String
转换为 &str
, 因为类型 String 实现了 Deref trait 并且其关联类型是 &str 也就是说: impl std::ops::Deref for String
中实现的 deref 方法 返回的值是一个 &str 数据类型, 看文档验证下
隐式解引用转换现象
fn say_hi(name: &str) {
println!("Hi, {}!", name);
}
fn main () {
let tom = "tom";
let jack = String::from("jack");
let jerry = Box::new(String::from("jerry"));
say_hi(tom); // 类型: &str
say_hi(&jack); // 类型: &String
say_hi(&jerry); // 类型: &Box<String>
// 为什么可以这样使用呢? 按照道理来说, 这应该类型不匹配才对啊
// 这是因为: 传入的参数都被隐式地解引用转换类型了, 并不是传入
// 什么类型, 函数就使用什么类型
}
能够进行隐式解引用转换的条件
只有实现了 std::ops::Deref 或 std::ops::DerefMut
这两个特性的类型, 才能进行隐式解引用转换
隐式解引用转换的规则
- 一直调用
deref
方法(所以要求必须实现std::ops::Deref
特性)直到类型正确 - 如果到最后无法再调用
deref
方法, 那么就证明不能隐式转换为函数想要的类型
say_hi(tom); // 类型: &str, 不用转换类型
say_hi(&jack);
// 传入类型: &String, 参数类型: &str
// 1. 调用 String deref => &str
say_hi(&jerry);
// 传入类型: &Box<String>, 参数类型: &str
// 1. 先调用 Box deref => String
// 2. 再调用 String deref => &str
解引用转换如何与可变性交互
上述代码传入的引用都是只读的, 如果有可变的引用, 如何进行 deref
解引用操作呢?
与 std::ops::Deref
类似的特性还有一个 std::ops::DerefMut 这个特性就是用于可变引用的
Rust编译器在发现类型和 trait 的实现满足以下任意一种情况时会进行解引用强制转换:
T: Deref<Target=U>
=>从 &T 转为 &U
T: DerefMut<Target=U>
=>从 &mut T 转为 &mut U
T: Deref<Target=U>
=>从 &mut T 转为 &U
Drop 特性
这个特性可以实现类似其他编程语言 垃圾回收
的效果, 但是原理不一样, 查看文档
编程语言的内存管理方式:
- 垃圾回收: 需要一直运行一个程序, 来检查, 所使用的内存是否还需要使用, 不使用了就回收
- Drop: 是 Rust 编译器, 在编译时就分析出代码运行结束的时候, 自动调用
Drop
特性 - Rust 语言的
Drop
特性, 是Rust
编译器在编译时, 自动调用的, 而不是运行时
Rust 既不同于 C/C++ 手动管理释放内存, 也不同于 Go/Java/Python 自动垃圾回收内存, 而是通过编译器在编译时分析并决定, 变量需要何时释放内存, 并且将释放内存的代码自动注入到源代码里, 然后再编译, 那么通过编译后的代码, 就能实现一种 类似垃圾回收的效果
由于, 手动释放内存可能会导致 重复释放
的问题(在C语言中很明显), 所以在 Rust 中 并不允许你手动调用这个特性的 drop 方法, 而是编译器会自动调用这个特性的 drop 方法
#[derive(Debug)]
struct TestDropTrait;
impl TestDropTrait {
fn new() -> Self {
TestDropTrait
}
}
impl Drop for TestDropTrait {
fn drop(&mut self) {
println!("TestDropTrait::drop executed");
}
}
fn main () {
let tdt = TestDropTrait::new();
println!("tdt created, {:?}", tdt);
// tdt.drop();
// 不能手动调用 drop, 会报错 explicit destructor calls not allowed
}
// 当 tdt 离开这个作用域后, 会自动的执行 drop 方法
// 也就是说, 程序执行完 main 方法后会输出: `TestDropTrait::drop executed`
Rc 引用计数指针
什么是引用计数智能指针?
引用计数智能指针英文: reference counting smart pointer
引用计数是指: 当一个对象被多个智能指针指向时, 这个对象被引用的次数
为什么有引用计数智能指针?
大多数情况下数据的所有权事非常明确的, 可以准确的知道那个变量拥有哪个数据的所有权, 但是有些情况, 数据的所有权并不明确, 需要多个变量共享数据的所有权, 这时就需要使用引用计数智能指针
比如: 一栋居民楼, 可以被多个业主共享, 但是这栋居民楼只有一个, 所以这栋居民楼的所有权是不明确的, 因为他不是某一个人的, 需要使用引用计数智能指针来管理
struct Building {
name: String,
}
struct Owner {
name: String,
owned_house_id: u32,
owned_house_in: Building,
}
impl Owner {
pub fn new(name: &str, owned_house_id: u32, owned_house_in: Building) -> Self {
Self {
name: name.to_string(),
owned_house_id,
owned_house_in,
}
}
}
fn main() {
let xfxq = Building {
name: "幸福小区1栋".to_string(),
};
// 默认情况下: 这样做是不行的,
// 创建 zs 的时候, xfxq 的所有权给了 zs 这个对象
// 之后再创建 ls, 此时 ls 是没有 xfxq 的所有权的
let zs = Owner::new("zhang sang", 1101, xfxq);
let ls = Owner::new("li si", 1201, xfxq);
// 难道 一栋楼有一个人买了一套房子之后, 其他的人就不能买其他房子了?
// 这显然是不合理的
}
使用引用计数智能指针共享数据
通过不可变引用, 可以在程序的不同部分之间共享只读的数据
use std::rc::Rc;
struct Building {
name: String,
}
struct Owner {
name: String,
owned_house_id: u32,
owned_house_in: Rc<Building>,
}
impl Owner {
pub fn new(name: &str, owned_house_id: u32, owned_house_in: Rc<Building>) -> Self {
Self {
name: name.to_string(),
owned_house_id,
owned_house_in,
}
}
}
fn show_house_info(o: Owner) {
println!("{}{}室的业主是:{}",o.owned_house_in.name, o.owned_house_id, o.name);
}
fn main() {
let xfxq = Building {
name: String::from("幸福小区1栋")
};
let rc_xfxq = Rc::new(xfxq); // 使用引用计数智能指针
// 此时可以通过引用计数指针来获取现在有多少个引用
println!("{}现在有{}个业主", rc_xfxq.name, Rc::strong_count(&rc_xfxq));
// 此时并没有将 xfxq 的所有权给到 zs 对象
// 而是传入了 rc_xfxq 克隆结果的所有权给了 zs ls ww 对象
// xfxq 是真正存储数据的变量而 rc_xfxq 只是一个指针,
// 因此克隆一个指针速度是非常快的, 而且克隆指针的时候会让计数递增
let zs = Owner::new("zhang sang", 1101, Rc::clone(&rc_xfxq));
let ls = Owner::new("li si", 1201, Rc::clone(&rc_xfxq));
let ww = Owner::new("wang wu", 1501, Rc::clone(&rc_xfxq));
// 此时会有4个引用: 因为 Rc::new 就算1个, 后续每次 clone 会让数字递增1
println!("{}现在有{}个业主", rc_xfxq.name, Rc::strong_count(&rc_xfxq));
show_house_info(zs);
show_house_info(ls);
show_house_info(ww);
}
RefCell 与 内部可变性
内部可变性英文: internal mutability
内部可变性允许你用只读引用去修改数据, 这违背了借用规则(内部使用了 unsafe 来绕过借用检查器)
RefCell 可以在运行时进行借用检查规则, 所谓在运行时进行检查就是告诉编译器, 在编译代码的时候你不要管借用检查了, 让我自己在程序运行的时候来判断引用是否可变
struct MessageManager {
history: Vec<String>,
}
impl MessageManager {
fn new() -> Self {
Self {
history: Vec::new()
}
}
fn send(&self, msg: &str) {
// 这样是不行的, 因为 history 属性是只读的
// 但是这个操作逻辑是没有问题的, 向一个空的 Vec 中添加元素
// error[E0596]: cannot borrow `self.history` as mutable,
// as it is behind a `&` reference
self.history.push(msg.to_string());
}
fn show_history(&self) {
let mut i = 0;
self.history.iter().for_each(|msg| {
i += 1;
println!("{}: {}", i, msg);
});
}
}
fn main() {
let mgr = MessageManager::new();
mgr.send("hello");
mgr.show_history();
}
use std::cell::RefCell;
struct MessageManager {
history: RefCell<Vec<String>>,
}
impl MessageManager {
fn new() -> Self {
Self {
history: RefCell::new(Vec::new()),
}
}
fn send(&self, msg: &str) {
// 在编译时不会检查借用是否可变, 让我自己手动控制
// borrow_mut 表示对 Vec::new 的可变借用
self.history.borrow_mut().push(msg.to_string());
}
fn show_history(&self) {
// borrow 表示对 Vec::new 的只读借用
let mut i = 0;
self.history.borrow().iter().for_each(|msg| {
i += 1;
println!("{}: {}", i, msg);
});
}
}
fn main() {
let mgr = MessageManager::new();
mgr.send("hello");
mgr.send("world");
mgr.show_history();
}
循环引用与内存泄漏
循环引用会导致内存一直无法释放(也就是内存泄漏)
use std::rc::Rc;
use std::cell::RefCell;
#[allow(unused)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}
impl Node {
fn new(value: i32) -> Self {
Self { value, next: None }
}
}
fn main() {
let a = Rc::new(RefCell::new(Node::new(11)));
let b = Rc::new(RefCell::new(Node::new(22)));
a.borrow_mut().next = Some(Rc::clone(&b));
b.borrow_mut().next = Some(Rc::clone(&a));
/* 此时的 a 和 b 就是循环引用, 这会导致引用计数(Rc)永远不会为0,
也就是说这两个引用所对应的内存一直不会被释放, 也就是内存泄漏
a: Node {
value: 11,
next : b
}
b: Node {
value: 22,
next: a
}
*/
}
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
#[allow(unused)]
struct Node {
value: i32,
next: Option<Weak<RefCell<Node>>>,
}
impl Node {
fn new(value: i32) -> Self {
Self { value, next: None }
}
}
fn main() {
let a = Rc::new(RefCell::new(Node::new(11)));
let b = Rc::new(RefCell::new(Node::new(22)));
// convert Rc to Weak: Weak 不会计数
let weak_a = Rc::downgrade(&a);
let weak_b = Rc::downgrade(&b);
a.borrow_mut().next = Some(weak_b);
b.borrow_mut().next = Some(weak_a);
// 如果是 RC, 是无法用 println 输出的,
// 因为循环引用, 会让导致栈溢出问题(stack over flow)
println!("a: {:?}", a);
println!("b: {:?}", b);
}