C语言中的内存管理详情
作者: 编程学习网
内容提要:
大家写C程序时,手工申请过内存吗?每次需要存储空间时都向操作系统申请吗?使用完申请到的内存后有把它还给操作系统吗?遇到过“段错误”吗?本文的主题和这一串问题有很大的关系。
1.malloc
手工申请内存使用malloc。先看一段例程。
#include <stdio.h> #include <string.h> #include <stdlib.h> char *say_hi(); int main(int argc, char *argv[]) { char *str = say_hi(); printf("str = %s\n", str); free(str); return 0; } char *say_hi() { char *ptr = (char *)malloc(100); char *str = "how are you?"; strcpy(ptr, str); return ptr; }
执行结果如下:
[root@localhost compiler-basic]# gcc -o t t.c -g -m32
[root@localhost compiler-basic]# ./t
str = how are you?
把修改成:
char *say_hi() { char *ptr = (char *)malloc(100); char *str = "how are you?"; strcpy(ptr, str); return str; }
执行结果如下:
[root@localhost compiler-basic]# gcc -o t t.c -g -m32
[root@localhost compiler-basic]# ./t
str = how are you?
Segmentation fault (core dumped)
在第二版say_hi中,返回值是str,我们看到,打印出的数据是how are you?。然而,这只是偶尔正确。C语言的函数并不能总是正确返回非数字类型的局部变量的值。在程序变得复杂后,保不准某个时候这样的函数就不能返回预期数据。
Segmentation fault (core dumped),这又是怎么回事呢?str不是malloc申请到的内存空间,用free释放它导致错误。
2.内存泄露
用malloc申请了内存空间却不用free释放,会造成内存泄露。
在前面的第二版say_hi中,ptr指向的内存空间就被泄露了。在程序员看来,执行完say_hi后,ptr指向的内存就没有价值了;由于没有正确地释放它,操作系统认为它仍然在使用中,当其他进程申请内存时,不会把这片内存回收重新分配。
像这样的内存泄露越来越多,会一直多到耗尽所有的内存。但实际上,那些被泄露的内存是完全应该被回收再使用的。
内存泄露后,会成为操作系统和程序员都无法掌控的内存。
我们在手工申请内存、使用完毕之后,一定要释放内存。malloc和free犹如一对连体婴儿,总是一起使用。
3.内存池
前面的例子,在say_hi使用malloc,在main使用free。连体婴儿却只能出现在两个函数中,这很危险。一不留神,就会忘记释放内存。
每次申请内存都使用malloc,需要陷入内核,性能开销很大。
有朋友会说,我只是个小菜鸟,暂时还不需要考虑性能开销,只要我写的程序能跑就行。
哈哈,你并不是第一个这么想的人,我也是这样想的,所以我不厌其烦地、勤快地多次使用malloc。终于,在昨天,我遇到了非常烦人的段错误。
段错误发生在malloc中,导致错误的函数调用链不同,在测试数据中随便加几个字符后错误又消失。断点调试不管用,在前几十次执行malloc没有段错误,在后面几十次中的某一次执行malloc时才出现段错误。段错误出现在内核中。内核是不会有问题的,即使问题指向内核,那一定是向内核提供了错误的输入数据。
在束手无策快要绝望之前,我把原始的malloc换成了向内存池申请内存。先前发生的奇怪错误,再也没有出现了。
4.理论
内存池,是使用malloc申请的一段内存;进程需要内存空间时,从这段内存中拿一块去用;当这段内存被用完后,再使用malloc申请一段新内存;像这样重复这个过程。
很容易发现,内存池减少了使用malloc的次数;在进程结束前,程序员能方便地一次性释放这些内存。
5.代码数据结构
struct mblock{ char *begin; char *avail; char *end; }; typedef struct heap{ struct mblock *last; struct mblock head; } *Heap; #define HEAP(hp) struct heap hp = { &hp.head } Heap CurrentHeap; struct heap ProgrameHeap; int HeapAlloc(Heap hp, int size); void *do_malloc(int size);
6.代码
char *HeapAlloc(Heap hp, int size){ struct mblock *blk = NULL; blk = hp->last; while(size > blk->end - blk->avail){ int m = 4096 + sizeof(struct mblock) + size; blk->next = malloc(m); blk = blk->next; if(blk == NULL){ printf("内存耗尽\n"); exit(-1); } blk->begin = blk->avail = (char *)(blk + 1); blk->end = (char *)blk + m; hp->last = blk; } blk->avail += size; return blk->avail - size; } void *do_malloc(int size){ CurrentHeap = HEAP(ProgrameHeap); void *p = HeapAlloc(CurrentHeap, size); memset(p, 0, size); return 0; }
要申请内存的时候,原来是使用malloc,现在,我们有了上面的这套内存管理机制后,就使用do_malloc来申请内存。
**解说
HEAP**
这个宏把heap的第一个成员last的值设置成第二个成员head的内存地址。
要熟悉这种{ &hp.head }初始化结构体的语法。
7.blk->begin
blk->begin = blk->avail = (char *)(blk + 1);
先看blk + 1。它表示,在blk的基础上,往后移动sizeof(struct mblock)个字节。指针的加减就是这么计算的。
blk指向一段m个字节的内存空间,这段内存空间的前sizeof(struct mblock)个字节存储一个mblock结构。怎么理解这个结构?它是这段内存空间的元数据。了解文件系统的实现机制的朋友会很容易理解这一点。
从元数据中,获取begin、avail、end。
如果把blk->begin做如下修改。
blk->begin = blk->avail = (char *)(blk);
怎么样?我们来推演一番。
- 第一次执行HeapAlloc,申请了一段内存,这段内存的前面是元数据;返回给进程的是这段内存的开始地址,也就是元数据的开始地址。
- 执行memset(p, 0, size);,元数据被擦除。
- 再次执行HeapAlloc,元数据end、avail不再是前一次执行HeapAlloc后设置的值,无法知晓内存池是否还有内存可以分配;已经乱套了。
blk->end最后一个问题,理解下面的代码。
blk->end = (char *)blk + m;
blk->end表示新申请的这片内存的末尾地址。
末尾地址等于初始地址加上这片内存的长度。看看上面的代码是不是这个意思。
(char
)blk + m就是这个意思。而(char
)(blk + m)就不是这个意思。
blk是这片内存的第一个字节,(char *)blk + m-1是这片内存的最后一个字节。
8.总结
每个内存池的开头都有一个mblock,存储这个内存池的元数据(begin、avail、end、next)。进程需要内存时,先向内存池申请。当前内存池容量不够时,再向系统申请一个内存池。把这个内存池连接到前一个内存池的元数据的next上。
一个内存池耗尽后,并非全部空间都被使用了。没有被利用的空间,在当前机制下,被浪费了。以后再找机会优化。
所有的内存池构成一个单链表。当进程完成它的功能后,在结束前,遍历这个单链表,从元数据中获取begin,然后调用free(begin)就能释放所有的内存。
到此这篇关于C语言中的内存管理详情的文章就介绍到这了,更多相关C内存管理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!