【问题标题】:How does automatic memory allocation actually work in C++?自动内存分配如何在 C++ 中实际工作?
【发布时间】:2010-12-09 10:30:16
【问题描述】:

在 C++ 中,假设没有优化,以下两个程序最终会得到相同的内存分配机器码吗?

int main()
{     
    int i;
    int *p;
}

int main()
{
    int *p = new int;
    delete p;
}

【问题讨论】:

  • 他们怎么能有相同的内存分配?第一个为 int (在堆栈上)和指针(在堆栈上)分配一个大小。第二个在堆栈上分配一个指针,在堆上为一个 int 分配空间。
  • 基本上我的问题是“在运行时,栈和堆有什么区别?”
  • 请参阅我的回答,以更深入地解释虚构(但足够准确)机器上的简单示例。
  • 是的,如果在 Linux 中,最后也是一样的。每个应用程序都会有一个默认的堆区域,无论您的程序中是否有动态分配代码;并且每行“int *p = new int;”,系统只是将默认堆区域的一些字节标记为正在使用,并且不会扩大您的堆区域,因为默认堆区域就足够了。第一个“main(){}”也有一个默认的堆区域。

标签: c++ memory memory-management


【解决方案1】:

为了更好地理解发生了什么,让我们假设我们只有一个非常原始的操作系统,它运行在一个 16 位处理器上,一次只能运行一个进程。这就是说:一次只能运行一个程序。此外,让我们假设所有中断都被禁用。

我们的处理器中有一个叫做栈的结构。堆栈是强加在物理内存上的逻辑结构。假设我们的 RAM 存在于地址 E000 到 FFFF 中。这意味着我们正在运行的程序可以以任何我们想要的方式使用这个内存。假设我们的操作系统说 E000 到 EFFF 是栈,F000 到 FFFF 是堆。

堆栈由硬件和机器指令维护。我们真的不需要做太多的事情来维护它。我们(或我们的操作系统)需要做的就是确保为堆栈的开始设置正确的地址。堆栈指针是一个物理实体,位于硬件(处理器)中,由处理器指令管理。在这种情况下,我们的堆栈指针将设置为 EFFF(假设堆栈向后增长,这很常见,-)。对于像 C 这样的编译语言,当你调用一个函数时,它会将你传入的任何参数推送到堆栈上的函数中。每个参数都有一定的大小。 int 通常是 16 或 32 位,char 通常是 8 位,等等。假设在我们的系统上,int 和 int* 是 16 位。对于每个参数,堆栈指针按 sizeof(argument) 递减 (--),并将参数复制到堆栈上。然后,您在作用域中声明的所有变量都以相同的方式压入堆栈,但它们的值并未初始化。

让我们重新考虑两个与您的两个示例相似的示例。

int hello(int eeep)
{
    int i;
    int *p;
}

在我们的 16 位系统上发生的情况如下: 1)将eeep推入堆栈。这意味着我们将堆栈指针递减到 EFFD(因为 sizeof(int) 为 2),然后实际上将 eeep 复制到地址 EFFE(我们的堆栈指针的当前值,减 1,因为我们的堆栈指针指向第一个可用点分配后)。有时有些指令可以一举完成(假设您要复制适合寄存器的数据。否则,您必须手动将数据类型的每个元素复制到堆栈上的适当位置——顺序很重要! )。

2) 为 i 创造空间。这可能意味着只是递减指向 EFFB 的堆栈指针。

3) 为 p 创造空间。这可能意味着只是将堆栈指针递减到 EFF9。

然后我们的程序运行,记住我们的变量所在的位置(eeep 从 EFFE 开始,i 在 EFFC,p 在 EFFA)。要记住的重要一点是,即使堆栈计数 BACKWARDS,变量仍会向前运行(这实际上取决于字节顺序,但重点是 &eeep == EFFE,而不是 EFFF)。

