【问题标题】:Why can't we allocate dynamic memory on the stack?为什么我们不能在堆栈上分配动态内存?
【发布时间】:2014-12-05 10:26:11
【问题描述】:

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

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

  • 如果要分配的数据大小只有在运行时才知道(动态分配)。

两个问题:

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

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

编辑:我知道一些编译器支持可变长度数组 - 这是动态分配的堆栈内存。但这确实是一般规则的例外。我有兴趣了解为什么我们不能在堆栈上分配动态内存的根本原因——技术原因和背后的原因。

【问题讨论】:

  • 是的,我们可以。 int test(int n) { int array[n]; } 自 C99 起有效。哦,如果你说的是 C++,那么 C++14 中引入了可变长度数组
  • 内存相关的RAII实际上是关于通过自动存储持续时间(或您所说的“堆栈”)变量来管理动态分配的内存。
  • 如果你放弃这个“堆栈”与“堆”术语会更好。

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


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

    示例:

    int array[n];
    
    //where n is only known at run-time
    

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

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

    示例:

    int i;
    int *ptr = &i;
    

    【讨论】:

    • 您在上一个示例中所做的是通过指针引用堆栈变量。我在问为什么我们不能通过普通变量来引用堆内存。
    • @AvivCohn:我们能不能别再叫他们“堆栈变量”了?它是一个具有自动(或静态)存储持续时间的变量。如果你需要一个具体的、实际的理由,那么考虑一下当你落入这个陷阱时会发生什么:struct T { int x; }; T* p = new T(); /* Is T::x "a stack variable"? */
    【解决方案3】:

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

    所以它不需要被显式释放。这里必须记住分配大小,因为可能会发生堆栈溢出异常。堆栈溢出异常处理可用于此类调用。如果发生堆栈溢出异常,可以使用 _resetstkoflw() 将其恢复。

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

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

    【讨论】:

      【解决方案4】:

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

      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 现在是“代码”,而不是“数据”。

      这意味着一个在另一个之后出现的变量要求“其​​他”保持固定的编译时定义的大小。

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

      这造成了复杂性,即取消引用地址需要算术(不仅仅是简单的偏移量)或在声明变量时修改操作码的能力(自修改代码)。

      两种解决方案在性能方面都变得次优,因为它们都可能破坏寻址的局部性,或者为每个变量访问添加更多计算。

      【讨论】:

      • 所以你的意思是堆栈分配内存的大小必须在编译时知道,因为堆栈上的所有变量的地址都直接在程序集中编码(就SP 的偏移量),因此不必在运行时进行额外的计算?
      • 不完全是。对于每个范围(彼此独立),给定范围内变量的偏移量必须相对于该范围的基础已知。 “base”的实际位置取决于函数调用顺序,即由 SP 跟踪运行时。这不是“所有堆栈都在一起”,而是逐个范围。 ...
      • ... 您无法在编译时知道堆栈的大小,因为您无法知道函数调用在运行时的嵌套级别。然而,操作系统强加了一个已知的每个线程的最大值,以控制对进程的资源分配。但这只是一个限制,而不是预先计算的值。
      • 顺便说一句,堆栈上变量的地址不是相对于堆栈指针定义的,而是相对于堆栈的开头,对吗?因为如果我们将x 定义为SP - 4,然后我们添加一个新变量y,它现在将是SP - 4,并且x 必须更新为SP - 8。因此,根据堆栈的开头设置地址更有意义,它保持设置,而不是不断移动的堆栈顶部。我错了吗?
      • @AvivCohn:这主要取决于编译器进行什么样的静态分析:给定一个范围,您知道有多少变量,因此您知道所需的空间有多宽。从结尾向后计数或从开头向前计数不会改变运行时复杂度。但原点不是“堆栈的开始”,而是“调用函数时堆栈所在的位置”(但我认为您的本意是这样,否则将毫无意义......)。跨度>
      【解决方案5】:

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

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

      我不确定它是否属于 C/C++ 标准,但许多编译器都包含 alloca() 的变体。如果您需要对齐分配,例如从“m”字节边界开始的“n”字节内存(其中 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;
      

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          【解决方案7】:

          虚拟内存是内存的虚拟化,这意味着它表现为它正在虚拟化的资源(内存)。在一个系统中,每个进程都有不同的虚拟内存空间:

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

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

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

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

          关于这一点,我将您的第一个问题重新解释为:

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

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

          • 函数使用的堆栈大小在编译时变得未知,这增加了堆栈管理的复杂性,必须使用额外的寄存器(变量),并且可能会阻碍某些编译器优化。
          • 堆栈在进程开始时被映射并且它具有固定的大小。如果默认情况下将可变大小数据放置在那里,则该大小应该大大增加。不大量使用堆栈的程序会浪费可用的虚拟内存。

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

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

          正如this answer 解释的那样,我们可以。

          【讨论】:

            【解决方案8】:

            阅读一些有关图灵机的信息,以了解事物为何如此。一切都以他们为出发点。

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

            从技术上讲,除此之外的任何东西都是令人憎恶的。

            【讨论】:

              猜你喜欢
              • 2010-12-11
              • 2014-06-01
              • 2019-05-17
              • 2019-12-08
              • 2019-12-07
              • 2020-04-25
              • 2011-09-14
              • 2010-12-05
              • 1970-01-01
              相关资源
              最近更新 更多