内存分配/释放?

41

最近我一直在研究内存分配,但对基础知识还有些困惑。我还没有完全理解其中的简单概念。什么是内存分配?会发生什么?我希望能得到以下任何问题的答案:

  1. "内存"分配在哪里?
  2. 这个"内存"是什么?数组中的空间吗?还是其他东西?
  3. 当这个"内存"被分配时会发生什么?
  4. 当内存被释放时会发生什么?
  5. 如果有人可以回答下面这些C++代码中malloc的作用,那将对我很有帮助:

char* x; 
x = (char*) malloc (8);

谢谢。


以上不是C++分配内存的方式。C++使用“new”来分配内存,“delete”来释放内存。 - gongzhitaao
1
哇,如果你真的得到了深刻的答案,我们之后可以将其出版成一本书。有很多方面需要考虑才能给出全面的答案。为了增加得到好答案的机会(适合这里),请先指定是哪个系统:PC、智能手机还是嵌入式系统... - Patrick B.
抱歉gongzhitaao。malloc是来自C语言,我正在导入它。不过从答案中我学到了,对于C++中的内存分配,new和delete绝对是正确的方法。还有针对Patrick的问题:我正在使用PC,并且现在意识到这个话题值得一本书来讨论。 - Isaac
4个回答

68

内存模型

C++标准有一个内存模型,它试图以一种通用的方式建模计算机系统中的内存。标准定义了字节是内存模型中的存储单元,并且内存由字节组成(§1.7):

C++内存模型中的基本存储单元是字节。[...] C++程序可用的内存由一个或多个连续字节序列组成。

对象模型

标准总是提供一个对象模型。这指定了对象是存储区域(因此它由字节组成并驻留在内存中)(§1.8):

C++程序中的构造物创建、销毁、引用、访问和操作对象。对象是存储区域。

所以,内存是对象存储的地方。要将对象存储到内存中,必须分配所需的存储区域。

分配和释放函数

标准提供了两个隐式声明的全局范围分配函数:

void* operator new(std::size_t);
void* operator new[](std::size_t);

这些的实现并不是标准所关心的内容。重要的是它们应该返回一个指向存储区域的指针,其字节数与传递的参数相对应(§3.7.4.1):

分配函数尝试分配请求的存储量。如果成功,它应该返回一个指向存储块起始地址的指针,其长度(以字节为单位)至少与请求大小一样大。从分配函数返回时,分配的存储内容无任何限制。

它还定义了两个相应的释放函数:
void operator delete(void*);
void operator delete[](void*);

以下函数被定义为释放之前已分配的存储空间 (§3.7.4.2):

如果标准库中的 deallocation 函数所接收的参数是一个非空指针 (4.10),则该 deallocation 函数必须释放由指针引用的存储空间,并使所有指向已释放存储空间的任何部分的指针无效。

newdelete

通常情况下,您不需要直接使用分配和释放函数,因为它们只会给您未初始化的内存。相反,在 C++ 中,您应该使用 newdelete 来动态地分配对象。一个 new-expression 通过使用上述分配函数之一来获取所请求类型的存储空间,然后以某种方式初始化该对象。例如,new int() 将分配一个 int 对象的空间,然后将其初始化为 0。参见 §5.3.4:

新表达式通过调用分配函数 (3.7.4.1) 获得对象的存储空间。

[...]

创建类型 T 的对象的 new 表达式初始化该对象 [...]

相反,delete 将调用对象的析构函数(如果有)然后释放存储空间 (§5.3.5):

如果 delete-expression 的操作数的值不是空指针值,则 delete-expression 将调用正在删除的对象或数组元素的析构函数(如果有)。

[...]

如果 delete-expression 的操作数的值不是空指针值,则 delete-expression 将调用 deallocation 函数 (3.7.4.2)。

其他分配方式

然而,这些并不是分配或释放存储空间的唯一方式。许多语言结构隐式要求分配存储空间。例如,给出一个对象定义,如 int a;,也需要存储空间 (§7):

