为什么我们不能在堆栈上分配动态内存?

时间:2014-10-10 09:32:12

标签: c++ pointers memory-management stack heap

在堆栈上分配内容非常棒,因为我们拥有RAII并且不必担心内存泄漏等问题。但有时我们必须在堆上分配:

  • 如果数据真的很大(推荐) - 因为堆栈很小。

  • 如果要分配的数据大小仅在运行时已知(动态分配)。

两个问题:

  1. 为什么我们不能分配动态内存(即大小的内存) 在堆栈上只在运行时知道吗?

  2. 为什么我们只能通过指针引用堆上的内存,而堆栈上的内存可以通过普通变量引用?即Thing t;

  3. 编辑:我知道有些编译器支持可变长度数组 - 它是动态分配的堆栈内存。但这确实是一般规则的例外。我有兴趣了解一般原因的基本原因,我们不能在堆栈上分配动态内存 - 技术原因及其背后的理性。

8 个答案:

答案 0 :(得分:5)

  

为什么我们不能在堆栈上分配动态内存(即只在运行时已知的大小的内存)?

实现这一点要复杂得多。由于完成的可执行文件需要包含以便工作的指令类型,每个堆栈帧的大小都会被烧入已编译的程序中。例如,函数局部变量的布局和内容通过它在低级汇编代码中描述的寄存器和内存地址实际上硬编码到程序中:“变量”实际上并不存在于可执行文件中。让这些“变量”的数量和大小在编译运行之间发生变化使这个过程变得非常复杂,尽管并非完全不可能(正如您所发现的那样,使用非标准可变长度数组)。 / p>

  

为什么我们只能通过指针引用堆上的内存,而堆栈上的内存可以通过普通变量引用

这只是语法的结果。 C ++的“正常”变量恰好是具有自动或静态存储持续时间的变量。 语言的设计者可以在技术上做到这一点,这样你就可以写出像Thing t = new Thing这样的东西,并且整天只使用t,但他们没有;再次,这将更难以实施。那你如何区分不同类型的物体呢?请记住,您编译的可执行文件必须记住自动销毁一种而不是另一种。

我很想详细说明为什么以及为什么这些事情都不困难,因为我相信这就是你在这里所追求的。不幸的是,我对装配的了解太有限了。

答案 1 :(得分:4)

  

为什么我们不能在堆栈上分配动态内存(即只在运行时已知的大小的内存)?

从技术上讲,这是可能的。但未经C ++标准批准。可变长度数组(VLA)允许您在堆栈内存上创建动态大小的构造。大多数编译器都允许这作为编译器扩展。

示例:

int array[n];

//where n is only known at run-time
  

为什么我们只能通过指针引用堆上的内存,而堆栈上的内存可以通过普通变量引用?即Thing t;

我们可以。是否这样做取决于手头特定任务的实施细节。

示例:

int i;
int *ptr = &i;

答案 2 :(得分:4)

我们可以使用函数_alloca在堆栈内存上动态分配可变长度空间。此函数从程序堆栈分配内存。它只需要分配字节数并将void *返回到已分配的空间,就像malloc调用一样。这个分配的内存将在函数退出时自动释放。

所以不需要明确释放它。在这里必须记住分配大小,因为可能发生堆栈溢出异常。堆栈溢出异常处理可用于此类调用。在堆栈溢出异常的情况下,可以使用_resetstkoflw()将其恢复。

所以我们使用_alloca的新代码将是:

 int NewFunctionA()
 {
  char* pszLineBuffer = (char*) _alloca(1024*sizeof(char));
  …..
  // Program logic
   ….
 //no need to free szLineBuffer
 return 1;
}

答案 3 :(得分:2)

在编译之后,每个具有名称的变量都成为一个解除引用的指针,其地址值通过添加计算(取决于平台,可能是"减去" ...)"偏移值"到堆栈指针(包含堆栈实际到达的地址的寄存器:通常"当前函数返回地址"存储在那里)。

int i,j,k;

变为

(SP-12) ;i
(SP-8) ;j
(SP-4) ;k

让这个"总和"为了提高效率,偏移必须是常数,以便它们可以直接在指令操作码中编码:

k=i+j;

成为

MOV (SP-12),A;   i-->>A
ADD A,(SP-8) ;   A+=j
MOV A,(SP-4) ;   A-->>k

