Rust中级教程:指针生态(引用、原始指针、智能指针)and内存操作(Stack、Heap)

  • Post author:
  • Post category:其他




指针的一些概览知识点



1.内存地址:指代内存中单个字节的一个整数。

指针(原始指针):就是指向某种类型的一个内存地址。

引用:就是指针,是rust提供的一种抽象,如果是动态大小,就是一个指针和一个具有额外保证的一个整数。



2.引用的大小和指针一样,它的大小用usize来表示,和系统的位数有关,在32位cpu上是4字节,在64位cpu上是8字节。
static B:[u8;2]=[11,22];
let b:Box<[u8]>=Box::new(B);
//这里的B的所有权跟着Box转移到b里面了。指针的大小比引用大一倍,Box会获取所有权,复制操作?
//指向的地址发生变化,static是在静态内存里面,而Box放在堆内存上了。
let c:&[u8;2]=&B;
//这样写就是普通的引用,指向的地址就是B的地址。

在这里插入图片描述



3.Cow:copy on write,在需要写的时候才会复制,否者只读不复制,

原始指针的操作需要在unsafe里面,下面就是原始指针,*mut T 和

const T,原始指针没有Rust标准保障的内存地址。

比如

mut String就是指向String类型的原始指针是可变的。原始指针就是操作方便,效率高。

//这个代码的作用是将一个数组里的数字解码成字符串。
let a:i64=42;
let b:String;
let c:Cow<str>;

let a_ptr:*const i64=&a as *const i64;
let b_ptr:*mut u8=&B as *const u8 as *mut u8;//原始指针,先变成不可变的,再变成可变的。
b=String::from_raw_parts(buf:b_ptr,length:10,capacity:10);

let c_ptr:*const i8=&C as *const u8 as *const c_char;
c=CStr::from_ptr(c_ptr).to_string_lossy();


4.解引用:dereference,指针生态

因为在底层上,引用被实现为原始指针,但引用带有额外的保证,所以尽量使用引用而不是原始指针。

访问原始指针的值总是unsafe的,并且是不拥有所有权的,允许多个原始指针指向统一数据。

什么时候使用原始指针?

不可避免的时候:和C交互。和共享对某些内容的访问至关重要,运行时性能要求高。

在这里插入图片描述

在这里插入图片描述



内存简介



1.值,变量:存储值的地方,存在于stack上面,槽。

变量的高级模型:当变量被访问的时候,可以从变量的上次访问到这次访问画一条线,从而在两次访问之间建立了依赖关系。

如果变量没有被初始化,或者变量被移动了,就不能从它那画线了。

使用这种模型,程序会出现很多的依赖关系,依赖线,叫做flow。

每个flow都在追踪一个值的特定实例的生命周期,当有分支存在的时候,flow可能分叉或合并,每个分成都在追逐该值的不同生命周期。

在程序中的任何给定点,编译器可以检查所有的flow是否相互兼容、并行存在。

例如:一个值不可能有两个具体可变访问的并行flow;也不能一个flow借用了一个值,但却没有flow拥有改值。



2.变量的低级模型

多次赋值会被覆盖。



3. 内存区域

三个比较重要的区域:stack、heap、static内存。

想要把数据放在Stack,编译器必须知道类型的大小。

1.Stack Frame(栈帧):每个函数被调用,在stack的顶部会分配一个连续的内存块,叫做stack frame,每个frame的大小是不一样的。

接近Stack底部附近的是main函数的栈帧,随着函数的调用,其余的frame都被推到栈上。

函数的frame包含函数的所有变量,以及函数所带的参数。当函数返回时,它的frame就会被回收。但是构成函数本地变量值的那些字节不会被立即擦除,访问不安全,后续有可能会被重写。

在函数调用期间,Stack Frame会包含函数的状态,当一个函数在另外一个内调用时,原来的函数的值会被及时冻结。

Stack Frame为函数参数,指向原来调用栈的指针以及本地变量(不包括在heap上分配的数据)提供空间。

为什么stack快呢?因为函数的所有变量在内存里都是紧紧挨着的。

例子:满足传递&str类型和String类型同时传递,一个在stack上,一个在heap上。