当函数关闭时,我们只需将堆栈指针增加 (++) 6,(因为 3 个大小为 2 的“对象”,而不是 c++ 类型,已被压入堆栈。

现在,您的第二种情况更难以解释,因为实现它的方法太多,几乎不可能在互联网上解释。

int hello(int eeep)
{
    int *p = malloc(sizeof(int));//C's pseudo-equivalent of new
    free(p);//C's pseudo-equivalent of delete
}

eeep 和 p 仍然像前面的例子一样被压入并分配到堆栈上。然而,在这种情况下,我们将 p 初始化为函数调用的结果。 malloc (或 new,但 new 在 c++ 中做得更多。它在适当的时候调用构造函数,以及其他所有东西。)所做的就是进入这个称为 HEAP 的黑盒并获取空闲内存的地址。我们的操作系统将为我们管理堆,但我们必须让它知道我们何时需要内存以及何时使用完。

在示例中,当我们调用 malloc() 时,操作系统将通过给我们这些字节的起始地址来返回一个 2 字节的块(我们系统上的 sizeof(int) 为 2)。假设第一次调用给了我们地址 F000。然后,操作系统会跟踪当前正在使用的地址 F000 和 F001。当我们调用 free(p) 时,操作系统会找到 p 指向的内存块,并将 2 个字节标记为未使用(因为 sizeof(star p) 为 2)。相反,如果我们分配更多内存,则地址 F002 可能会作为新内存的起始块返回。请注意, malloc() 本身就是一个函数。当 p 被压入堆栈以进行 malloc() 的调用时,p 被再次复制到堆栈中第一个打开的地址,该地址在堆栈上有足够的空间以适应 p 的大小(可能是 EFFB,因为我们只压入了 2这次堆栈上的东西大小为 2,并且 sizeof(p) 为 2),堆栈指针再次递减到 EFF9,malloc() 将从该位置开始将其局部变量放在堆栈上。当 malloc 完成时,它将所有项目从堆栈中弹出,并将堆栈指针设置为调用之前的值。 malloc() 的返回值,一个空星,可能会被放置在某个寄存器中(在许多系统上通常是累加器)供我们使用。

在实施过程中,这两个例子都没有这么简单。当您为新函数调用分配堆栈内存时,您必须确保保存状态(保存所有寄存器),以便新函数不会永久擦除值。这通常也涉及将它们推入堆栈。同样的方法,你通常会保存程序计数器寄存器,以便子程序返回后可以返回正确的位置。内存管理器会用尽自己的内存,以便“记住”哪些内存已经发出,哪些没有。虚拟内存和内存分段使这个过程更加复杂,内存管理算法必须不断移动块(并保护它们)以防止内存碎片(它自己的整个主题),这与虚拟内存有关也是。与第一个示例相比,第二个示例确实是一大罐蠕虫。此外,运行多个进程使这一切变得更加复杂,因为每个进程都有自己的堆栈,并且堆可以被多个进程访问(这意味着它必须保护自己)。此外,每种处理器架构都不同。有些架构会希望您将堆栈指针设置为堆栈上的第一个空闲地址,而其他架构会希望您将其指向第一个非空闲地址。

我希望这会有所帮助。请告诉我。

请注意,以上所有示例都是针对过度简化的虚构机器。在真正的硬件上,这会有点麻烦。

编辑:星号没有出现。我用“明星”这个词代替了它们


不管怎样,如果我们在示例中使用(大部分)相同的代码,分别将“hello”替换为“example1”和“example2”,我们会在 wndows 上得到以下 intel 的汇编输出。

.文件“test1.c” 。文本 .globl _example1 .def _example1; .scl 2; . 类型 32; .endef _example1: 推%ebp movl %esp, %ebp 低于 8 美元,%esp 离开 ret .globl _example2 .def _example2; .scl 2; . 类型 32; .endef _example2: 推%ebp movl %esp, %ebp 低于 8 美元,%esp movl $4, (%esp) 调用 _malloc movl %eax, -4(%ebp) movl -4(%ebp), %eax movl %eax, (%esp) 拨打_free 离开 ret .def _free; .scl 3; . 类型 32; .endef .def _malloc; .scl 3; . 类型 32; .endef

【讨论】:

  • 正如可以预料的那样乏味的事情,我不断发现一些小错误。请对它们发表评论,如果我认为这是一个好点,我会更新。
  • FF002 => F002。非常好的入口!我认为您可以通过一些您所谈论的内存布局的 ascii 图更清楚地说明,但这需要更多的工作。 :)
  • 这现在是一个社区维基,所以如果有人想添加图表(好建议!),请随时这样做。
