陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

01|内存:值放堆上还是放栈上,这是一个问题

指针引用栈上的变量
数据结构在多个线程中访问
吞吐量
延迟
自动引用计数
实时系统
嵌入式系统
吞吐量
延迟
追踪式垃圾回收
使用已释放内存
堆越界
内存泄漏
动态生命周期的内存
动态大小的内存
栈溢出
帧
课程的GitHub仓库
Erlang VM
自动引用计数
追踪式垃圾回收
微软安全反应中心(MSRC)的研究
指针引用栈上的变量
数据结构在多个线程中访问
思考题
ARC
GC
堆
栈
拓展阅读
思考题
内存

该思维导图由 AI 生成,仅供参考

你好,我是陈天。今天我们打卡 Rust 学习的第一讲。
你是不是已经迫不及待想要了解 Rust 了,但是别着急,我们不会按常规直接开始介绍语法,而会先来回顾那些你平时认为非常基础的知识,比如说内存、函数。
提到基础知识,你是不是已经有点提不起兴趣了,这些东西我都知道,何必浪费时间再学一次呢?其实不然,这些年我接触过的资深工程师里,基础知识没学透,工作多年了,还得回来补课的大有人在。
以最基础的内存为例,很多人其实并没有搞懂什么时候数据应该放在栈上,什么时候应该在堆上,直到工作中实际出现问题了,才意识到数据的存放方式居然会严重影响并发安全,无奈回头重新补基础,时间精力的耗费都很大。
其实作为开发者,我们一辈子会经历很多工具、框架和语言,但是这些东西无论怎么变,底层逻辑都是不变的。
所以今天我们得回头重新思考,编程中那些耳熟能详却又似懂非懂的基础概念,搞清楚底层逻辑。而且这些概念,对我们后面学习和理解 Rust 中的知识点非常重要,之后,我们也会根据需要再穿插深入讲解。
代码中最基本的概念是变量和值,而存放它们的地方是内存,所以我们就从内存开始。

内存

我们的程序无时无刻不在跟内存打交道。在下面这个把 “hello world!” 赋值给 s 的简单语句中,就跟只读数据段(RODATA)、堆、栈分别有深度交互:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了内存管理中的栈和堆的原理和应用。栈作为程序运行的基础,用于存储函数调用时的局部变量和上下文信息,但其大小有限,过大的内存分配可能导致栈溢出。相比之下,堆适用于动态大小的内存和灵活的生命周期,但需要显式操作来进行内存分配和释放。文章通过具体的代码示例和图示,深入浅出地解释了栈和堆的设计原理和使用场景,帮助读者理解内存管理的重要性和技术特点。此外,文章还探讨了堆内存管理中可能出现的问题,以及追踪式垃圾回收和自动引用计数等解决方案。总之,本文为想要深入了解Rust语言的开发者提供了宝贵的基础知识,有助于理解Rust中的内存管理机制。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
,新⼈⾸单¥68
立即购买
登录 后留言