fn main(){
let pw:&str="juskl";
let is_strong:bool=is_strong(pw);
}
fn is_strong<T:Into<String>>(password:T)->bool{
password.into().len()>5}

任何在Stack上的frame里存储的变量,在frame消失之后,他就无法访问了。所以任何到这些变量引用的生命周期,最多只能和frame一样长。

2.Stack Pointer(栈指针):随着程序的进行,CPU里有一个游标会随着更新,他反应的是当前stack frame的当前地址。随着函数的不断调用,stack增长或者减少。

3.heap内存

heap意味着混乱。是一个内存池,并没有绑定到当前程序的调用栈,是为在编译时没有已知大小的类型准备的。

什么叫在编译时大小不已知呢?一些类型随着时间会变大变小,例如String,Vec。另一些类型的大小不会改变,但无法告诉编译器需要分配多少内存,这些都叫做动态大小的类型。还有就是trait对象,通过模拟动态语言,允许将多个类型放进一个容器。

heap允许你显式的分配连续的内存块,当这么做的时候,你会得到一个指针,它指向内存块开始的地方。里面的值会一直有效,直到你显式的对它释放。如果你想让值活得比当前函数frame的生命周期长,就很有用。

这种值活得长的方法在stack里面也可以使用,如果值是函数的返回值,那么调用函数可以在它的stack上留一些空间给被调用的函数让它把值在返回前写入进去。

4.heap线程安全的例子

如果想把值送到另一个线程,当前线程可能根本无法与那个线程共享stack frame,你就可以把它放在heap上。

因为函数返回时heap上的分配并不会消失,所以你在一个地方为值分配内存,把指向它的指针传递给另一个线程,就可以让那个线程继续安全的操作这个值。

换一种说法:当你分配heap内存时,结果指针会有一个无约束的生命周期,你的程序让他话多久都行。

5.heap交互机制

heap上的变量,都必须通过指针来访问。想要进行同类型操作,需要解引用。

Rust里与Heap交互的首要机制就是Box类型。当Box::new(value)时,值就会放在heap上,返回的(Box)就是指向heap上的值的指针。当Box被丢弃的时候,内存就会被释放。

如果忘记释放内存,就会导致内存泄漏。drop(a),在生命周期结束之前进行释放,这里的a是放在stack上的,释放之后堆上的数据不会立刻释放,只是标记为可以使用覆盖。

6.static静态内存

static是指的是程序编译之后的文件中几个密切相关的区域。static内存里的值会在你的程序的整个执行过程一直存活,通常是只读的。

‘static生命周期,例如T:’static表示类型T可以存活我们想要的任意时长,直到程序结束。同时要求T是拥有所有权的和自给自足的,意思就是它不借用其他(非static)值,或者它借用的东西也都是static的。

const和static的区别:const只是一个名字,没有任何内存或者存储,在编译期间被替换为某个特定的值。



内存第二部分



1.动态内存分配

当程序需要更多的内存时,就需要从OS请求。dynamic allocation

首先,通过系统调用从OS请求内存,alloc()。

然后使用分配的内存,

最后释放不再需要的内存给OS,free()。



2.为什么Stack和Heap存在性能上的差异?

Stack和heap只是概念而已,内存在物理上没有这两个区域。

访问stack快:函数本地变量在RAM上彼此相邻(连续布局),连续布局对缓存友好。

访问heap慢,分配在heap上的变量不太可能彼此相邻。访问heap上的数据涉及对指针进行解引用(页表查找和去访问主存)

stack上的数据结构在生命周期内大小不能变,heap上的数据结构更加灵活,因为指针可以改变。



3.虚拟内存

程序的内存视图,程序可访问的所有数据都是由操作系统在其地址空间提供。

segmentaation fault:当cpu或os检测到程序试图请求非法(禁止访问)的内存地址时所产生的错误。

segment:虚拟内存中的块,虚拟内存被划分为块,以最小化虚拟和物理地址之间转换所需的空间。

访问越界的内存,OS就会关掉你的程序。

程序里所用的地址都是虚拟地址,虚拟地址都会被转化为物理地址。



版权声明:本文为phthon1997原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。