本人是在网上看了几篇博客加深对堆和栈的了解,内容转自几篇博客,所以内容会比较多,切可能相互重复,但是肯定能让你对堆和栈有详细的认识。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~版块一
这个十分钟的视频是B站一个up主做的,简单明了的给出堆和栈的几个最基本区别,如果不做深入的了解可以先看看这个
视频https://www.bilibili.com/video/av31604373?from=search&seid=3551931342125968644
这个是总结的截屏:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~版块二
1.静态 Static
指在编译时就能确定的每个数据目标在运行时刻需要的存储空间需求。因而在编译的时候就可以给他们分配固定的存储空间。 这种数据目标在编译时就为他们分配固定的内存。
限制:
- 代码中不能有可变数据结构,如数组。
- 代码中不允许有递归或嵌套结构的出现。
2.栈式 Stack
栈式存储分配可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能知道。
指在编译时不能确定大小,但在运行的时候能够确定,且规定在运行中进入一个程序模块时,就必须知道该模块所需要的数据区大小,才能为其分配内存,和我们在数据结构中所知道的栈一样,内存分配为e栈原则,先进后出的原则进行分配。
分配是在运行时执行的,但是大小是在编译时确定的;
特点:
在C/C++中,所有的方法调用都是通过栈来进行的,所有局部变量,形式参数都是从栈中分配内存空间的。
栈的分配和回收:
栈分配内存空间:从栈低向栈顶,依次存储;
栈回收内存空间:修改栈顶指针的位置,完成栈中内容销毁,这样的模式速度很快。
栈 :存放基本数据类型,速度快
- 栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄;
- 栈的存取速度比堆要快;
- 栈数据可以共享;
- 栈的数据大小与生存期必须是确定的,缺乏灵活性。
3.堆式 Heap
指编译时,运行时模块入口都不能确定存储要求的数据结构的内存分配。
比如可变长度的串和对象实例。
堆由大片的可利用的块或空闲组成,堆中的内存可以按照任意顺序分配和释放。
堆是在运行的时候,请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁的时候都要占用时间,因此对的效率低下。
堆的优点:编译时不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长时间,因此堆存储数据时,灵活性比较大;
在面向对象编程中,堆是必不可少的,因为面向对象的多态性,多态变量所需的存储空间只有在运行时创建了对象之后才能确定。
堆: 用new建立,垃圾自动回收负责回收
- 堆是一个"运行时"数据区,类实例化的对象就是从堆上去分配空间的;
- 在堆上分配空间是通过"new"等指令建立的;
- Java针对堆的操作和C++的区别就是,Java不需要在空间不用的时候来显式的释放;
- Java的堆是由Java的垃圾回收机制来负责处理的,堆是动态分配内存大小,垃圾收集器可以自动回收不再使用的内存空间。
- 但缺点是,因为在运行时动态分配内存,所以内存的存取速度较慢。
4. 堆和栈的比较
比较方面 |
堆 |
栈 |
功能的比较 |
存放对象 |
执行程序 |
存储内容 |
new关键字创建的内容 |
局部变量,形式参数 |
存储速度 |
慢 |
快(所以用来执行程序) |
5.JVM中的堆栈
JVM是基于堆栈的虚拟机,JVM中的堆栈是两块不同的存储区域。
JVM为每个线程程都分配了一个堆和栈,所以对于java程序来说,程序的运行是通过对堆栈的操作来完成的。
JVM 堆 |
JVM 栈 |
是存储的单位 |
是运行时的单位 |
JVM堆解决的是数据存储的问题,即数据怎么放、放在哪儿。 |
JVM栈解决程序的运行问题,即程序如何执行,或者说如何处理数据; |
JVM堆中存的是对象。 |
JVM栈中存的是基本数据类型和JVM堆中对象的引用 |
一个对象的大小是不可估计的,或者说是可以动态变化的,但是在JVM栈中,一个对象只对应了一个4btye的引用(JVM堆JVM栈分离的好处:))。
为什么不把基本类型放JVM堆中呢?
因为基本类型其占用的空间一般是1~8个字节(需要空间比较少),而且因为是基本类型,所以不会出现动态增长的情况(长度固定),因此JVM栈中存储就够了,如果把他存在JVM堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在JVM栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。
但是基本类型、对象引用和对象本身就有所区别了,因为一个是JVM栈中的数据一个是JVM堆中的数据的引用。最常见的一个问题就是,Java中参数传递时的问题。
JAVA中堆栈的应用
Java中的参数传递时传值呢?还是传引用?
要说明这个问题,先要明确两点:
1.不要试图与C进行类比,Java中没有指针的概念
2.程序运行永远都是在JVM栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题,不会直接传对象本身。总结:传递的是对象的引用值或是基本类型的值;
明确以上两点后。Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。因此,很多书里面都说Java是进行传值调用,这点没有问题,而且也简化的C中复杂性。
但是传引用的错觉是如何造成的呢?
在运行JVM栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到JVM堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是JVM堆中的数据。所以这个修改是可以保持的了。
对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
JVM堆和JVM栈中,JVM栈是程序运行最根本的东西。程序运行可以没有JVM堆,但是不能没有JVM栈。而JVM堆是为JVM栈进行数据存储服务,说白了JVM堆就是一块共享的内存。不过,正是因为JVM堆和JVM栈的分离的思想,才使得Java的垃圾回收成为可能。
JVM中线程堆栈
线程(thread),有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。 线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~板块三
数据结构的栈和堆
首先在数据结构上要知道堆栈,尽管我们这么称呼它,但实际上堆栈是两种数据结构:堆和栈。
堆和栈都是一种数据项按序排列的数据结构。
栈就像装数据的桶或箱子
我们先从大家比较熟悉的栈说起吧,它是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。
这就如同我们要取出放在箱子里面底下的东西(放入的比较早的物体),我们首先要移开压在它上面的物体(放入的比较晚的物体)。
堆像一棵倒过来的树
- 而堆就不同了,堆是一种经过排序的树形数据结构,每个结点都有一个值。
- 通常我们所说的堆的数据结构,是指二叉堆。
- 堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。
由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
内存分配中的栈和堆
先看百度百科中的内存堆栈介绍:
堆栈空间分配
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
堆栈缓存方式
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。
堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
下面就说说C语言程序内存分配中的堆和栈,这里有必要把内存分配也提一下,大家不要嫌我啰嗦,一般情况下程序存放在Rom(只读内存,比如硬盘)或Flash中,运行时需要拷到RAM(随机存储器RAM)中执行,RAM会分别存储不同的信息,如下图所示:
内存中的栈区处于相对较高的地址以地址的增长方向为上的话,栈地址是向下增长的。
栈中分配局部变量空间,堆区是向上增长的用于分配程序员申请的内存空间。另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。
来看一个网上很流行的经典例子:
0.申请方式和回收方式不同
不知道你是否有点明白了。
堆和栈的第一个区别就是申请方式不同:栈(英文名称是stack)是系统自动分配空间的,例如我们定义一个 char a;系统会自动在栈上为其开辟空间。而堆(英文名称是heap)则是程序员根据需要自己申请的空间,例如malloc(10);开辟十个字节的空间。
由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。还有其他的一些区别我认为网上的朋友总结的不错这里转述一下:
1.申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
也就是说堆会在申请后还要做一些后续的工作这就会引出申请效率的问题。
2.申请效率的比较
根据第0点和第1点可知。
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片不过用起来最方便。
3.申请大小的限制
栈:在Windows下栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4.堆和栈中的存储内容
由于栈的大小有限,所以用子函数还是有物理意义的,而不仅仅是逻辑意义。
栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
5.存取效率的比较
aaaaaaaaaaa是在运行时刻赋值的;放在栈中。
而bbbbbbbbbbb是在编译时就确定的;放在堆中。
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov clbyte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4]cl
11: a = p[1];
0040106D 8B 55 EC mov edxdword ptr [ebp-14h]
00401070 8A 42 01 mov albyte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4]al
关于堆和栈区别的比喻
堆和栈的区别可以引用一位前辈的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是*度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且*度大。比喻很形象,说的很通俗易懂,不知道你是否有点收获。
问题描述
编程语言书籍中经常解释值类型被创建在栈上,引用类型被创建在堆上,但是并没有本质上解释这堆和栈是什么。什么是栈,但是它们到底是什么,在哪儿呢(站在实际的计算机物理内存的角度上看)?
- 在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗?
- 它们的作用范围是什么?
- 它们的大小由什么决定?
- 哪个更快?
答案一
栈是为执行线程留出的内存空间。当函数被调用的时候,栈顶为局部变量和一些 bookkeeping 数据预留块。当函数执行完毕,块就没有用了,可能在下次的函数调用的时候再被使用。栈通常用后进先出(LIFO)的方式预留空间;因此最近的保留块(reserved block)通常最先被释放。这么做可以使跟踪堆栈变的简单;从栈中释放块(free block)只不过是指针的偏移而已。
堆(heap)是为动态分配预留的内存空间。和栈不一样,从堆上分配和重新分配块没有固定模式;你可以在任何时候分配和释放它。这样使得跟踪哪部分堆已经被分配和被释放变的异常复杂;有许多定制的堆分配策略用来为不同的使用模式下调整堆的性能。
每一个线程都有一个栈,但是每一个应用程序通常都只有一个堆(尽管为不同类型分配内存使用多个堆的情况也是有的)。
直接回答你的问题: 1. 当线程创建的时候,操作系统(OS)为每一个系统级(system-level)的线程分配栈。通常情况下,操作系统通过调用语言的运行时(runtime)去为应用程序分配堆。 2. 栈附属于线程,因此当线程结束时栈被回收。堆通常通过运行时在应用程序启动时被分配,当应用程序(进程)退出时被回收。 3. 当线程被创建的时候,设置栈的大小。在应用程序启动的时候,设置堆的大小,但是可以在需要的时候扩展(分配器向操作系统申请更多的内存)。 4. 栈比堆要快,因为它存取模式使它可以轻松的分配和重新分配内存(指针/整型只是进行简单的递增或者递减运算),然而堆在分配和释放的时候有更多的复杂的 bookkeeping 参与。另外,在栈上的每个字节频繁的被复用也就意味着它可能映射到处理器缓存中,所以很快(译者注:局部性原理)。
答案二
Stack:
- 和堆一样存储在计算机 RAM 中。
- 在栈上创建变量的时候会扩展,并且会自动回收。
- 相比堆而言在栈上分配要快的多。
- 用数据结构中的栈实现。
- 存储局部数据,返回地址,用做参数传递。
- 当用栈过多时可导致栈溢出(无穷次(大量的)的递归调用,或者大量的内存分配)。
- 在栈上的数据可以直接访问(不是非要使用指针访问)。
- 如果你在编译之前精确的知道你需要分配数据的大小并且不是太大的时候,可以使用栈。
- 当你程序启动时决定栈的容量上限。
Heap:
- 和栈一样存储在计算机RAM。
- 在堆上的变量必须要手动释放,不存在作用域的问题。数据可用 delete, delete[] 或者 free 来释放。
- 相比在栈上分配内存要慢。
- 通过程序按需分配。
- 大量的分配和释放可造成内存碎片。
- 在 C++ 中,在堆上创建数的据使用指针访问,用 new 或者 malloc 分配内存。
- 如果申请的缓冲区过大的话,可能申请失败。
- 在运行期间你不知道会需要多大的数据或者你需要分配大量的内存的时候,建议你使用堆。
- 可能造成内存泄露。
举例
C++
答案三
堆和栈是两种内存分配的两个统称。可能有很多种不同的实现方式,但是实现要符合几个基本的概念:
1.对栈而言,栈中的新加数据项放在其他数据的顶部,移除时你也只能移除最顶部的数据(不能越位获取)。
2.对堆而言,数据项位置没有固定的顺序。你可以以任何顺序插入和删除,因为他们没有“顶部”数据这一概念。
上面上个图片很好的描述了堆和栈分配内存的方式。
在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗?
如前所述,堆和栈是一个统称,可以有很多的实现方式。计算机程序通常有一个栈叫做调用栈,用来存储当前函数调用相关的信息(比如:主调函数的地址,局部变量),因为函数调用之后需要返回给主调函数。栈通过扩展和收缩来承载信息。实际上,程序不是由运行时来控制的,它由编程语言、操作系统甚至是系统架构来决定。
堆是在任何内存中动态和随机分配的(内存的)统称;也就是无序的。内存通常由操作系统分配,通过应用程序调用 API 接口去实现分配。在管理动态分配内存上会有一些额外的开销,不过这由操作系统来处理。
它们的作用范围是什么?
调用栈是一个低层次的概念,就程序而言,它和“作用范围”没什么关系。如果你反汇编一些代码,你就会看到指针引用堆栈部分。就高级语言而言,语言有它自己的范围规则。一旦函数返回,函数中的局部变量会直接直接释放。你的编程语言就是依据这个工作的。
在堆中,也很难去定义。作用范围是由操作系统限定的,但是你的编程语言可能增加它自己的一些规则,去限定堆在应用程序中的范围。体系架构和操作系统是使用虚拟地址的,然后由处理器翻译到实际的物理地址中,还有页面错误等等。它们记录那个页面属于那个应用程序。不过你不用关心这些,因为你仅仅在你的编程语言中分配和释放内存,和一些错误检查(出现分配失败和释放失败的原因)。
它们的大小由什么决定?
依旧,依赖于语言,编译器,操作系统和架构。栈通常提前分配好了,因为栈必须是连续的内存块。语言的编译器或者操作系统决定它的大小。不要在栈上存储大块数据,这样可以保证有足够的空间不会溢出,除非出现了无限递归的情况(额,栈溢出了)或者其它不常见了编程决议。
堆是任何可以动态分配的内存的统称。这要看你怎么看待它了,它的大小是变动的。在现代处理器中和操作系统的工作方式是高度抽象的,因此你在正常情况下不需要担心它实际的大小,除非你必须要使用你还没有分配的内存或者已经释放了的内存。
哪个更快一些?
栈更快因为所有的空闲内存都是连续的,因此不需要对空闲内存块通过列表来维护。只是一个简单的指向当前栈顶的指针。编译器通常用一个专门的、快速的寄存器来实现。更重要的一点事是,随后的栈上操作通常集中在一个内存块的附近,这样的话有利于处理器的高速访问(译者注:局部性原理)。
答案四
你问题的答案是依赖于实现的,根据不同的编译器和处理器架构而不同。下面简单的解释一下:
- 栈和堆都是用来从底层操作系统中获取内存的。
- 在多线程环境下每一个线程都可以有他自己完全的独立的栈,但是他们共享堆。并行存取被堆控制而不是栈。
堆:
- 堆包含一个链表来维护已用和空闲的内存块。在堆上新分配(用 new 或者 malloc)内存是从空闲的内存块中找到一些满足要求的合适块。这个操作会更新堆中的块链表。这些元信息也存储在堆上,经常在每个块的头部一个很小区域。
- 堆的增加新快通常从地地址向高地址扩展。因此你可以认为堆随着内存分配而不断的增加大小。如果申请的内存大小很小的话,通常从底层操作系统中得到比申请大小要多的内存。
- 申请和释放许多小的块可能会产生如下状态:在已用块之间存在很多小的空闲块。进而申请大块内存失败,虽然空闲块的总和足够,但是空闲的小块是零散的,不能满足申请的大小,。这叫做“堆碎片”。
- 当旁边有空闲块的已用块被释放时,新的空闲块可能会与相邻的空闲块合并为一个大的空闲块,这样可以有效的减少“堆碎片”的产生。
栈:
- 栈经常与 sp 寄存器(译者注:”stack pointer”,了解汇编的朋友应该都知道)一起工作,最初 sp 指向栈顶(栈的高地址)。
- CPU 用 push 指令来将数据压栈,用 pop 指令来弹栈。当用 push 压栈时,sp 值减少(向低地址扩展)。当用 pop 弹栈时,sp 值增大。存储和获取数据都是 CPU 寄存器的值。
- 当函数被调用时,CPU使用特定的指令把当前的 IP (译者注:“instruction pointer”,是一个寄存器,用来记录 CPU 指令的位置)压栈。即执行代码的地址。CPU 接下来将调用函数地址赋给 IP ,进行调用。当函数返回时,旧的 IP 被弹栈,CPU 继续去函数调用之前的代码。
- 当进入函数时,sp 向下扩展,扩展到确保为函数的局部变量留足够大小的空间。如果函数中有一个 32-bit 的局部变量会在栈中留够四字节的空间。当函数返回时,sp 通过返回原来的位置来释放空间。
- 如果函数有参数的话,在函数调用之前,会将参数压栈。函数中的代码通过 sp 的当前位置来定位参数并访问它们。
- 函数嵌套调用和使用魔法一样,每一次新调用的函数都会分配函数参数,返回值地址、局部变量空间、嵌套调用的活动记录都要被压入栈中。函数返回时,按照正确方式的撤销。
- 栈要受到内存块的限制,不断的函数嵌套/为局部变量分配太多的空间,可能会导致栈溢出。当栈中的内存区域都已经被使用完之后继续向下写(低地址),会触发一个 CPU 异常。这个异常接下会通过语言的运行时转成各种类型的栈溢出异常。(译者注:“不同语言的异常提示不同,因此通过语言运行时来转换”我想他表达的是这个含义)
*函数的分配可以用堆来代替栈吗?
不可以的,函数的活动记录(即局部或者自动变量)被分配在栈上, 这样做不但存储了这些变量,而且可以用来嵌套函数的追踪。
堆的管理依赖于运行时环境,C 使用 malloc ,C++ 使用 new ,但是很多语言有垃圾回收机制。
栈是更低层次的特性与处理器架构紧密的结合到一起。当堆不够时可以扩展空间,这不难做到,因为可以有库函数可以调用。但是,扩展栈通常来说是不可能的,因为在栈溢出的时候,执行线程就被操作系统关闭了,这已经太晚了。
作者:你的雷哥