学习Rust 前置步骤,别着急,我们不会直接开始介绍语法,而会先来回顾那些你平时认为非常基础的知识,比如说内存、函数。
编程语言万变不离其宗,底层逻辑都是不变的。
所以今天我们得回头重新思考,编程中那些耳熟能详却又似懂非懂的基础概念,搞清楚底层逻辑。而且这些概念,对我们后面学习和理解 Rust 中的知识点非常重要,之后,我们也会根据需要再穿插深入讲解。
代码中最基本的概念是变量和值,而存放它们的地方是内存,所以我们就从内存开始。
内存:
先来rust 的hello world ,查看这一句“hello world” 对应的内存使用情况
代码语言:javascript复制let s = "hello world".to_string();
首先,“hello world” 作为一个字符串常量(string literal),在编译时被存入可执行文件的 .RODATA 段(GCC)或者 .RDATA 段(VC ),然后在程序加载时,获得一个固定的内存地址。
当“hello world” .to_string() 的时候,在堆上,一块新的内存被分配出来,并吧 “hello world” 逐个字节拷贝过去。
当我们把堆上的数据赋值给 s 的时候,s 作为栈上的一个变量,需要知道堆上的内存的地址,由于堆上的数据大小不确定且可以增长,我们还需要知道size
最终,为了表述这个字符串,使用了三个word:
第一个,表示指针;
第二个, 表示字符串的当前长度;
第三个,表示这片内存的总容量。
内存变换流程
数据什么时候可以放在栈上,什么时候需啊哟放在堆上呢?
下面来复习一下堆栈的概念
我们使用java 会大概了解内存管理的一些规则
- 基本类型(primitive type)存储在栈上,对象存储在堆上;
- 少量数据存储在栈上,大量的数据存储在堆上。
这两个概念只是初略概念
我们深挖一下堆栈的设计原理:
栈
栈是程序运行的基础。每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存被称为帧(frame)。
我们知道,栈是自顶向下增长的,一个程序的调用栈最底部,除去入口帧(entry frame),就是 main() 函数对应的帧,而随着 main() 函数一层层调用,栈会一层层扩展;调用结束,栈又会一层层回溯,把内存释放回去。
在调用的过程中,一个新的帧会分配足够的空间存储寄存器的上下文。在函数里使用到的通用寄存器会在栈保存一个副本,当这个函数调用结束,通过副本,可以恢复出原本的寄存器的上下文,就像什么都没有经历一样。此外,函数所需要使用到的局部变量,也都会在帧分配的时候被预留出来。
整个过程你可以再看看这张图辅助理解:
栈
一个函数运行,怎么确定需要多大的帧呢?
这要归功于编译器。在编译并优化代码的时候,一个函数就是一个最小的编译单元。(需要生命周期标记的原因)
在这个函数里,编译器得知道要用到哪些寄存器、栈上要放哪些局部变量,而这些都要在编译时确定。所以编译器就需要明确每个局部变量的大小,以便于预留空间。
这下我们就明白了:在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上。比如一个函数,参数是字符串:
代码语言:javascript复制fn say_name(name: String) {}
// 调用
say_name("Lindsey".to_string());
say_name("Rosie".to_string());
字符串的数据结构,在编译时大小不确定,运行时执行到具体的代码才知道大小。比如上面的代码,“Lindsey” 和 “Rosie” 的长度不一样,say_name() 函数只有在运行的时候,才知道参数的具体的长度。
所以,我们无法把字符串本身放在栈上,只能先将其放在堆上,然后在栈上分配对应的指针,引用堆上的内存。
放栈上的问题
从刚才的图中你也可以直观看到,栈上的内存分配是非常高效的。只需要改动栈指针(stack pointer),就可以预留相应的空间;把栈指针改动回来,预留的空间又会被释放掉。预留和释放只是动动寄存器,不涉及额外计算、不涉及系统调用,因而效率很高。
所以理论上说,只要可能,我们应该把变量分配到栈上,这样可以达到更好的运行速度。
那为什么在实际工作中,我们又要避免把大量的数据分配在栈上呢?
在实际调用的时候需要考虑到栈的大小,避免栈溢出(stack overflow),一旦当前程序的调用栈超出了系统允许的最大栈空间,无法创建新的帧,来运行下一个要执行的函数,就会发生栈溢出,这时程序会被系统终止,产生崩溃信息。
过大的栈内存分配是导致栈溢出的原因之一,更广为人知的原因是递归函数没有妥善终止。一个递归函数会不断调用自己,每次调用都会形成一个新的帧,如果递归函数无法终止,最终就会导致栈溢出。
堆上的问题
在使用堆内存的时候,也需要注意内存管理
如果使用手动管理内存空间的时候,需要及时释放内存,以免造成内存泄露,一旦内存泄露,没有及时回收内存,随着程序运行的时间越来越长,内存到最后会被吃满导致系统挂掉。
还有个要点:
假如堆内存被多个线程的调用栈引用,改动内存的时候要特别注意,需要加锁来独占访问,避免潜在问题,
线程很常见的问题比如:
一个线程在遍历List ,而另外一个线程在释放List 中的某一项,这时候就可能会访问野指针(野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。),导致堆越界(heap out of bounds)