堆内存是每个进程独有的吗?还是多个进程共享同一内存位置?

23

每个进程都可以使用堆内存来存储和共享进程内的数据。编程中有一个规则,当我们在堆内存中占用一些空间时,需要在任务完成后释放它,否则会导致内存泄漏。

int *pIntPtr = new int;
.
.
.
delete pIntPtr;

我的问题是:堆内存是每个进程独立的吗?

如果是,

那么只有当进程处于运行状态时才可能出现内存泄漏。

如果不是,

那么这意味着操作系统能够在某个地方保留数据的内存。如果是这样的话,是否有一种方法可以让另一个进程访问这段内存?这也可以成为进程间通信的一种方式。

我认为我的问题的答案是“是”。请提供您宝贵的反馈意见。


3
我同意大多数答案,但需要注意当他们提到当前操作系统时指的是当前的桌面/服务器操作系统。在SymbianOS中(一些智能手机,主要是诺基亚),内存泄漏无法恢复,即使应用程序已被系统有效关闭,也需要重新启动机器。我可以想象,在没有这种功能的嵌入式操作系统中只是其中一个例子,可能还有其他类似情况。 - David Rodríguez - dribeas
FYI:一些库(如Boost.Interprocess)允许在进程之间共享内存(这不是常见的堆)。在这种情况下,泄漏是共享的。特别是如果一个进程崩溃并未能清理。 - Matthieu M.
6个回答

33

在几乎所有当前使用的系统中,堆内存都是针对每个进程的。在没有受保护内存的旧系统上,堆内存是系统范围内共享的。(简而言之,这就是受保护内存所做的事情: 它使您的堆和栈对您的进程私有。)

因此,在任何现代系统上,如果进程在调用delete pIntPtr之前终止,pIntPtr仍将被释放(虽然它的析构函数不会被调用,因为int类型没有析构函数)。

请注意,受保护内存是实现细节,不是C++或C标准的特性。一个系统可以自由地在进程之间共享内存(现代系统之所以不这样做,是因为这是攻击者攻击的好方法。)


3
+1:表示这不是语言特性,而是操作系统实现的选择。 - ereOn
只要我们一直在追求严谨,ISO C标准根本不涉及“堆”,而ISO C++仅在std::make_heap和相关的STL算法上下文中使用它。因此,这个正确的标准术语应该是“动态存储期”。因此,“堆”本身严格来说只是一个实现细节,而不是语言特性;当然,由此可以得出任何所述堆的属性也都是实现细节的结论。 - Pavel Minaev
True;然而,“heap”更容易打。;p - Jonathan Grynspan
我认为在讨论C语言时很少见到“堆(heap)”这个术语。它似乎仅限于完全抽象内存的高级语言(如C#,Java,PHP等)。在C++讨论中确实会提到“堆(heap)”,但我仍然很少见到它的使用。 - Jonathan Grynspan
我认为这与一个人的编程专业有关,比其他任何事情都更重要。如果你使用的语言具有裸机内存访问(C、C++、Obj-C,除了内存管理),你往往不会将内存视为一个遥远的抽象概念(堆)。而Java等编程人员首先不需要关心内存。 - Jonathan Grynspan
@Pavel:如果我可以继续吹毛求疵的话,至少C++标准是提到了“自由存储区(free store)”,它等价于人们在讨论内存管理时通常所指的“堆(heap)”。而具有相同名称的数据结构是不相关的(或许只是历史上的原因),我想这就是为什么标准选择了一个不同的名称。 - Mike Seymour

4
在大多数现代操作系统中,每个进程都有自己的堆,该堆仅由该进程访问,并在进程终止时被回收 - 这个“私有”堆通常由 new 使用。此外,可能存在全局堆(例如查看 Win32 GlobalAlloc() 函数家族),它在进程之间共享,在系统运行时持久存在,并且确实可用于进程间通信。

1
据我所知,使用GlobalAlloc分配的内存不能再与其他进程共享。它已被文件映射所取代。 - adf88
1
如果是真的,那么在我有限的思维中,这个函数的名称就非常有趣了。 :) - unwind
MSDN: "由GlobalAlloc和LocalAlloc分配的内存对象位于私有、已提交的页面中,具有读/写访问权限,其他进程无法访问。使用GMEM_DDESHARE的GlobalAlloc分配的内存实际上不像在16位Windows中那样在全局共享。此值没有影响,仅供兼容性使用。需要用于其他目的的共享内存的应用程序必须使用文件映射对象。多个进程可以映射同一文件映射对象的视图,以提供命名的共享内存" - adf88