你在这里看到4,8和12现在是怎样"代码"而不是"数据"。

这意味着一个接一个的变量需要"其他"保留固定编译时定义的大小。

动态声明的数组可以是异常,但它们只能是函数的最后一个变量。否则,后面的所有变量都将具有一个偏移量,该偏移量必须在该数组分配后的运行时调整。

这就产生了解除引用地址需要算术(不仅仅是普通偏移)的复杂性,或者在声明变量时修改操作码的能力(自修改代码)。

该解决方案在性能方面都变得次优,因为所有解决方案都可以打破寻址的位置,或者为每个变量访问添加更多计算。

答案 4 :(得分:1)

  

为什么我们不能在堆栈上分配动态内存(即只在运行时已知的大小的内存)?

您可以使用_alloca()_malloca()的Microsoft编译器。对于gcc,它是alloca()

我不确定它是C / C ++标准的一部分,但是alloca()的变体包含在许多编译器中。如果你需要对齐分配,这样一个“n”字节的内存从“m”字节边界开始(其中m是2的幂),你可以分配n + m字节的内存,将m添加到指针并屏蔽掉低位。在十六进制100边界上分配十六进制1000字节内存的示例。您不需要保留_alloca()返回的值,因为它是堆栈内存,并在函数退出时自动释放。

char *p;
    p = _alloca(0x1000+0x100);
    (size_t)p = ((size_t)0x100 + (size_t)p) & ~(size_t)0xff;

答案 5 :(得分:1)

最重要的原因是可以以任何顺序释放使用的内存,但是堆栈需要以固定的顺序(即 LIFO 顺序)释放内存。因此,实际上很难实现这一目的。

答案 6 :(得分:0)

虚拟内存是内存的虚拟化,这意味着它的作用与虚拟化的资源(内存)相同。在系统中,每个进程都有不同的虚拟内存空间:

  • 32位程序:2 ^ 32字节(4 GB)
  • 64位程序:2 ^ 64字节(16艾字节)

由于虚拟空间太大,因此该虚拟空间中只有某些区域可用(这意味着只有某些区域可以像真实内存一样被读取/写入)。虚拟内存区域已初始化,并可以通过映射使用。虚拟内存不消耗资源,可以被认为是无限的(对于64位程序),但是可用(映射)的虚拟内存受到限制并耗尽了资源。

对于每个进程,某些映射由内核完成,而其他映射则由用户代码完成。例如,即使在代码开始执行之前,内核也会为代码指令,全局变量,共享库,堆栈空间等映射进程的虚拟内存空间的特定区域。用户代码使用 dynamic分配(例如mallocfree等分配包装器)或垃圾收集器(自动分配)来管理应用程序级的虚拟内存映射(例如,如果调用malloc时没有足够的可用可用虚拟内存,则会自动映射新的虚拟内存。)

您应该区分映射的虚拟内存(堆栈的总大小,堆的总当前大小...)和分配的虚拟内存(malloc明确告知程序可以使用的堆部分)。

对此,我将您的第一个问题重新解释为:

我们为什么不能在堆栈上保存动态数据(即仅在运行时知道大小的数据)?

首先,正如其他人所说,这是可能的:可变长度数组就是这样(至少在C语言中,我在C ++中也是如此)。但是,它有一些技术缺陷,也许这就是例外的原因:

  • 函数使用的堆栈大小在编译时变得未知,这增加了堆栈管理的复杂性,必须使用附加的寄存器(变量),并且可能会妨碍某些编译器优化。
  • 堆栈在过程开始时被映射,并且具有固定的大小。如果默认将variable-size-data放置在该位置,则该尺寸应大大增加。不大量使用堆栈的程序将浪费可用的虚拟内存。

此外,必须按后进先出的顺序保存和删除堆栈上保存的数据,这对于函数内的局部变量来说是完美的选择,但如果我们需要更灵活的方法,则不合适。

为什么我们只能通过指针引用堆中的内存,而堆栈中的内存却可以通过普通变量引用呢?

正如this answer所言,我们可以。

答案 7 :(得分:-1)

阅读一些有关Turing Machines的知识,以了解事物为什么如此。一切都围绕着它们建立起来。

https://en.wikipedia.org/wiki/Turing_machine

从技术上讲,这之外的一切都是可憎的事情。