相比静态地分配内存空间,使用动态内存分配具有明显的优势:
1, 分配空间的大小够精确: 设想一个读取用户输入行的程序, 如果使用静态分配的数组作为buffer, 那么, 你如何确定该数组的长度呢? 太大或太小都不合适. 因为你无法事先知道用户输入字符串的长度. 而使用动态内存分配就精准多了.
2, 静态分配的空间大小无法更改, 而动态分配的内存大小是可调的.
所以, 理解C语言中的动态内存分配对于编写实用, 有效, 安全的程序来说必不可少. 本文假设你使用C语言编程, 且使用GNU/Linux系统. (其实由于现在的许多系统都是POSIX兼容的, 本文的内容使用于任何操作系统, 只是其中提到的某些工具仅存于GNU/Linux上.)
要理解内存管理, 首先要理解程序在内存中的布局, 既: 内存程序映像. 可参考本blog的
GNU/Linux平台的C程序开发及程序运行环境标准C中的内存管理函数
函数原型如下:
#include
void *malloc(size_t size); void *calloc(size_t nobj, size_t size); void *realloc(void *ptr, size_t newsize); 若返回的指针=NULL, 则失败, 否则成功.
void free(void *ptr); ISO C
|
上表列出了标准C规定的四个常用内存管理函数, 一般而言, 这4个函数已经足够我们进行内存管理了.
malloc(), calloc(), realloc()返回的指针若不为NULL, 那么该指针指向被分配数据块中的第一个元素. 并且, 保证该指针能针对各种数据类型满足对齐要求.
void *malloc(size_t size);
调用malloc()的步骤1, 针对你想要存放数据的数据结构, 声明一个指向它的指针.
2, 计算你需要分配的字节数. 通常利用sizeof(数据结构) * n, n为你要使用的数据结构的个数.
3, 调用malloc()分配内存, 并将它所返回的通用指针(void *)显式地映射为指向该数据结构的指针, 并检查malloc()是否返回了NULL, 若返回值为NULL, 则进行错误处理.
实现代码struct num { int x, y, z;};struct num *p;int n;if ((p = (struct num *)malloc(n * sizeof(struct num))) == NULL) { 错误处理;}使用p指向的内存; (记得初始化!)
标准C中规定, void *p是一个通用指针, 可以将它赋予任何类型的数据. 但在这里最好还是显式地将它的类型映射为需要分配的数据类型. why? 先看看下面替代上面使用malloc()函数的语句: if ((p = malloc(n * sizeof(*p))) != NULL) 这里将sizeof的参数换成了*P, 这样, 即便p被修改, 指向了不同的数据结构, sizeof也能计算正确的字节数. 这里省略了类型映射, 但加上它以后, 能够在p指向不同的数据结构后, 编译时给出警告信息. 总之, 我们使用这样的语句来调用malloc(): if ((p = (ds *)malloc(n * sizeof(*p))) == NULL) 其中(ds *)将通用指针显式地映射到要分配的数据类型.
另外, 传统c中使用char *作为通用指针, C++要求对malloc()的返回值进行显式的映射.
|
void *realloc(void *ptr, size_t newsize);
使用realloc()可以调整之前分配的数据块大小. 包括增加或则减小. 一般而言不用减小已分配数据块的大小.
关于realloc()的原型, 有几点值得注意:
1, newsize是调整数据块大小后最终的值, 并非差量.
2, 若ptr != NULL && nesize == 0, 则 realloc(p, 0)等价于free(p).
3, ptr指向需要调整的数据块, 若ptr == NULL, relalloc(NULL, newsize)等价于malloc(newsize).
虽然可以用realloc()实现free()和malloc()的功能, 但不推荐这样做. 还是使用标准的接口比较合适.
调用realloc()的步骤:1, 计算你新需要的字节数.
2, 找到指向你需要调整的数据块的指针(它是malloc()或calloc()甚至realloc()的返回值.), 并将它和新的字节大小传递给realloc(). 注意不要用增量!
3, 调用realloc()分配内存, 并将它所返回的通用指针(void *)显式地映射为指向该数据结构的指针, 并检查malloc()是否返回了NULL, 若返回值为NULL, 则进行错误处理.
注意: GNU Coding Standards规定: 即便realloc()失败, 之前分配的数据块也会保持不变, 可以继续使用.
|
实现代码这里继续上述malloc()中的代码, 假设之前分配的n个num结构不够, 还需要再分配m个:
struct num *q;int newsize = (n+m) * sizeof(*p);
if ((q = (struct num *)realloc(p, newsize)) == NULL) { 错误处理;}p = q;继续使用p;注意: realloc()返回的地址赋给了一个新的指针q. 在调用完realloc()之后, 又将q的值赋给p , 继续使用p. 为何如此麻烦呢? 原因有二:
(1)看看上面的框框, GNU保证即便realloc()返回NULL, 之前调用malloc()分配给p的数据块也能使用, 但若直接把realloc()的返回值赋给p, 可能令p = NULL, 使得之前p指向的数据段无法使用.
(2)使用realloc()时, 脑子里应该时刻铭记一点: 由于对之前的数据块大小进行了调整, realloc()可能将以前的数据块挪到内存中别的位置. 考虑增大数据块的情况: 若之前分配的数据块所在的内存空间所剩的空间不够, 那么realloc()会将以前的数据块拷贝到内存中其他位置, 并释放之前分配的数据块. 这样之前的p就指向了无效的区域. 即便调用realloc()来减小数据块, 该数据块也可能被移到内存中的其他位置!
某个已分配的数据块b1, 调用realloc()调整b1大小得到b2之后, 不能假设b1和b2的第一个元素在同一位置. 指向b1的所有指针必须被更新! 引用原b1数据块中的元素有两种途径: 1, 使用数组下标. 2, 使用被更新后的指针. 绝不能使用以前指向p1的指针!
|
void *calloc(size_t nobj, size_t size);
calloc()可视为malloc()的一个封装, 下面是它可能的一个实现:
void *calloc(size_t nobj, size_t size)
{
void *p;
size_t total;
total = nobj * size;
if ((p = malloc(total)) != NULL) {
memset(p, '\0', total);
return p;
}
调用calloc()的方法与malloc()相同. calloc()与malloc()的区别在于两点:
1, calloc()将分配的内存数据块中的内容初始化为0. 这里的0指的是bitwise, 既每个位被清0, 具体的数值由要联系数据结构中各元素的类型.
2, 传递给calloc()的参数有2个, 第一个是想要分配的数据结构的个数, 第二个是数据结构的大小.
如果传递给malloc()或calloc()的size = 0, 标准C并未规定返回的指针一定为NULL, 它可能为非NULL. 但是这种情况下不能引用该指针.
|
void free(void *ptr);
在完成对动态分配的数据块的使用之后, 要通过调用free()来释放它. 这里所说的"释放"是指将该数据块占用的内存放回到堆中, 以后再调用malloc(), calloc()或realloc()时可以利用该数据块占用的内存段. 注意free()并不能够改变进程地址空间的大小, 被释放的内存仍位于堆空间中.
如果不及时释放内存, 会引发内存泄露(memory leaks), 特别是运行时间比较长的程序要注意这个问题, 如果发生了内存泄露, 系统即便不因为缺少内存资源而崩溃, 也会由于内存抖动(memory thrashing)而性能下降.
调用free()的方法:
free(p);
p = NULL;
调用free()时, 有几点注意:
1, p必须指向由malloc(), calloc()或realloc()返回的地址. 即传递给free()的参数必须是数据块第一个元素的地址. 因为malloc()的实现往往在分配的数据块的首部存储一些用来管理分配的数据块的记账信息. 如果不将数据块首部地址传递给free(), free()无法知道数据块的具体信息, 也就无法释放. 把NULL传递给free()是合法的, free()不起任何作用.
NULL == ((void *)0), 在现代系统上, 地址0不在进程地址空间之内, 引用0地址会引发段错误.
|
2, 谨防"dangling pointer(野指针)", 当p指向的数据块被释放后, p就成为了一个野指针. 如果再次通过p引用数据就存在问题了. p可能指向了内存中别的位置.( 如果在p被释放之后没有调用内存分配函数, p可能还指向原来的数据块, 但该情况不确定). 所以, 在调用free(p);之后, 要紧接着将p设为NULL. 这样如果引用p, 就会马上引起段错误. 不会干扰程序的其他地方.
3, 一个数据块只能被释放一次, 如果对同一数据块释放多次, 会引发问题. (多次调用free(NULL)不存在任何问题.)
4, 被释放的内存依然位于进程地址空间, 用于以后调用malloc(), calloc(), realloc()返回的数据块.
在栈上分配内存: alloca()
前面的malloc(), calloc(), realloc()都在堆上分配内存, 需要显式地释放所分配的内存. 如果使用alloca()在栈上分配内存, 由于每次函数返回时都会释放它所在的栈空间, alloca()所分配的内存会像动态变量一样被自动释放.
alloca()的原型:
#include void *alloca(size_t size); |
不推荐使用alloca(), 因为它不属于ISO C或POSIX标准, 依赖于具体的系统和编译器, 即便在支持它的系统上, 它的实现也有bug.
brk()和sbrk()系统调用
在UNIX系统中, malloc(), calloc(), realloc(), free()这4个函数都是在brk()和sbrk()这两个系统函数基础上实现的. 在应用程序中, 极少见到这两个函数, 这里对它们做一个简单介绍, 并利用它们来查看进程地址空间信息.
brk(), sbrk()的原型:
#include int brk(void *end_data_segment); void *sbrk(intptr_t increment); |
brk()j将进程地址空间的data段尾(既内存程序映像的堆尾)设置为end_data_segment所指向的位置. 若成功, 返回0, 否则返回-1.
sbrk()使用差量来调整进程地址空间data段尾的位置, 并返回之前data段尾的地址.
下面看看这样一个程序, 它显示进程地址空间的相关信息:
1 /*
2 * Show address of code, data and stack sections,
3 * as well as BSS and dynamic memory.
4 */
5
6 #include
7 #include /* for definition of ptrdiff_t on GLIBC */
8 #include
9 #include /* for demonstration only */
10
11 extern void afunc(void); /* a function for showing stack growth */
12
13 int bss_var; /* auto init to 0, should be in BSS */
14 int data_var = 42; /* init to nonzero, should be in data */
15
16 int
17 main(int argc, char **argv)
18 {
19 char *p, *b, *nb;
20 int i;
21 printf("Text Locations:\n");
22 printf("\tAddress of main: %p\n", (void *)main);
23 printf("\tAddress of afunc: %p\n", afunc);
24
25 printf("Stack Locations:\n");
26 afunc();
27
28 p = (char *) alloca(32);
29 if (p != NULL) {
30 printf("\tStart of alloca()'ed array: %p\n", p);
31 printf("\tEnd of alloca()'ed array: %p\n", p + 31);
32 }
33
34 printf("Data Locations:\n");
35 printf("\tAddress of data_var: %p\n", & data_var);
36
37 printf("BSS Locations:\n");
38 printf("\tAddress of bss_var: %p\n", & bss_var);
39
40 nb = sbrk((ptrdiff_t) 0);
41 printf("Heap Locations:\n");
42 printf("\tInitial end of heap: %p\n", nb);
43 b = sbrk((ptrdiff_t) (32)); /* lower heap address */
44 printf("\t sbrk return : %p\n", b);
45
46
47 nb = sbrk((ptrdiff_t) 0);
48 printf("\tNew end of heap: %p\n", nb);
49
50 b = sbrk((ptrdiff_t) -16); /* shrink it */
51 nb = sbrk((ptrdiff_t) 0);
52 printf("\tFinal end of heap: %p\n", nb);
53
54 printf("Command-Line Arguments:\n");
55 for (i = 0; argv[i] != NULL; i++)
56 printf("\tAddress of arg%d(%s) is %p\n", i, argv[i], &(argv[i]));
57
58 return 0;
59 }
60
61 void
62 afunc(void)
63 {
64 static int level = 0; /* recursion level */
65 auto int stack_var; /* automatic variable, on stack */
66
67 if (++level == 3) /* avoid infinite recursion */
68 return;
69
70 printf("\tStack level %d: address of stack_var: %p\n",
71 level, & stack_var);
72 afunc(); /* recursive call */
73 }
在Linux, x86系统中, 代码段开始于 0x08048000; 栈底地址开始于0xc0000000.
|