Linux 动态内存分配与管理全解析
1. 动态内存分配概述
在编程中,很多时候我们需要在运行时动态地分配内存,而不是在编译时就确定内存的大小。这是因为在程序运行前,我们可能无法预知所需内存的具体大小,或者需要根据用户的输入动态调整内存的使用。例如,当我们要存储文件内容或用户从键盘输入的数据时,由于文件大小未知,用户输入的字符数量也不确定,所以需要动态地分配内存。
在 C 语言中,没有直接支持动态内存的变量类型。例如,C 语言没有提供直接获取存在于动态内存中的
struct pirate_ship
的机制,而是提供了分配足够内存来存储
pirate_ship
结构体的方法,程序员通过指针来操作这块内存。
2. 经典的动态内存分配函数:malloc()
malloc()
是 C 语言中用于获取动态内存的经典接口,其原型如下:
#include <stdlib.h>
void * malloc (size_t size);
-
当
malloc()调用成功时,它会分配size字节的内存,并返回一个指向新分配内存区域起始位置的指针。需要注意的是,这块内存的内容是未定义的,不会自动初始化为零。 -
如果调用失败,
malloc()会返回NULL,并将errno设置为ENOMEM。
以下是
malloc()
的使用示例:
// 分配固定数量的字节
char *p;
/* give me 2 KB! */
p = malloc (2048);
if (!p)
perror ("malloc");
// 分配一个结构体
struct treasure_map *map;
/*
* allocate enough memory to hold a treasure_map stucture
* and point 'map' at it
*/
map = malloc (sizeof (struct treasure_map));
if (!map)
perror ("malloc");
在 C 语言中,赋值时会自动将
void
指针提升为其他指针类型,所以上述示例不需要对
malloc()
的返回值进行类型转换。但在 C++ 中,不会自动进行
void
指针的提升,因此需要进行类型转换,示例如下:
char *name;
/* allocate 512 bytes */
name = (char *) malloc (512);
if (!name)
perror ("malloc");
不过,有些 C 程序员喜欢对返回
void
指针的函数结果进行类型转换,包括
malloc()
。但这种做法并不可取,因为如果函数的返回值类型发生变化,类型转换会掩盖错误;而且如果函数没有正确声明,类型转换也会掩盖潜在的 bug。
由于
malloc()
可能返回
NULL
,所以开发者在使用时必须始终检查并处理错误情况。很多程序会定义一个
malloc()
的包装函数,当
malloc()
返回
NULL
时,打印错误信息并终止程序,这个常见的包装函数通常被称为
xmalloc()
:
/* like malloc(), but terminates on failure */
void * xmalloc (size_t size)
{
void *p;
p = malloc (size);
if (!p) {
perror ("xmalloc");
exit (EXIT_FAILURE);
}
return p;
}
3. 数组的动态分配:calloc()
当需要动态分配数组时,数组元素的大小可能是固定的,但元素的数量是动态的,这种情况下动态内存分配会比较复杂。为了简化这种情况,C 语言提供了
calloc()
函数,其原型如下:
#include <stdlib.h>
void * calloc (size_t nr, size_t size);
-
当
calloc()调用成功时,它会返回一个指向适合存储nr个元素的内存块的指针,每个元素的大小为size字节。 -
与
malloc()不同的是,calloc()会将返回的内存块中的所有字节初始化为零。
以下是
calloc()
的使用示例:
int *x, *y;
x = malloc (50 * sizeof (int));
if (!x) {
perror ("malloc");
return -1;
}
y = calloc (50, sizeof (int));
if (!y) {
perror ("calloc");
return -1;
}
在这个示例中,
x
指向的数组元素内容是未定义的,而
y
指向的数组元素的值都为 0。除非程序会立即设置所有元素的值,否则建议使用
calloc()
来确保数组元素不会被填充为无意义的数据。
关于
calloc()
名称的由来,存在一些争议。Unix 历史学家们争论
c
是代表
count
(因为该函数接受数组元素的数量作为参数),还是代表
clear
(因为该函数会将内存清零)。经过询问,Brian Kernighan 认为
c
代表
clear
。
由于
calloc()
可以直接提供已经清零的内存,所以在需要将动态内存清零时,使用
calloc()
比后续使用
memset()
函数更高效。当
calloc()
调用失败时,它会返回
NULL
,并将
errno
设置为
ENOMEM
。
开发者还可以自定义与
malloc()
功能相同但会将内存清零的函数
malloc0()
,以及结合
xmalloc()
和
malloc0()
的
xmalloc0()
:
/* works identically to malloc(), but memory is zeroed */
void * malloc0 (size_t size)
{
return calloc (1, size);
}
/* like malloc(), but zeros memory and terminates on failure */
void * xmalloc0 (size_t size)
{
void *p;
p = calloc (1, size);
if (!p) {
perror ("xmalloc0");
exit (EXIT_FAILURE);
}
return p;
}
4. 调整已分配内存的大小:realloc()
C 语言提供了
realloc()
函数来调整已分配内存的大小,其原型如下:
#include <stdlib.h>
void * realloc (void *ptr, size_t size);
-
当
realloc()调用成功时,它会将ptr指向的内存区域调整为size字节的新大小,并返回一个指向新大小内存区域的指针,这个指针可能与ptr相同,也可能不同。 -
当扩大内存区域时,如果
realloc()无法在原地扩展现有内存块,它会分配一个新的size字节的内存区域,将旧区域的内容复制到新区域,并释放旧区域。在任何操作中,内存区域的内容会保留到旧大小和新大小的最小值。由于可能涉及复制操作,扩大内存区域的realloc()操作可能会比较耗时。 -
如果
size为 0,realloc()的效果等同于调用free()释放ptr指向的内存。 -
如果
ptr为NULL,realloc()的操作结果等同于调用malloc()分配新的内存。如果ptr不为NULL,它必须是之前通过malloc()、calloc()或realloc()返回的指针。 -
当
realloc()调用失败时,它会返回NULL,并将errno设置为ENOMEM,ptr指向的内存状态保持不变。
以下是
realloc()
缩小内存区域的示例:
struct map *p;
/* allocate memory for two map structures */
p = calloc (2, sizeof (struct map));
if (!p) {
perror ("calloc");
return -1;
}
/* use p[0] and p[1]... */
struct map *r;
/* we now need memory for only one map */
r = realloc (p, sizeof (struct map));
if (!r) {
/* note that 'p' is still valid! */
perror ("realloc");
return -1;
}
/* use 'r'... */
free (r);
在这个示例中,
realloc()
调用后,
p[0]
的数据会被保留。如果调用失败,
p
仍然有效,可以继续使用并最终释放;如果调用成功,则使用
r
并负责在使用完后释放它。
5. 释放动态内存:free()
与自动分配的内存(在栈展开时会自动回收)不同,动态分配的内存会一直存在于进程的地址空间中,直到手动释放。因此,程序员有责任将不再使用的动态分配内存归还给系统。
在 C 语言中,使用
malloc()
、
calloc()
或
realloc()
分配的内存,在不再使用时必须通过
free()
函数释放,其原型如下:
#include <stdlib.h>
void free (void *ptr);
-
free()函数用于释放ptr指向的内存。ptr必须是之前通过malloc()、calloc()或realloc()返回的指针,不能使用free()释放部分内存块,否则会导致内存状态未定义,可能会引发程序崩溃。 -
如果
ptr为NULL,free()会直接返回,不会进行任何操作。因此,在调用free()之前检查ptr是否为NULL是多余的。
以下是
free()
的使用示例:
void print_chars (int n, char c)
{
int i;
for (i = 0; i < n; i++) {
char *s;
int j;
/*
* Allocate and zero an i+2 element array
* of chars. Note that 'sizeof (char)'
* is always 1.
*/
s = calloc (i + 2, 1);
if (!s) {
perror ("calloc");
break;
}
for (j = 0; j < i + 1; j++)
s[j] = c;
printf ("%s\n", s);
/* Okay, all done. Hand back the memory. */
free (s);
}
}
这个示例会分配
n
个字符数组,数组元素的数量逐渐增加,从 2 个元素到
n + 1
个元素。对于每个数组,将字符
c
写入除最后一个字节外的每个字节,然后将数组作为字符串打印,最后释放动态分配的内存。
如果在这个示例中不调用
free()
,程序将不会将内存归还给系统,并且会丢失对该内存的唯一引用,导致无法再次访问该内存,这种编程错误被称为内存泄漏。内存泄漏和其他动态内存错误是 C 编程中最常见且最具危害性的错误之一,因此 C 程序员必须密切关注所有的内存分配情况。
另一个常见的 C 编程陷阱是
use-after-free
,即释放内存块后又尝试访问该内存。一旦调用
free()
释放了一块内存,程序就不能再访问其内容。程序员必须特别注意悬空指针,即非
NULL
但指向无效内存块的指针。
Valgrind
是一个检测程序内存错误的优秀工具。
6. 内存对齐
6.1 数据对齐的概念
数据对齐是指数据在内存中的排列方式。当
n
是 2 的幂次方,且内存地址
A
是
n
的倍数时,称地址
A
是
n
字节对齐的。系统中的处理器、内存子系统和其他组件都有特定的对齐要求。例如,大多数处理器只能操作字对齐的内存地址,内存管理单元也只处理页对齐的地址。
如果一个变量的内存地址是其大小的倍数,则称该变量是自然对齐的。例如,一个 32 位的变量,如果其内存地址是 4 的倍数(即地址的最低两位为 0),则它是自然对齐的。因此,大小为
2n
字节的类型,其地址的
n
个最低有效位必须为 0。
不同的系统对数据对齐的要求不同,有些机器架构对数据对齐有非常严格的要求,而有些则相对宽松。一些系统会产生可捕获的错误,内核可以选择终止违规进程,或者手动执行未对齐的访问(通常通过多次对齐访问),这会导致性能下降并牺牲原子性,但至少进程不会被终止。在编写可移植性代码时,程序员必须小心避免违反对齐要求。
6.2 分配对齐的内存
在大多数情况下,编译器和 C 库会自动处理对齐问题。POSIX 规定,通过
malloc()
、
calloc()
和
realloc()
返回的内存必须与任何标准 C 类型的使用对齐。在 Linux 上,这些函数在 32 位系统上总是返回 8 字节对齐的内存,在 64 位系统上总是返回 16 字节对齐的内存。
有时,程序员需要特定对齐的动态内存,例如页对齐的内存。POSIX 1003.1d 提供了
posix_memalign()
函数来满足这种需求,其原型如下:
/* one or the other -- either suffices */
#define _XOPEN_SOURCE 600
#define _GNU_SOURCE
#include <stdlib.h>
int posix_memalign (void **memptr,
size_t alignment,
size_t size);
-
当
posix_memalign()调用成功时,它会分配size字节的动态内存,并确保该内存地址是alignment的倍数。alignment必须是 2 的幂次方,并且是void指针大小的倍数。分配的内存地址会存储在memptr中,函数返回 0。 -
当调用失败时,不会分配内存,
memptr未定义,函数返回以下错误代码之一: -
EINVAL:alignment不是 2 的幂次方或不是void指针大小的倍数。 -
ENOMEM:没有足够的内存来满足请求的分配。
需要注意的是,
posix_memalign()
不会设置
errno
,而是直接返回错误代码。通过
posix_memalign()
获得的内存可以使用
free()
释放,使用示例如下:
char *buf;
int ret;
/* allocate 1 KB along a 256-byte boundary */
ret = posix_memalign (&buf, 256, 1024);
if (ret) {
fprintf (stderr, "posix_memalign: %s\n",
strerror (ret));
return -1;
}
/* use 'buf'... */
free (buf);
在 POSIX 定义
posix_memalign()
之前,BSD 和 SunOS 分别提供了以下接口:
#include <malloc.h>
void * valloc (size_t size);
void * memalign (size_t boundary, size_t size);
-
valloc()的操作与malloc()相同,只是分配的内存是页对齐的。可以使用getpagesize()函数获取系统的页大小。 -
memalign()与posix_memalign()类似,它将分配的内存按照boundary字节对齐,boundary必须是 2 的幂次方。
以下是使用
valloc()
和
memalign()
分配页对齐内存的示例:
struct ship *pirate, *hms;
pirate = valloc (sizeof (struct ship));
if (!pirate) {
perror ("valloc");
return -1;
}
hms = memalign (getpagesize (), sizeof (struct ship));
if (!hms) {
perror ("memalign");
free (pirate);
return -1;
}
/* use 'pirate' and 'hms'... */
free (hms);
free (pirate);
在 Linux 上,通过这两个函数获得的内存可以使用
free()
释放,但在其他 Unix 系统上可能并非如此,有些系统没有提供安全释放这些函数分配的内存的机制。因此,考虑到可移植性,Linux 程序员应仅在与旧系统兼容时使用这两个函数,
posix_memalign()
更优越且标准化。只有在需要比
malloc()
提供的更大对齐时,才需要使用这三个接口。
6.3 其他对齐问题
对齐问题不仅涉及标准类型和动态内存分配的自然对齐,非标准和复杂类型的对齐要求比标准类型更复杂。此外,在不同类型指针之间赋值和使用类型转换时,对齐问题也更为重要。
非标准和复杂数据类型的对齐要求超出了自然对齐的简单要求,以下是四个有用的规则:
- 结构体的对齐要求与其最大组成类型的对齐要求相同。例如,如果结构体中最大的类型是一个 32 位整数,且该整数的对齐边界为 4 字节,则结构体至少也要按照 4 字节边界对齐。
- 结构体可能需要填充字节,以确保每个组成类型都能正确对齐。例如,如果一个
char
类型(可能对齐为 1 字节)后面跟着一个
int
类型(可能对齐为 4 字节),编译器会在两者之间插入 3 字节的填充,以确保
int
类型位于 4 字节边界上。程序员有时会按大小降序排列结构体的成员,以最小化填充所浪费的空间。
gcc
的
-Wpadded
选项可以帮助检测编译器插入的隐式填充。
- 联合体的对齐要求与其最大联合类型的对齐要求相同。
- 数组的对齐要求与其基类型的对齐要求相同。因此,数组的对齐要求与单个元素的对齐要求相同,这使得数组的所有成员都能自然对齐。
虽然编译器会自动处理大多数对齐要求,但在处理指针和类型转换时,仍可能会遇到对齐问题。例如,通过将指针从较小对齐的内存块转换为较大对齐的内存块来访问数据,可能会导致处理器加载未正确对齐的数据。以下是一个示例:
char greeting[] = "Ahoy Matey";
char *c = &greeting[1];
unsigned long badnews = *(unsigned long *) c;
在这个示例中,
unsigned long
类型通常是 4 或 8 字节对齐的,而
c
可能没有对齐到相同的边界。因此,类型转换后加载
c
会导致对齐违规,根据不同的架构,这可能会导致性能下降甚至程序崩溃。在能够检测但无法正确处理对齐违规的机器架构上,内核会向违规进程发送
SIGBUS
信号,终止该进程。
综上所述,在进行动态内存分配和使用时,我们需要合理选择合适的内存分配函数,注意内存的释放以避免内存泄漏,同时要考虑不同系统的对齐要求,确保程序具有良好的性能和可移植性。
Linux 动态内存分配与管理全解析
7. 动态内存分配与管理的操作流程总结
为了更清晰地理解动态内存分配与管理的过程,下面通过一个 mermaid 流程图来展示主要的操作流程:
graph LR
A[开始] --> B{选择分配函数}
B -->|malloc()| C[分配指定大小内存]
B -->|calloc()| D[分配指定数量和大小的内存并清零]
B -->|realloc()| E[调整已分配内存大小]
C --> F{检查分配结果}
D --> F
E --> F
F -->|成功| G[使用内存]
F -->|失败| H[处理错误]
G --> I{是否需要调整大小}
I -->|是| E
I -->|否| J{是否使用完毕}
J -->|是| K[释放内存]
J -->|否| G
K --> L[结束]
H --> L
8. 常见内存分配函数对比
为了方便大家选择合适的内存分配函数,下面通过一个表格对常见的内存分配函数进行对比:
| 函数名 | 功能描述 | 内存初始化 | 返回值 | 错误处理 |
| ---- | ---- | ---- | ---- | ---- |
|
malloc()
| 分配指定大小的内存 | 未初始化 | 成功:指向分配内存的指针;失败:
NULL
|
errno
设置为
ENOMEM
|
|
calloc()
| 分配指定数量和大小的内存并清零 | 清零 | 成功:指向分配内存的指针;失败:
NULL
|
errno
设置为
ENOMEM
|
|
realloc()
| 调整已分配内存的大小 | 保留原有内容至新旧大小的最小值 | 成功:指向新大小内存的指针;失败:
NULL
|
errno
设置为
ENOMEM
|
|
posix_memalign()
| 分配指定大小且按指定边界对齐的内存 | 未初始化 | 成功:返回 0,内存地址存于
memptr
;失败:返回错误代码 | 直接返回错误代码,不设置
errno
|
|
valloc()
| 分配指定大小且页对齐的内存 | 未初始化 | 成功:指向分配内存的指针;失败:
NULL
| 未提及特殊错误处理 |
|
memalign()
| 分配指定大小且按指定边界对齐的内存 | 未初始化 | 成功:指向分配内存的指针;失败:
NULL
| 未提及特殊错误处理 |
9. 内存管理的最佳实践
在进行动态内存分配和管理时,遵循以下最佳实践可以帮助我们避免常见的错误:
-
始终检查分配结果
:使用
malloc()
、
calloc()
、
realloc()
等函数分配内存后,一定要检查返回值是否为
NULL
,并处理可能的错误。例如:
void *p = malloc(1024);
if (p == NULL) {
perror("malloc");
// 进行错误处理,如退出程序
exit(EXIT_FAILURE);
}
-
及时释放不再使用的内存
:使用完动态分配的内存后,要及时调用
free()函数释放内存,避免内存泄漏。例如:
void *p = malloc(1024);
if (p != NULL) {
// 使用内存
// ...
free(p);
}
-
避免使用悬空指针
:释放内存后,将指针设置为
NULL,避免后续误操作。例如:
void *p = malloc(1024);
if (p != NULL) {
// 使用内存
// ...
free(p);
p = NULL;
}
-
合理选择分配函数
:根据具体需求选择合适的分配函数。如果需要清零内存,优先使用
calloc();如果需要调整内存大小,使用realloc();如果需要特定对齐的内存,使用posix_memalign()。
10. 内存管理错误检测工具
除了前面提到的
Valgrind
工具,还有一些其他的工具可以帮助我们检测内存管理错误:
-
AddressSanitizer(ASan)
:是一个快速的内存错误检测工具,能够检测出多种内存错误,如越界访问、使用已释放的内存等。使用方法如下:
- 编译时添加
-fsanitize=address
选项,例如:
gcc -fsanitize=address -g your_program.c -o your_program
- 运行程序,当检测到内存错误时,会输出详细的错误信息。
-
LeakSanitizer(LSan)
:专门用于检测内存泄漏的工具,它可以与
AddressSanitizer一起使用。使用方法与AddressSanitizer类似,编译时添加相应选项即可。
11. 总结
动态内存分配与管理是编程中非常重要的一部分,尤其是在 C 语言中。通过合理使用
malloc()
、
calloc()
、
realloc()
等函数,我们可以在运行时灵活地分配和使用内存。同时,要注意内存的释放,避免内存泄漏和
use-after-free
等错误。此外,内存对齐也是一个需要关注的问题,不同的系统和数据类型有不同的对齐要求,我们需要根据具体情况进行处理。在实际编程中,遵循最佳实践并使用合适的错误检测工具,可以帮助我们编写出更健壮、高效的程序。
希望通过本文的介绍,大家对 Linux 下的动态内存分配与管理有了更深入的理解,能够在实际开发中更好地运用这些知识。
超级会员免费看
1521

被折叠的 条评论
为什么被折叠?



