栈和堆

1  

栈是由操作系统在创建线程的时候,系统自动创建,栈是由顶像下分配的, DELPHI 中默认的栈大小是 1M ,这个可以通过 Project->Options->Linker->Max Stack size 来改变其大小。

栈是线程执行代码的地方,操作系统根据系统调度算法来加载执行的代码,另外栈还存放函数的参数值,局部变量。栈的存取是按 4 字节偏移,不会根据需要动态增长,因此超出范围会报栈溢出。

 

2  

我们把在栈之外的分配内存都叫在堆上分配内存,堆是由程序员分配释放。在 DELPHI 中是用 GetMem.inc 中的代码来管理堆的,堆中包含许多大小不确定的块。初始状态下,堆仅有一个块,即堆本身。经过一段时间地取用和回收以后,堆中将可能只剩下一些“切割”后残余的“碎片”,且这些碎片可能已经无法再合并。此时,如果一个新的请求大于任何一个碎片,那么就必须再申请一个新的、大的块放在堆中。堆的使用永远是一个“拆东墙补西墙”的过程。

堆的大小是 2G ,在扩展内存模式下能达到 3G 。注意它与数据结构中的堆是两回事,它的分配方式类似于链表,访问“堆”的内容的时候需要先找到这个“堆”,然后再遍历链表,因此“堆”访问会比“栈”慢。

3   哪些在栈中

3.1            获取栈的首尾地址

获取通常情况下的栈地址

在写汇编的时候,我们知道 esp 存放栈顶指针, ebp 存放栈底指针

procedure GetStackAddress(var AStackTop, AStackBottom: Cardinal);

begin

  asm

    mov [eax], esp;     // 栈顶, eax 接收第一个参数

    mov [edx], ebp;     // 栈底, edx 接收第二个参数

  end;

end;

获取异常发生时的栈地址

         Windows 下, FS:[4] 存放发生异常时的栈顶指针。

procedure GetStackAddress(var AStackTop, AStackBottom: Cardinal);

begin

  asm

    mov ecx, FS:[4];  //FS:[4] 放置发生异常时的栈信息

    sub ecx, 3;

    mov [eax], eax;   // 栈顶, eax 接收第一个参数

    mov [edx], ebp;   // 栈低, edx 接收第二个参数

  end;

end;

知道了栈的首尾地址之后,我们就可以取出变量地址,然后和栈的地址比较,如果超出栈的范围,则表示变量在堆中。

3.2            基本数据类型( Integer Cardinal Shortint Smallint Longint Int64 Byte Word LongWord Char )都是在栈中的

基本类型在函数体内分配是在栈中的,如果在类中分配则是在堆中的。另外 Int64 也是在栈中分配的,它具体的分配是偏移 8 字节。我们写下如下测试代码:

procedure TestInt64;

var

  Value: Int64;

  StackTop, StackBottom: Cardinal;

begin

  Value := 10;

  GetStackAddress(StackTop, StackBottom);

  ShowMessage(Format('StackTop: %s, StackBottom: %s; Int64 Address: %s',

    [IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@Value), 8)]));

end;

我电脑测试显示的信息为 StackTop: 0012F5E0, StackBottom: 0012F628; Int64 Address: 0012F620 ,从上面信息我们可以看出栈底偏 8 字节就是 Value 的地址。

3.3            指针类型是指针在栈中,指针所指向的地址在堆中

指针在函数体内分配,指针的地址是在栈中的,指针的内容是在堆中的。指针如果在类中分配则,指针地址和指针内容都是在堆中的。我们写下如下测试代码:

procedure TestPointer;

var

  APoint: Pointer;

  StackTop, StackBottom: Cardinal;

begin

  GetMem(APoint, 1000);

  GetStackAddress(StackTop, StackBottom);

  ShowMessage(Format('StackTop: %s, StackBottom: %s; Pointer Address: %s; Pointer Content Address: %s',

    [IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@APoint), 8),

    IntToHex(Integer(APoint), 8)]));

end;

我的电脑测试显示的信息为 StackTop: 0012F568, StackBottom: 0012F5B8; Pointer Address: 0012F5B4; Pointer Content Address: 00A3FD10 ,从上面的信息我们可以栈底偏 4 字节就是指针的地址。

3.4            固定数组在栈中

固定数组在函数体内分配是在栈中的,如果在类中分配则是在堆中的。因此不能函数体内分配超过 1M 大小的固定数组,否则会造成栈溢出。我们写下如下测试代码:

type

  TFixArray = array[0..9] of Integer;

procedure TestFixArray;

var

  FixArray: TFixArray;

  StackTop, StackBottom: Cardinal;

begin

  FixArray[0] := 10;

  GetStackAddress(StackTop, StackBottom);

  ShowMessage(Format('StackTop: %s, StackBottom: %s; Int64 Address: %s',

    [IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@FixArray[0]), 8)]));

end;