【解决方案2】:

不,没有优化...

int main() 
{      
    int i; 
    int *p; 
}

几乎什么都不做 - 只是调整堆栈指针的几条指令,但是

int main() 
{ 
    int *p = new int; 
    delete p; 
}

在堆上分配一块内存然后释放它,这是一个很大的工作(我在这里是认真的 - 堆分配不是一个简单的操作)。

【讨论】:

  • +1,堆分配调用了很多记账信息,只谈内存开销。
【解决方案3】:
    int i;
    int *p;

^栈上分配一个整数和一个整数指针

int *p = new int;
delete p;

^ 在栈上分配一个整数指针,在堆上分配整数大小的块

编辑:

堆栈段和堆段的区别


(来源:maxi-pedia.com

void another_function(){
   int var1_in_other_function;   /* Stack- main-y-sr-another_function-var1_in_other_function */
   int var2_in_other_function;/* Stack- main-y-sr-another_function-var1_in_other_function-var2_in_other_function */
}
int main() {                     /* Stack- main */
   int y;                        /* Stack- main-y */
   char str;                     /* Stack- main-y-sr */
   another_function();           /*Stack- main-y-sr-another_function*/
   return 1 ;                    /* Stack- main-y-sr */ //stack will be empty after this statement                        
}

每当任何程序开始执行时,它都会将其所有变量存储在称为堆栈段的特殊内存位置。例如,在 C/C++ 的情况下,调用的第一个函数是 main。所以它将首先放入堆栈。 main 中的任何变量都将在程序执行时放入堆栈。现在 main 是第一个调用的函数,它将是最后一个返回任何值的函数(或者将从堆栈中弹出)。

现在,当您使用new 动态分配内存时,会使用另一个特殊的内存位置,称为堆段。即使堆指针上存在实际数据,也位于堆栈上。

【讨论】:

  • 这很有帮助。在您的图表中,您用箭头绘制了堆 - 我想您的意思是堆栈和堆之间可以有任意距离(不属于程序的内存)?
  • 为每个进程分配4GB的虚拟内存,这样内存就会分布在这些段之间
【解决方案4】:

听起来你不知道堆栈和堆。您的第一个示例只是在堆栈上分配一些内存,一旦超出范围就会被删除。使用 malloc/new 获得的堆内存将一直存在,直到您使用 free/delete 将其删除。

【讨论】:

  • 当它“一旦超出范围就被删除” - 编译器是否放入代码来执行此操作?
  • 编译器处理堆栈和范围规则所需的堆栈帧,所以在某种程度上是的......但是“删除”堆栈上的某些东西是微不足道的,因为它实际上只涉及递减堆栈指针, 和堆分配和释放完全不同。
  • 是的,它向后移动堆栈指针。堆栈的大小是固定的(或者至少你可以假装是)。
  • 还有这个堆栈指针的东西——操作系统给我们的?
  • @Tarquila:处理器有一个堆栈指针,编译器可以编写代码直接操作它。
【解决方案5】:

在第一个程序中,您的变量都驻留在堆栈中。您没有分配任何动态内存。 'p' 只是坐在堆栈上,如果你取消引用它,你会得到垃圾。在第二个程序中,您实际上是在堆上创建一个整数值。在这种情况下,'p' 实际上指向一些有效的内存。您实际上可以取消引用 p 并将其安全地设置为有意义的值:

*p = 5;

这在第二个程序(删除之前)中有效,而不是第一个。希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 2019-01-17
    • 1970-01-01
    • 2014-09-24
    • 2013-12-20
    • 1970-01-01
    • 2021-07-24
    • 1970-01-01
    • 2016-10-26
    • 1970-01-01
    相关资源
    最近更新 更多