定义会导致适当数量的存储空间被保留,并进行任何适当的初始化 (8.5)。

C 标准库: mallocfree

此外,<cstdlib> 头文件引入了 stdlib.h C 标准库的内容,其中包括 mallocfree 函数。它们也被 C 标准定义为分配和释放内存,类似于 C++ 标准定义的分配和释放函数。这是 malloc 的定义 (C99 §7.20.3.3):

void *malloc(size_t size);
描述
malloc函数分配指定大小的对象空间,其值不确定。
返回值
malloc函数返回空指针或分配空间的指针。

free的定义(C99 §7.20.3.2):

void free(void *ptr);
描述
free函数释放指向的空间,使其可以被重新分配。如果ptr是空指针,则不执行任何操作。否则,如果参数与之前由callocmallocrealloc函数返回的指针不匹配,或者空间已经被freerealloc调用释放,则行为未定义。

然而,在C++中使用mallocfree没有充分的理由,因为C++有自己的替代方法。


问题的回答

直接回答您的问题:

  1. Where is the "memory" that is being allocated?

    The C++ standard doesn't care. It simply says that the program has some memory which is made up of bytes. This memory can be allocated.

  2. What is this "memory"? Space in an array? Or something else?

    As far as the standard is concerned, the memory is just a sequence of bytes. This is purposefully very generic, as the standard only tries to model typical computer systems. You can, for the most part, think of it as a model of the RAM of your computer.

  3. What happens exactly when this "memory" gets allocated?

    Allocating memory makes some region of storage available for use by the program. Objects are initialized in allocated memory. All you need to know is that you can allocate memory. The actual allocation of physical memory to your process tends to be done by the operating system.

  4. What happens exactly when the memory gets deallocated?

    Deallocating some previously allocated memory causes that memory to be unavailable to the program. It becomes deallocated storage.

  5. It would also really help me if someone could answer what malloc does in these C++ lines:

    char* x; 
    x = (char*) malloc (8);
    

    Here, malloc is simply allocating 8 bytes of memory. The pointer it returns is being cast to a char* and stored in x.


1
谢谢!那真的非常有帮助。它甚至回答了我在阅读时想到的一个问题。不过,我现在又有一个问题。内存分段是否是内存分配中的一个问题?例如:10个未使用的字节卡在两个已分配的内存块中。或者通常不被视为问题?再次感谢! - Isaac
2
@Isaac 如果您创建本地变量或使用newdelete动态分配对象,则无需关心分配问题。编译器将确保分配正确数量的存储空间。类类型通常在成员之间包含填充字节,但它们具有一定目的。就标准而言,您不应该需要关心这些内容。然而,在实践中,您可能需要关注这些内容。一些与此相关的最热门的SO问题(这里这里,等等)。 - Joseph Mansfield
1
我理解你的问题。比如说,你为一个字符串分配了100个字节,但只使用了50个字节,那么剩余的字节就是空着的。而且重点是它们仍然被分配了。这意味着它们不能被用于/重新分配给其他任务。因此,未使用的字节不可用,这显然构成了一个问题。针对这种问题,在标准C中有一个realloc()函数,它会释放现有内存,为新位置分配请求的内存,并将现有内容复制到该位置。 - Shameel Mohamed
1
因此,您可以使用realloc()在需要时分配额外的内存,无需担心剩余未使用的内存。我不知道C++中是否有realloc()的替身,请告诉我如果您找到了。 - Shameel Mohamed

13

1) "memory"被分配在哪里?

这完全取决于你的操作系统、编程环境(gcc vs Visual C++ vs Borland C++ vs 其他任何工具)、计算机及可用内存等因素。一般情况下,内存是从堆中进行分配的,堆是一个等待你使用的内存区域。它通常会使用可用的RAM。但总会有例外情况。就大多数情况而言,只要分配内存,它来自哪里并不是一件很重要的事情。还有一些特殊类型的内存,例如虚拟内存,可能实际上并没有在任何时候存在于RAM中,并且如果你的真实内存不足,则可以将其移动到硬盘(或类似的存储设备)中。详细的解释需要写很长!

