堆地址与栈地址共享吗?

4

我了解到,在大多数操作系统中,内存地址是从最高位到最低位排列的。因此,我想知道堆、栈和全局内存是否都属于相同的排序方式..?

如果我创建了...

pointerType* pointer = new pointerType  //creates memory address 0xffffff

然后在堆栈上创建了一个本地变量。

localObject object

本地对象的地址是否为0xfffffe?

或者堆栈的排序完全不同。

9个回答

4
内存地址从高到低开始。
你街上的房子地址是从高到低还是从低到高排序的?这取决于你开车的方向。
就像邮政地址一样,内存地址并没有真正的顺序。每个地址只是简单地标识了内存中的一个唯一位置(至少在概念上是这样的。我们暂时忽略分段或虚拟内存)。
但当你的邮递员送信时,他很可能按照从高到低或从低到高的顺序工作(可能两者都有,沿着街道的一侧下来,另一侧上去)。这样做当然比随机跳跃到每个房子更有效率。此外,这使得邮递员的工作更加简单。如果他按照随机顺序跳跃到每个房子,那么很难跟踪他已经访问过哪些房子,哪些房子仍需要投递。如果他只是按顺序进行,那么他的卡车位置就足以跟踪所有信息。
一个栈与此类似。它不会占据内存的任意位置,而是有一个第一个位置,其后续位置从那里按逻辑顺序依次排列。因此,只需要一个栈指针(通常为“SP”)来跟踪哪些栈位置被占用,哪些是空闲的。
然而,堆必须是不同的。虽然栈本质上具有先进后出的顺序,但堆本质上是无序的。堆内存可以在任何时候分配和释放。早期的分配可以比后期的分配更长久存在。因此,堆必须能够分配任意地址范围,并跟踪它们所有。
由于栈和堆的运作方式不同,它们应该占据内存的不同区域。在您的示例中,第二个栈分配将覆盖您的堆分配所占用的内存。显然,这是一件坏事,就是所谓的栈溢出
大多数现代CPU都具备将堆栈内存和堆内存完全分离的必要功能。这就是内存段和虚拟内存发挥作用的地方。在某些平台上,堆栈和堆可能由相同的地址范围标识,同时仍占据不同的物理内存甚至是二级存储器的区域。本文不讨论这是如何实现的。
但是,大多数现代操作系统实际上并没有这样做。更常见的是使用“平坦”的地址空间,其中所有地址(无论是堆、栈、代码还是其他)都指向同一个地址空间。这使应用程序开发人员更容易,因为不需要为每个地址携带段标识符。
在平坦的地址空间中,采用了与古老的CPU相同的堆栈分离方案,这些CPU没有内存分割或虚拟化:栈从内存的“顶部”(较高地址)向下增长,而堆从内存的底部(较低地址)向上增长。两者之间的某一点可以被选为两者的限制点,当一个达到该点时,会出现错误条件——堆栈溢出或内存不足。
当然,这个描述是极度简化的,但希望能够更好地基本理解。

3

堆和栈通常在内存中的两个非常不同的位置。


这意味着如果两个变量在内存中的两个不同位置,则它们的地址可能相同吗? - numerical25
2
@numerical25,这意味着完全相反的情况。因为它们在内存中是两个不同的位置,所以没有两个变量可以具有相同的地址。 - JSBձոգչ

3

通常情况下,您会发现堆栈从某个起始地址向下增长,而堆从完全不同的起始地址向上增长。

然而,堆栈和堆(在C++标准中称为动态存储)的确切工作方式是由实现定义的,并不受C++标准的约束。


2

这取决于情况。在典型的 CPU 上,您拥有一个地址空间来存储所有内容,因此任何两个变量必须具有不同的地址。但是,大多数 CPU 支持虚拟寻址,这种情况下可能会出现两个不同的虚拟地址指向同一个物理地址。

然而,在 DSP(例如)上,通常有两到三个完全独立的地址空间 -- 例如,代码和数据将具有完全独立的物理 寻址(即,一个用于数据的内存芯片集连接到一个内存总线上,另一个用于代码的芯片集连接到另一条总线上)。在这种情况下,一个物理地址可能会引用内存的两个不同部分; 如果不知道它要引用的类型,则该地址可能根本无法区分这两个部分。