2
通常,进程内存分配发生在堆管理之下。
换句话说,堆是建立在操作系统赋予进程的虚拟地址空间内,并且是该进程私有的。当进程退出时,该内存会被操作系统回收。
需要注意的是,C++标准并未规定这一点,这是C++运行环境的一部分,因此ISO标准并不规定此行为。我所讨论的是常见的实现方式。
在UNIX中,使用brksbrk系统调用来从操作系统中分配更多内存以扩展堆。然后,一旦进程结束,所有这些内存都会被还给操作系统。
获得可以超越进程生命周期的内存的正常方法是使用共享内存(在UNIX类型操作系统下,不确定Windows是否也适用)。这可能会导致泄漏,但更多的是系统资源而不是进程资源。

1

有一些特殊用途的操作系统在进程退出时不会回收内存。如果你的目标是这样的操作系统,你可能已经知道了。

大多数系统不允许你访问另一个进程的内存,但同样存在一些独特的情况不是这样的。

C++标准处理这种情况的方式是不对未释放内存后退出会发生什么以及如果尝试访问不明确属于你的内存会发生什么做出任何声明。这就是“未定义行为”的本质,也是指针“无效”的含义核心。除了这两个问题之外,还有更多问题,但这两个问题起到了一定的作用。


0

从实际角度来看,你的问题的答案是肯定的。现代操作系统通常会在进程关闭时释放由该进程分配的内存。然而,依赖这种行为是一种非常糟糕的做法。即使我们可以确保操作系统总是以这种方式运行,代码也很脆弱。如果某个未能释放内存的函数突然被重新用于其他目的,它可能会导致应用程序级别的内存泄漏。

尽管如此,这个问题的性质和发布的示例要求我在道德上指出RAII给你和你的团队参考。

int *pIntPtr = new int;
...
delete pIntPtr;

这段代码存在内存泄漏的问题。如果 [...] 中出现任何异常,就会导致内存泄漏。有几种解决方案:

int *pIntPtr = 0;
try
{
    pIntPtr = new int;
    ...
}
catch (...)
{
    delete pIntPtr;
    throw;
}
delete pIntPtr;

第二种解决方案使用nothrow(不一定比第一种更好,但允许在定义pIntPtr时进行合理的初始化):

int *pIntPtr = new(nothrow) int;
if (pIntPtr)
{
    try
    {
         ...
    }
    catch (...)
    {
        delete pIntPtr;
        throw;
    }
    delete pIntPtr;
}

还有一种简单的方法:

scoped_ptr<int> pIntPtr(new int);
...

在这个最后且最好的示例中,无论我们如何退出此块(RAII和智能指针万岁),都不需要在pIntPtr上调用delete,因为这将自动完成。

2
感谢您在这个时候教授RAII(我碰巧已经知道了!)在编写异常安全的C++代码时,RAII至关重要:在允许异常传播之前释放资源(以避免资源泄漏),可以编写适当的析构函数,而不是在可能执行或不执行的异常处理块之间分散和复制清除逻辑。 - Akaanthan Ccoder

0

通常情况下,当进程终止时,操作系统会回收任何泄漏的内存。

因此,我认为C++程序员可以不必显式释放在进程退出之前需要的任何内存;例如,进程中的任何“单例”通常都不会被显式释放。

尽管这种行为可能是特定于操作系统的(尽管它对于Windows和Linux等操作系统都是正确的),但这并不是C++标准的理论部分。


如果你确定分配的只是内存,那么或许可以这样做。然而,你必须意识到构造的对象可能消耗的资源不仅仅是内存。如果是这样的话,在进程终止前进行析构函数调用可能是必要的以确保适当的清理。 - csj
@csj 操作系统(Windows)在进程结束时应恢复该进程的所有资源,包括但不限于打开的文件句柄、互斥体、设备句柄以及图形设备接口对象等。 - ChrisW
没有已知的操作系统可以做到这一点。临时文件可能是最常见的泄漏。句柄已经关闭,但文件仍然留存在磁盘上。 - MSalters
@MSalters,除了临时文件之外,您知道还有哪些“泄漏”类型吗? - ChrisW
1
共享内存映射、子进程、TCP连接(将超时)、正在进行的非原子操作,例如带通配符的ShFileOperation,... - MSalters

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