2) 这个"memory"是什么?数组中的空间吗?还是其他东西?

内存通常是您计算机中的RAM。如果将内存视为一个巨大的“数组”,它确实像一个数组一样运行,那么请将其视为大量字节(8位值,类似于unsigned char值)。它从内存底部的索引0开始。但是同样地,在这里有大量的例外情况,一些内存部分可能映射到硬件,或者根本不存在!

3) "memory"被分配时会发生什么?

在任何时候,软件都应该有一些可供分配的内存(我们真的希望如此!)。它是如何分配的高度依赖于系统。通常情况下,分配一个内存区域,分配器将其标记为已使用,然后给您返回一个指针,告诉程序Memory所在的所有系统内存中哪里。在您的示例中,程序将查找8个字节(char)的连续块并在将其标记为“正在使用”后返回它找到该块的位置的指针。

4) "memory"被释放时会发生什么?

系统将该内存标记为可用以再次使用。这非常复杂,因为这通常会导致内存中的空洞。分配8个字节,然后再分配8个字节,然后释放第一个8个字节,你就得到了一个空洞。关于处理释放、内存分配等方面,有整本书写过。因此,希望简短的回答就足够了!

5) 如果有人能回答以下C++代码行中的malloc是做什么,那也会对我非常有帮助:

假定它在一个函数中(顺便说一下,永远不要这样做,因为它不释放你的内存并导致内存泄漏):

void mysample() {
  char *x; // 1
  x = (char *) malloc(8); // 2
}

1) 这是在本地堆栈空间保留的指针。它尚未初始化,因此指向该内存位中存在的任何内容。

2) 它使用参数为 8 调用 malloc。强制转换只是让 C/C++ 知道你打算将其视为 (char *),因为它返回一个没有应用类型的 (void *)。然后,结果指针被存储在变量 x 中。

在非常简单的 x86 32 位汇编中,这看起来大致像:

PROC mysample:
  ; char *x;
  x = DWord Ptr [ebp - 4]
  enter 4, 0   ; Enter and preserve 4 bytes for use with 

  ; x = (char *) malloc(8);
  push 8       ; We're using 8 for Malloc
  call malloc  ; Call malloc to do it's thing
  sub esp, 4   ; Correct the stack
  mov x, eax   ; Store the return value, which is in EAX, into x

  leave
  ret

实际分配通常在第三点模糊地描述。 Malloc通常只是调用处理所有其余内容的系统函数,就像这里的所有其他内容一样,它在不同的操作系统、系统上有很大不同等。


谢谢!那帮了我很多。不过我现在有点害怕会在内存中创建洞。这是我应该担心的事情吗?还是它只是发生了? - Isaac
1
漏洞经常发生。通常称为碎片化,有很多方法可以解决这个问题。一般来说,除非您一遍又一遍地分配/释放内存,否则它不会对您产生太大影响...在这种情况下,您可能需要比malloc/free(或new/delete)更高级的内存管理器。有关更多(尽管含糊)信息,可以在维基百科上找到足够的描述:http://en.wikipedia.org/wiki/Fragmentation_%28computing%29 - Mark Ormston
抱歉再次打扰你。如果你有时间的话,我真的很感激你的帮助。当你说它“标记”为使用时,这是什么意思?我理解如果字节没有被分配,它可能会被设置为00,如果它被分配和使用,则将是设置值。但是那些分配但未使用的字节呢?是否有一种方法可以将它们与未分配的字节区分开来? - Isaac
没关系!我把同样的代码搞了一下,找到了一个方法。 - Isaac

8

1 . 被分配的“内存”存储在哪里?

从语言角度来看,这并没有具体说明,主要是因为细节问题通常并不重要。此外,C++ 标准倾向于低估硬件细节,以最小化不必要的限制(对编译器可以运行的平台和可能的优化)。