2
在现代操作系统中,情况比这更复杂,但以下内容可以帮助您入门: 在大多数个人计算机类系统上,栈和堆都是同一个地址空间的一部分,但按照惯例,堆被认为是不属于栈、全局变量空间(.data 和 .bss)或程序代码空间(.text)的任何 RAM,并且栈通常从高地址开始,并向下增长,当有新元素被压入时。 这种情况通常被称为冯·诺依曼结构 - 所有数据和代码都存在于一个通用地址空间中。 哈佛架构类型是另一种架构类型。在哈佛体系结构中,代码存储在 ROM 中,大多数数据存储在 RAM 中,RAM 和 ROM 不共享地址空间。这意味着函数和变量可能具有相同的数字地址,但仍然不在同一位置。Atmel 的 AVR 架构是一个很好的例子,除了一些小版本只有没有 RAM 的寄存器(这只会混淆问题),以及一些将地址空间从 16 位扩展到 24 位并可能(不确定此项功能)模糊了地址空间边界的大版本。 8051 系列处理器仍然不同,并且与您的问题更相关。通常,它们具有少量快速 RAM,这是栈区域,与通用 RAM 的不同地址空间中。全局变量和可能的堆区域所在的位置。它们还通常将其代码放置在另一个单独的地址空间中。 堆实际上只是一种对未使用的内存/地址空间进行处理的方法。从这个备用内存中分配和释放内存的算法就是使其成为“堆”的原因。 由于通常存在用于调用、返回、推送和弹出的指令,因此堆栈通常有点不同,但这些甚至都不是必要的。当您开始处理中断时,处理器的“发生了中断”操作需要使用堆栈在执行中断服务例程之前保存处理器的状态,因此系统堆栈的硬件支持变得必要。 现代台式机/工作站操作系统在现代硬件上通常能够利用内存管理硬件来扭曲应用程序对地址空间的视图,以使应用程序更容易处理,并允许多个应用程序共享 RAM,因此这通常是错误的。
如果你使用线程编程,那么你的应用程序可能会有多个堆栈,因为每个线程通常需要自己的堆栈。构成此堆栈的内存通常以与从堆区分配内存的方式非常相似的方式分配,并且甚至可以使用堆分配函数。但有时会以不同的方式完成,以便堆栈可以动态增长。

1

pointerType* pointer = new pointerType //创建内存地址0xffffff

你误解了0xffffff。它不是新分配的内存地址,而是指针变量本身的地址。如果你想检查内存分配的位置,需要检查存储在0xffffff中的值。


0

栈和堆并不是互相排斥的。想象这两种假设的编译器实现:

void PreMain()
{
    char initialHeap[initialHeapSize]
    HeapPointer heapHead = &initialHeap;
    ...
    int returncode = main(argc, argv);
    ...
}

void PreMain()
{
    void * stack = GetFromOSHeap(stackSize);
    // some assembly intrinsic to replace the stack pointer
    ...
    int returncode = main(argc, argv);
    ...
}

你可能在主流编译器中找不到这些,但我敢打赌肯定有某个嵌入式系统是这样工作的。


0

指针本身和局部对象都位于栈上。指针所指向的对象位于堆上。

这就是为什么在 C 中经常说只有传值。当你将指针传递给函数时,与传递 int 没有任何区别:函数参数接收端上的指针/int 是栈上的另一 "个" 变量。然而,由于指针的值(指向堆对象的地址)被复制到接收端,因此可以访问相同的堆对象。


0

你可以随时检查一下。这是其中一件事情,你可以通过输出值并检查它来找出答案。这并不能保证在所有计算机或编译器上都有相同的行为,但你可以看看你的情况如何。只需printf("0x%x\n", &variable)以查看变量的地址。在堆上分配一些东西,在栈上分配一些东西,然后检查它们的所有地址,你就会看到它们的位置以及每个地址向哪个方向扩展。

我通常会在我使用的每种语言中保留一个测试项目,只是为了快速添加一些内容,以便自己看看它是如何工作的。这样做非常适合像这样的情况,你甚至比StackOverflow提供答案更快得到你的答案。


是的,但如果缺乏特定硬件架构和操作系统的知识,你可能会基于那种经验性测试得出错误的结论。 - BobbyShaftoe
@BobbyShaftoe 我不知道怎么做。我提到了规则可能因环境而异的事实,但只要你进行一系列测试并获得一致的结果,那么你就知道你的环境是如何运作的。是的,你需要记住在另一个环境中可能会有所不同,但通常情况下,只要你记住可能没有官方规范,你就可以通过实践学习。如果你担心是否有规范,那就是一个不同但密切相关的问题。如果问题是“我测试了这个,它是否被指定为如此?”,我们本可以直接回答“不”。 - Loduwijk
1
有一种方法是考虑来自@nategoose的答案。如果你处于一种非常不同的架构中,并且不了解其基本原理,那么这种实验可能并不能告诉你太多,甚至会让你感到困惑。这是我的基本观点。 - BobbyShaftoe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接