我的电脑测试显示的信息为 StackTop: 0012F550, StackBottom: 0012F5B8; Fix Array Address: 0012F588 ,从上面的信息我们可以看出固定数组是在栈中的,动态数组类似指针,只是动态数组的指针在栈中,动态数组的内容是在堆中的。另外我们从汇编代码也可以看出相同的信息, FixArray[0] := 10 对应的汇编代码是 mov [ebp-$30],$0000000a ebp 指向栈底,因此我们可以看出动态数组的内存是在栈中的。

3.5            结构体在栈中

结构体在函数体内分配是在栈中的,在类中分配则是在堆中的,如果结构体内含有 string 等指针类型,则指针的地址在站内,指针的内容是在堆中的。在函数体内分配超过 1M 大小的结构体也会造成栈溢出。我们写下如下测试代码:

type

  TRecord = record

    Value: string;

    Len: Integer;

  end;

procedure TestRecord;

var

  PntRecord: TRecord;

  StackTop, StackBottom: Cardinal;

begin

  PntRecord.Value := 'Test';

  GetStackAddress(StackTop, StackBottom);

  ShowMessage(Format('StackTop: %s, StackBottom: %s; Record Address: %s; Record Pointer Address: %s',

    [IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@PntRecord), 8),

    IntToHex(Integer(PChar(PntRecord.Value)), 8)]));

end;

我的电脑测试显示的信息为 StackTop: 0012F564, StackBottom: 0012F5B8; Record Address: 0012F5B0; Record Pointer Address: 0045F4B4 ,从上面的信息我们可以看出结构体是在栈中的,结构体指针和指针一样。

4   哪些在堆中

用内存申请函数申请的内存都是在堆中的,如用 New GetMem StrAlloc AllocMem SysGetMem ,哪些自管理类型 string 、动态数组的内容都是在堆中的,下面我们给出结论,测试代码大家可以仿照上面的判断变量是否在栈中的代码编写。

4.1            指针指向的内容是在堆中

4.2            动态数组的内容是在堆中

4.3            String ShortString WideString 的内容是在堆中

4.4            变体 Variant OleVariant 的内容是在堆中

变体类型是一个结构体,它的定义是:

TVarData = packed record

    case Integer of

      0: (VType: TVarType;

          case Integer of

            0: (Reserved1: Word;

                case Integer of

                  0: (Reserved2, Reserved3: Word;

                      case Integer of

                        varSmallInt: (VSmallInt: SmallInt);

                        varInteger:  (VInteger: Integer);

                         varSingle:   (VSingle: Single);

                        varDouble:   (VDouble: Double);

                        varCurrency: (VCurrency: Currency);

                        varDate:     (VDate: TDateTime);

                        varOleStr:   (VOleStr: PWideChar);

                        varDispatch: (VDispatch: Pointer);

                        varError:    (VError: HRESULT);

                        varBoolean:  (VBoolean: WordBool);

                        varUnknown:  (VUnknown: Pointer);

                         varShortInt: (VShortInt: ShortInt);

                        varByte:     (VByte: Byte);

                        varWord:     (VWord: Word);

                        varLongWord: (VLongWord: LongWord);

                        varInt64:    (VInt64: Int64);

                        varString:   (VString: Pointer);

                        varAny:      (VAny: Pointer);

                        varArray:    (VArray: PVarArray);

                        varByRef:    (VPointer: Pointer);

                      );

                  1: (VLongs: array[0..2] of LongInt);

               );

            2: (VWords: array [0..6] of Word);

            3: (VBytes: array [0..13] of Byte);

          );

      1: (RawData: array [0..3] of LongInt);

       end;

         从定义中我们可以看出 varOleStr varString varArray varByRef 都是在堆中的。

5   全局变量在堆中

全局变量的指针地址和指针内容都不是在栈中的,我们把他归类到堆中。

6   栈和堆比较

6.1            栈和堆的管理方式比较

栈:由操作系统自动分配,而且在栈上分配内存是由编译器自动完成的,栈不需要编译器管理,操作系统自动实现申请释放;

堆:由操作系统提供接口,各个编译器实现管理方式,由外部程序申请释放,如果外部程序在程序结束时没有释放,由操作系统强行释放,在 DELPHI 中是用 GetMem.inc 来实现内存管理;

6.2            栈和堆 的初始化比较

栈:分配的内存不会初始化,是一个垃圾值;

堆:分配的内存不会初始化,是一个垃圾值,但是 DELPHI 默认初始化类变量和全局变量;

6.3            栈和堆的申请方式 比较

栈:由系统自动分配,如在函数申明一个局部变量 i: Integer ;编译器会自动在栈中分配内存;

堆:由程序自己管理,需要程序员自己申请,并指明大小;

6.4            堆和栈的效率比较

栈:在栈上分配空间是直接用 add 指令,对 esp 进行移位,例如 add esp,-$44 ,可以在一个指令周期内完成;