全部留言(94)

  • 最新
  • 精选
  • 冰河
    置顶
    评论区的大神一大片呀~所以学习课程的同学一定要看看评论区呀。

    编辑回复: 哈哈是的,大家要多多在留言区提问、回答、互动,学习气氛才会越来越好喔

    2021-08-26
    2
    7
  • 一期一会
    置顶
    golang的GC是不是和Java类似也有stw?感觉golang卡顿少一些

    作者回复: 追踪式 GC 的 STW 问题是很难避免的。所以 golang 也有同样的问题。Golang 在 2018 年之后性能好很多,golang 团队花了大力气优化,STW 从一个 GC cycle 的 10ms(2014 年)到两次 500us(2018年)。可以看这篇博客:https://go.dev/blog/ismmkeynote。

    2021-08-20
    2
    22
  • 特修斯之船
    置顶
    对于大小未知的变量都放在堆中,那是不是可以认为动态语言例如JS和Python,变量都是放在堆中?

    作者回复: Python 和 JS 的 primitive type 放在栈上,对象放在堆上,通过引用传递。

    2021-08-19
    6
    12
  • f
    置顶
    看到老师文章中的playground的代码,想起来一直存在的一个疑惑,"{:p}"输出某个变量的地址,是这个变量自身结构的地址,还是变量所指向值的地址呢。比如 ```rust let s = "helloworld".to_string(); println!("addr: {:p}", &s); ``` 输出的这个地址,是栈中s ptr|len|cap 变量结构本身的地址,还是ptr的值呢?

    作者回复: String 在 Rust 中是一个智能指针,我们后续会讲到,它内部是一个结构体,放在栈上,结构体中有指针指向堆内存。所以 &s 指向一个栈上的地址。 多讲两句。{:p} 是通过 Pointer trait 实现。你可以看它的文档:https://doc.rust-lang.org/std/fmt/trait.Pointer.html。 下面的代码可以帮助你更好地理解数据在内存的什么位置: ```rust static MAX: u32 = 0; fn foo() {} fn main() { let hello = "hello world".to_string(); let data = Box::new(1); // string literals 指向 RODATA 地址 println!("RODATA: {:p}", "hello world!"); // static 变量在 DATA section println!("DATA (static var): {:p}", &MAX); // function 在 TEXT println!("TEXT (function): {:p}", foo as *const ()); // String 结构体分配在栈上,所以其引用指向一个栈地址 println!("STACK (&hello): {:p}", &hello); // 需要通过解引用获取其堆上数据,然后取其引用 println!("HEAP (&*hello): {:p}", &*hello); // Box 实现了 Pointer trait 无需额外解引用 println!("HEAP (box impl Pointer) {:p} {:p}", data, &*data); } ``` 你可以直接在 playground 里运行这段代码: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5ca2cfb1d03936eae4b9a77a40d9987b

    2021-08-19
    10
    64
  • pedro
    1. 如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么? 不能,栈上的数据会随着当前线程的函数调用栈而回收,多个线程访问须在堆上开辟。 2. 可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以这么做? 可以,在当前函数调用栈中,可以新建变量在栈上开辟,顺便分配一个指针指向它,但是注意,这个指针的生命周期只能在当前栈帧中,不能作为返回值给别人用。 提一下,通篇读下来,明显感受到老师的内容极其深厚,能在各种语言之间斡旋,尖锐地指出每一门语言的特点,顺便再谈谈 rust 是如何解决它们的痛点,这才是 rust 真正的精髓所在。 rust 作为极其现代的语言,集百家之长而成,当然错误处理至今仍然很拉垮,将性能和安全做到了极致,但同时也带来了巨大的学习曲线,初学时,经常出现满屏的编译错误,每天都在和编译器做斗争。 也正是因为编译器极其严苛的规则,导致 rust 程序拥有无与伦比的信任度,基本编译通过、单测跑完就敢上线,去年有幸上线过 rust 项目,相较于原来的 java 版,无论是内存还是性能都带来了巨大提升,尤其是内存,几乎缩小了 80 倍左右。 未来很大程度上,rust 虽然不会成为主流,因为难,但它会是语言界的扫地僧。 写下第一个留言,期待和陈天老师成为朋友~

    作者回复: 嗯,@pedro 回答得很详尽了。我补充一下。1. 在多线程场景下,每个线程的生命周期是不固定的,无法在编译期知道谁先结束谁后结束,所以你不能把属于某个线程 A 调用栈上的内存共享给线程 B,因为 A 可能先于 B 结束。这时候,只能使用堆内存。这里有个例外,如果结束的顺序是确定的,那么可以共享,比如 scoped thread;2. 而同一个调用栈下,main() 调用 hello(),再调用 world(),编译器很清楚,world() 会先结束,之后是 hello(),最后是 main()。所以在 world() 下用指针引用 hello() 或者 main() 内部的变量没有问题,这个指针必然先于它指向的值结束。这个两个问题的实质是我们要搞明白哪些东西在编译期可以确定它们的关系或者因果,哪些只能在运行期确定。

    2021-08-16
    9
    196
  • 有铭
    在编译时,一切无法确定大小或者大小可以改变的数据,都无法放在栈上,只能放在堆上 ====== 我对这句话感到困惑,那可变长参数呢?可变长参数也属于在编译时无法确认大小的,难道也放在堆上?我搜索了一下发现rust似乎不支持可变长参数的函数,也不支持函数重载。所以我在想这种堆栈的特性是否是rust独有的。因为至少,Java就是支持可变长参数的,但是我目前没找到资料说明Java的可变长参数是如何实现再栈上的

    作者回复: 嗯,这是个好问题。这句话我表述地太绝对了。应该修改为:「在编译时,一切无法确定大小或者大小可以改变的数据,都无法**安全地**放在栈上,**最好**放在堆上」。 可变参数的函数是一个很好的例子。对于 Java,可变参数 String... a 是 String[] 的语法糖,它是放在堆上的。在 C 语言里,这个行为是未定义的,它只是定义了你可以通过 var_start / var_end 来获得可变参数的起始位置,以及最终结束可变参数的访问。但 gcc 的实现将可变参数放在栈上(估计是为了性能)。比如 C,你可以用 var_start / var_end 获取可变参数,但如果不小心处理, 会导致访问栈上的垃圾内容,甚至导致程序崩溃: ```C #include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list ap; int i; double sum = 0; va_start(ap, count); for (i = 0; i < count; i++) { sum += va_arg(ap, int); } va_end(ap); return sum; } int main(int argc, char const *argv[]) { printf("%d\n", sum(10, 1, 2, 3)); // 传入 3 个值但 count 为 10 return 0; } ``` 同时谢谢 c4f 的提醒,alloca() 可以在栈上分配动态大小的内存,然而使用它需要非常小心,按 linux 的文档([https://man7.org/linux/man-pages/man3/alloca.3.html](https://man7.org/linux/man-pages/man3/alloca.3.html)),官方建议配合 longjmp 使用。alloca() 如果分配太大的数据,超过栈容量会导致程序崩溃,即使你分配很小的数据,但如果使用 alloca() 的函数被优化导致 inline,又恰巧出现在大的 for/while 循环中,也可能会导致崩溃。 所以,这两种在栈上分配可变大小的数据,是不安全的。

    2021-08-17
    6
    39
  • 🔥神山 | 雷神山
    1. 如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么? 不能, 每个线程都会拥有自己的栈,栈上数据无法进行跨栈访问. 2. 可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以这么做? 可以,只要指针的生命周期小于或者等于栈上的引用源就行,如果生命周期大于引用源就会出现野指针的情况.在rust中会报错从而无法编译.

    作者回复: 👍

    2021-08-16
    3
    21
  • DustyBun
    只能说钱没白花,感觉这门课陈天老师不仅是在教授一门语言,更是在通过分析语言的特性来巩固我们的计算机基础知识。感谢老师,学生受教了,非常期待后面的课程。

    作者回复: 多谢支持:)

    2021-08-22
    3
    15
  • Geek_97036e
    请教陈老师: 关于可执行文件加载string literal 到内存,如果不同的两个函数,foo() 和bar()中,各自都用到了一个string literal “hello”, 那么编译器从从可执行文件.rodata 中加载内存,是加载一份”hello”,还是两份?

    作者回复: 是一份。它们指向 .rodata 中同样的地址。你可以用这段代码测试一下: ```rust fn main() { let x = "hello world"; println!("x = {:p}", x); foo(); } fn foo() { let v = "hello world"; println!("v = {:p}", v); } ```

    2021-08-17
    15
  • Christian
    1. 如果有一个数据结构需要在多个线程中访问,可以把它放在栈上吗?为什么? 这种情况是可能的。比如主线程创建一个子线程并等待子线程结束。子线程使用指针访问主线程当前栈帧上的数据。 2. 可以使用指针引用栈上的某个变量吗?如果可以,在什么情况下可以这么做? 可以,只要指针的生命周期小于等于引用的值就行。

    作者回复: 主线程创建子线程并等待子线程结束这个动作,是我们通过程序的上下文得出的结论,但这是运行时的编译器并不能理解他们的因果关系。比如: ```rust fn main() { let x = 10; let handle = thread::spawn(|| { println!("x: {}", x); }); handle.join().unwrap(); } ``` 从代码中我们很容易得出结论,主线程的 x 被子线程引用是安全的,但在编译期编译器无法做出这样的保证。

    2021-08-17
    3
    9
收起评论
显示
设置
留言
94
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部