计算机是一个数据处理设备,CPU负责处理数据,内存负责装载数据,外设负责输入输出数据。对于一门计算机语言,关注的是存储数据的内存管理。以前的计算机语言在提高效率和提高安全性之间总是有取舍,而rust在这方面却别具一格,鱼和熊掌兼得之
内存管理的通用概念
物理内存与虚拟内存
早期CPU直接读写物理内存,但是这种方式不能用于多进程。一个进程使用了 0x100
的地址,另一个相同进程也会用到 0x100
的地址,这样就会起冲突。
随着计算机技术的发展,就出现了虚拟内存,它让每个进程都有自己独立的地址空间;整个虚拟内存由多个页(大小相等的块,一段连续的地址)组成。而且虚拟内存的地址空间分成两部分:用户空间和内核空间(linux是3:1,windows是2:2)
栈
- 栈(stack)
- 一种后进先出的数据结构
- 固定的一端为栈底,运动的一端是栈顶
- 栈顶由
ESP
(extened stack point)寄存器保存 - 栈底由
EBP
(extened base point)寄存器保存
- 栈顶由
- 栈向内存地址减小的方向生长
- 保存函数调用时的数据
stack frame
- 函数的返回地址与参数
- 临时变量
- 非静态局部变量
- 编译器的临时变量
- 上下文
- 在
main
调用函数foo
时- 把返回地址压栈保存
- 把
main
的EBP
压栈 - 进入当前函数
foo
的栈帧 - 预分配参数和临时变量的空间
- 运行
foo
函数 - 执行完毕,变量依次弹出
- 直到
main
函数的EBP
- 跳回
main
函数栈
堆
- 数据结构的堆
- 完全二叉树
- 大顶堆
- 父节点大于两个子节点
- 小顶堆
- 父节点小于两个子节点
- 内存的堆
- 长久保存在内存中的值
- 跨函数保存的内容
内存布局
为了CPU读写内存的高效,要内存对齐。CPU在单位时间能处理的一组比特位称为字,字包含的比特位数称为字长,字节对齐是对齐CPU的字长。
Rust的资源管理
Rust也遵循上面的基本规定,除此之外,Rust在编译期就可以确定内存的分配与释放
变量与函数
常量是用const
定义,没有固定内存地址,会被编译器内联到每个使用它的地方
静态变量用static
定义,有固定的内存地址。不在栈中也不在堆中,而是与程序代码一并存储在静态存储区(随程序的二进制文件的生成被分配的)
为了保证内存安全,Rust会对变量进行一系列检查:
- 检查每个变量是否都初始化了
- 检查
if
每条分支是否都完成初始化了变量let x: i32; if true { x = 1; } println!("{}", x);
else 分支没有完成对x的初始化
- 检查循环中是否完成初始化了变量
- 转移所有权会产生未初始化变量
let y = Box::new(5); let y2 = y;
y 是未初始化变量 首先 y 是智能指针,没有实现Copy,
y2 = y
后y交出了所有权
智能指针与RAII
Rust 内存地址的可以用指针来表示
- 引用
&T
或&mut T
- Rust中的普通指针
- Rust对其安全检查
- 原始指针
*const T
或*mut T
- 不进行安全检查的内存地址
- 智能指针
- 结构体
- 值是内存地址
- 实现了
Drop
和Deref
Deref
重载解引用*
Drop
析构资源
- 结构体
确定性析构
C++
资源管理方式RAII
资源获取即初始化,RAII利用析构函数来回收资源。当变量离开作用域后,资源由变量的析构函数drop
回收。所以RAII也称作作用域界定的资源管理(Scope-Bound Resource Management, SBRM)
实现了
Copy
的类型,没有析构函数
内存泄漏与内存安全
- 未定义内存
- Rust在编译期可以指出
- 空指针
Safe
中,创造不出空指针- Rust不支持将整数转为指针
Option<T>
代表有无情况
- 悬垂指针
- 用生命期解决
- 数组越界检查
内存泄漏是没有对合法的数据进行操作,内存不安全是对不合法的数据进行操作
主动泄漏是因为,Rust通过FFI与外部函数打交道,把数据交由C代码处理,防止Rust调用析构函数引起问题
附录
循环引用
创建一个泛型链表节点
struct Node<T> {
data: T,
next: NodePtr<T>,
}
因为NodePtr
是指针型,所以应该是Box<Node<T>>
,又因为可以为空,所以是Option<Box<Node<T>>>
这样一个循环引用如下:
let mut frist = Box::new(Node {data: 1, next: None});
let mut second = Box::new(Node {data:2, next: None});
frist.next = Some(second);
second.next = Some(frist);
但因为second
是指针,没有实现Copy
,所以Some(second)
会将second这块内存的所有权移到Some()
中。
为了能在不同地方都能持有同一块内存,使用Rc
共享智能指针,所以NodePtr
应该是Option<Rc<Node<T>>>
let frist = Rc::new(Node {data: 1, next: None});
let second = Rc::new(Node {data: 2, next: None});
frist.next = Some(second.clone());
second.next = Some(frist.clone());
但因为frist.next = ...
需要frist
有改写能力,而Rc<T>
是只读共享指针,为了修改内部的数据,必须引入RefCell<T>
,这样NodePtr
应该是Option<Rc<RefCell<Node<T>>>>
let frist = Rc::new(RefCell::new(
Node {data: 1, next: None}
));
let second = Rc::new(RefCell::new(
Node {data: 2, next: None}
));
frist.brrow_mut().next = Some(second.clone());
second.brrow_mut().next = Some(frist.clone());