堆: 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的 FreeMem 语句才能正确的释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 在堆中 分配内存的时候会用 HeapLock HeapUnlock 加锁,因此在多线程中分配内存是线性的,效率低下;

6.5            栈和堆的大小限制 比较

栈:在 Windows 下栈默认大小是 1M, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow 。因此,能从栈获得的空间较小。

堆:在 Windows 下默认堆大小是 2GB ,堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

6.6            栈和堆 的超出范围比较

栈:超出栈大小会报栈溢出;

堆:超出堆大小会报 Out Of Memory

 

<think>嗯,用户想了解的区别。这个问题在编程计算机科学中挺基础的,但解释清楚需要仔细组织思路。首先,我得确定用户可能的知识背景,可能是个刚开始学编程的新手,或者是对内存管理不太清楚的人。 首先,都是内存管理的部分,但用途特性不同。是系统自动管理的,而需要手动或通过垃圾回收机制来管理。我应该从内存分配、生命周期、访问速度等方面来区分它们。 然后,要考虑结构。是LIFO结构,函数调用时的局部变量存放在这里,调用结束自动释放。而则是动态分配,需要显式申请释放,比如用malloc或new。生命周期更长,但可能产生内存泄漏。 还要提到访问速度,的访问更快,因为内存是连续的,而可能碎片化,访问较慢。此外,的大小通常比大,但具体取决于系统设置。 可能用户还会关心为什么要有两种结构,所以需要解释的高效自动管理适合确定生命周期的数据,而的灵活性适合动态大小或长时间存在的数据。 要注意避免术语混淆,比如“”有时也指数据结构,但这里讨论的是内存区域。同时,例子要简单明了,比如用变量对象分配来说明。 另外,是否需要提到不同编程语言中的情况?比如Java的自动垃圾回收处理,而C++需要手动管理。这可能会帮助用户理解实际应用中的差异。 还要检查是否有常见的误解,比如溢出溢出的不同原因。溢出通常是递归太深或局部变量过大,溢出则是内存泄漏或频繁分配大块内存。 最后,总结两者的区别,用表格或对比的方式更清晰。确保语言口语化,避免过于技术化,让新手也能理解。</think>是计算机内存管理的两种核心机制,它们的区别主要体现在以下方面: --- ### **1. 内存分配方式** - **** - **自动分配与释放**:由系统自动管理(如函数调用时分配局部变量,函数返回时释放) - **固定大小**:内存大小在编译时确定(如 `int arr[10]`) - **示例** ```c void func() { int a = 5; // 变量a在上分配 } // 函数结束,a自动释放 ``` - **** - **手动分配与释放**:需显式申请(如 `malloc`/`new`)释放(如 `free`/`delete`) - **动态大小**:内存大小可在运行时决定(如 `int* arr = new int[n]`) - **示例** ```c void func() { int* p = (int*)malloc(100 * sizeof(int)); // 上分配100个int空间 free(p); // 需手动释放 } ``` --- ### **2. 生命周期** - **** - **短暂**:变量生命周期与函数调用绑定,函数结束即销毁 - **严格顺序**:后进先出(LIFO),保证内存有序释放 - **** - **灵活**:内存可跨函数或长期存在(需手动管理) - **风险**:忘记释放会导致内存泄漏;重复释放可能引发崩溃 --- ### **3. 访问速度** - **** - **速度快**:内存连续分配,通过寄存器直接寻址 - **空间小**:默认大小有限(如Linux默认约8MB) - **** - **速度慢**:需通过指针间接访问,内存可能碎片化 - **空间大**:可利用全部可用内存(受操作系统限制) --- ### **4. 典型用途** - **** - 局部变量、函数参数、返回值地址 - 递归函数调用(但深度受大小限制) - **** - 动态数据结构(如链表、树) - 大块数据(如图像、文件缓存) --- ### **5. 对比表格** | 特性 | | | |--------------|------------------------|------------------------| | **管理方式** | 系统自动管理 | 需手动申请/释放 | | **生命周期** | 函数调用周期内 | 可跨函数长期存在 | | **速度** | 快(连续内存) | 慢(碎片化) | | **空间限制** | 较小(默认约MB级) | 较大(接近系统内存上限)| | **风险** | 溢出(如无限递归) | 内存泄漏、碎片化 | --- ### **6. 实际场景示例** - **溢出** ```c void infinite_recursion() { int a[1000]; // 每次递归消耗空间 infinite_recursion(); } ``` 多次递归后触发溢出(Stack Overflow)。 - **泄漏** ```c void leak_memory() { int* p = (int*)malloc(1000 * sizeof(int)); // 忘记调用 free(p) } ``` 多次调用后耗尽内存。 --- ### **总结** - 选择:数据量小、生命周期短、需高效访问时 - 选择:数据量大、生命周期不确定、需动态调整时 - 关键区别:**管理方式**与**生命周期控制**是核心差异!
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值