sftrabbit的答案给出了一个很好的概述(这就是你需要的所有内容),但我可以举几个实际例子,以帮助你理解。

示例 1:

在足够老的单用户计算机上(或足够小的嵌入式系统中),大部分物理 RAM 可能直接可供程序使用。在这种情况下,调用 mallocnew 实质上是内部簿记,允许运行时库跟踪当前正在使用的该 RAM 块。你也可以手动做到这一点,但很快就会变得乏味。

示例 2:

在现代多任务操作系统上,物理 RAM 与许多进程和其他任务(包括内核线程)共享。它还用于磁盘高速缓存和后台 I/O 缓冲,并由虚拟内存子系统增强,当它们不被使用时可以将数据交换到磁盘(或其他存储设备)中。

在这种情况下,调用 new 可能首先检查你的进程是否已经有足够的空间可用,如果没有,则从操作系统请求更多。返回的任何内存均可能是物理内存,也可能是虚拟内存(在这种情况下,直到实际访问它之前,物理 RAM 可能不会被分配为存储它)。你甚至无法区分它们,至少不使用特定于平台的 API,因为内存硬件和内核共同隐藏了它们。

2 . 这个“内存”是什么?数组中的空间吗?还是其他东西?

在示例 1 中,它类似于数组中的空间:返回的地址标识了一个可寻址的物理 RAM 块。即使在这里,RAM 地址也不一定是平坦或连续的 - 有些地址可能保留给 ROM 或 I/O 端口。

在示例 2 中,它是指向更虚拟的东西的索引:你的进程地址空间。这是一个抽象,用于隐藏底层的虚拟内存细节。当你访问这个地址时,内存硬件可能直接访问一些真实的 RAM,或者它可能需要请求虚拟内存子系统提供一些。

3 . 这个“内存”被分配时会发生什么?

通常会返回一个指针,你可以使用它来存储尽可能多的字节。在两种情况下,mallocnew 操作符将进行一些管理工作,以跟踪哪些部分正在使用,哪些部分是空闲的。

4 . 当内存被释放时会发生什么?

一般来说,freedelete 会进行一些内存清理操作,以便让内存可以被重新分配使用。

如果有人能回答下面这段C++代码中 malloc 的作用,那么对我来说也将非常有帮助:

char* x; 
x = (char*) malloc (8);

它返回一个指针,该指针要么是NULL(如果它无法找到你想要的8个字节),要么是非NULL值。
关于这个非NULL值,你唯一可以有用地说的是:
  • 访问这8个字节x[0]..x[7]是合法的(和安全的);
  • 访问x[-1]x[8]或者实际上任何x[i]除非0 <= i <= 7是不合法的(未定义行为);
  • 比较任何x, x+1, ..., x+8都是合法的(尽管你不能解引用最后一个);
  • 如果您的平台/硬件/任何其他内容对内存中存储数据的位置有任何限制,则x符合这些限制。

谢谢!我最后看到了你的回答。但它帮助加强了我从其他人那里学到的知识的信心。 - Isaac

3
分配内存意味着向操作系统请求内存。这意味着当程序需要内存时,它会向RAM请求“空间”。例如,如果您想使用一个数组但在运行程序之前不知道其大小,则可以有两种做法: - 声明一个长度为 x 的数组[x],其中 x 是由您决定的任意长度。例如100。但是如果您的程序只需要一个包含20个元素的数组呢?这样就浪费了不必要的内存。 - 接下来,当程序知道 x 的正确大小时,它可以动态分配一个由 x 个元素组成的数组。 内存中的程序分为4个段: -栈(用于调用函数) -代码(二进制可执行代码) -数据(全局变量/数据) -堆,在此段中,您可以找到已分配的内存。 当您决定不再需要已分配的内存时,您将其归还给操作系统。
如果您想分配一个由10个整数组成的数组,请执行以下操作:
int *array = (int *)malloc(sizeof(int) * 10)
然后您可以使用以下命令将其归还给操作系统: free(array)

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