Skip to content

Rust

String和&str

  对于字面量"hello"来说,并不是先在内存中以String类型的方式存储"hello",然后再创建该String数据的引用来得到了一个&str的。

  编译器对字符串字面量做了特殊处理:编译器编译的时候直接将字符串字面量以硬编码的方式写入程序二进制文件中,当程序被加载时,字符串字面量被放在内存的某个位置(不在堆中也不在栈中,而是在类似于静态数据区的全局字面量区)。当程序执行到let s="hello";准备将其赋值给变量s时(注:s在栈上),直接将字面量内存区的该数据地址保存到&str类型的s中。

alt text

  理解了这一点,再理解let s = String::from("hello");这样的代码就很容易了。编译器将"hello"硬编码写入程序二进制文件,程序加载期间字符串字面量被放入字面量内存区,当程序运行到let s = String::from()操作时,从字面量内存区将其拷贝到堆内存中,然后将堆内存中该数据的地址保存到栈内变量s中。

栈空间和栈帧

  栈空间和栈帧都是属于操作系统的概念,操作系统负责管理栈空间,负责创建、释放栈帧。

  栈空间采用后进先出的方式存放数据(就像叠盘子)。每次调用函数,都会在栈的顶端创建一个栈帧(stack frame),用来保存该函数的上下文数据。比如该函数内部声明的局部变量通常会保存在栈帧中。当该函数返回时,函数返回值也保留在该栈帧中。当函数调用者从栈帧中取得该函数返回值后,该栈帧被释放(实际上不会真的释放栈帧的空间,无效的栈帧可以被复用)。

  实际上,有一个ESP寄存器专门用来跟踪栈帧,该寄存器中保存了当前最顶端的栈帧地址。当调用函数创建新的栈帧时(栈帧总是在栈顶创建),ESP寄存器的值更新为此栈帧的地址,当函数返回且返回值已被读取后,该函数栈帧被移除出栈,出栈的方式很简单,只需更新ESP寄存器使其指向上一个栈帧的地址即可。

  不仅栈空间中的栈帧是后进先出的,栈帧内部的数据也是后进先出的。比如函数内先创建的局部变量在栈帧的底部,后创建的局部变量在栈帧的顶部。当然,上下顺序并非一定会如此,这和编译器有关,但编写程序时可如此理解。

  实际上,有一个EBP寄存器专门用来跟踪调用者栈帧的位置。当在函数a中调用函数b时,首先创建函数a的栈帧,当开始调用函数b时,将在栈顶创建函数b的栈帧,并拷贝上一个ESP的值到EBP,这样EBP寄存器就保存了函数a的栈帧地址,当函数b返回时通过EBP就可以回到函数a的栈帧。

  在编写代码的时候,通常不考虑属于操作系统的栈空间和栈帧的概念,而是这样思考:有一块内存,这块内存中存放数据的方式是后进先出。比如,调用函数时,函数内部的局部变量可以说成【存放在栈中或栈空间中】,而不将其具体到【存放在该函数的栈帧中】。也就是说,此时可以混用栈和栈空间的说法,且重在描述(主要是为了将栈和堆区分开来)而不是侧重于其准确性。后文也都如此混用栈和栈空间。

栈 or 堆

  Rust中各种类型的值默认都存储在栈中,除非显式地使用Box::new()将它们存放在堆上。

  但数据要存放在栈中,要求其数据类型的大小已知。对于静态大小的类型,可直接存储在栈上。

  例如如下类型的数据存放在栈中:

  • 裸指针(一个机器字长)、普通引用(一个机器字长)、胖指针(除了指针外还包含其他元数据信息,智能指针也是一种带有额外功能的胖指针,而胖指针实际上又是Struct结构)
  • 布尔值
  • char
  • 各种整数、浮点数
  • 数组(Rust数组的元素数据类型和数组长度都是固定不变的)
  • 元组

  对于动态大小的类型(如Vec、String),则数据部分分布在堆中(被称为allocate buffer),并在栈中留下胖指针(Struct方式实现)指向实际的数据,栈中的那个胖指针结构是静态大小的(换句话说,动态类型以Vec为例,Vec类型的值理应是那些连续的元素,但因为这样的连续内存的大小是不确定的,所以改变了它的行为,它的值是那个栈中的胖指针,而不是存储在allocatge buffer中的实际数据)。

以上分类需要注意几点:

  将栈中数据赋值给变量时,数据直接存放在栈中。比如i32类型的33,33直接存放在栈内,而不是在堆中存放33并在栈中存放指向33的指针

  因为类型的值默认都分布在栈中(即便是动态类型的数据,但也通过胖指针改变了该类型的值的表现形式),所以创建某个变量的引用时,引用的是栈中的那个值

  有些数据是0字节的,不需要占用空间,比如()

  尽管【容器】结构中(如数组、元组、Struct)可以存放任意数据,但保存在容器中的要么是原始类型的栈中值,要么是指向堆中数据的引用,所以这些容器类型的值也在栈中。例如,对于struct User {name: String},name字段存储的是String类型的胖指针,String类型实际的数据则在堆中

  尽管Box::new(T)可以将类型T的数据放入堆中,但Box类型本身是一个struct,它是一个胖指针(更严格地说是智能指针),它在栈中

  实际上,对于理解来说,只有Box才能让数据存放到堆中,但对于实现上,只有调用alloc才能申请堆内存并将数据存放在堆中。比如,自己想实现一个类型,将某些数据明确存放在堆中,那么必须要在实现代码中调用alloc来分配堆内存,但同时,要实现的这个类型本身,它的值是在栈中的。