操作系统会将物理内存映射成虚拟地址空间,程序在启动时看到的虚拟地址空间是一块完整连续的内存。栈内存从高位地址向下增长,且栈内存分配是连续的,一般操作系统对栈内存大小是有限制的,Linux/Unix类系统上面可以通过ulimit设置最大栈空间大小,所以C语言中无法创建任意长度的数组。在Rust里,函数调用时会创建一个临时栈空间,调用结束后Rust会让这个栈空间里的对象自动进入Drop流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作后进先出(last in, first out)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈(pushing onto the stack),而移出数据叫做出栈(popping off the stack)。
栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为分配(allocating)。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。堆上内存则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的,一般由程序员手动申请和释放的,如果想申请一块连续内存,则操作系统需要在堆中查找一块未使用的满足大小的连续内存空间,故其效率比栈要低很多,尤其是堆上如果有大量不连续内存时。另外内存使用完也必须由程序员手动释放,不然就会出现内存泄漏,内存泄漏对需要长时间运行的程序(例如守护进程)影响非常大。
之所以这样写,很多人觉得可以直接拷贝字符串a的引用从而避免拷贝整个字符串,然而得到的结果却是a does not live long enough的编译错误。因为引用了一个函数栈中临时创建的变量,函数栈在函数调用结束后会销毁,这样返回的引用就变得毫无意义了,指向了一个并不存在的变量。相对于C/C++而言,使用Rust就会幸运很多,因为C/C++中写出上面那样的程序,编译器会默默地让你通过直到运行时才会给你报错。