标签:
malloc()是C语言中动态存储管理 的一组标准库函数之一。其作用是在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值 是一个指向所分配的连续存储域的起始地址的指针。
动态内存分配 就 是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据 程序的需要即时分配,且分配的大小就是程序要求的大小。本文简单介绍动态内存分配函数malloc()及几种实现方法。
1. 简介
malloc()是C语言中动态存储管理的一组标准库函数之一。其作用是在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数,返回值是一个指向所分配的连续存储域的起始地址的指针。还有一点必须注意的是,当函数未能成功分配存储空间(如内存不足 )就会返回一个NULL指针。所以在调用该函数时应该检测返回值是否为NULL并执行相应的操作。
2. 函数说明
C语言的动态存储管理由一组标准库函数实现,其原型在标准文件<stdlib.h>里描述,需要用这些功能时应包含这个文件。与动态存储分 配有关的函数共有四个,其中就包括存储分配函数malloc()。函数原型是:void *malloc (size_t n);这里的size_t是标准库里定义的一个类型,它是一个无符号整型。这个整型能够满足所有对存储块大小描述的需要,具体相当于哪个整型由具体的C系 统确定。malloc的返回值为(void *)类型(这是通用指针的一个重要用途),它分配一片能存放大小为n的数据的存储块,返回对应的指针值;如果不能满足申请(找不到能满足要求的存储块)就
返回NULL。在使用时,应该把malloc的返回值转换到特定指针类型,赋给一个指针。
注意,虽然这里的存储块是通过动态分配得到的,但 是它的大小也是确定的,同样不允许越界使用。例如上面程序段分配的块里能存n个双精度数据,随后的使用就必须在这个范围内进行。越界使用动态分配的存储 块,尤其是越界赋值,可能引起非常严重的后果,通常会破坏程序的运行系统,可能造成本程序或者整个计算机系统 垮台。
下例是一个动态分配的例子:
#include <stdlib.h>
main()
{
int count,*array; /*count是一个计数器,array是一个整型指针,也可以理解为指向一个整型数组的首地址*/
if((array(int *) malloc (10*sizeof (int)))==NULL)
{
printf("不能成功分配存储空间。");
exit(1);
}
for (count=0;count〈10;count++) /*给数组赋值*/
array[count]=count;
for(count=0;count〈10;count++) /*打印数组元素*/
printf("%2d",array[count]);
}
上例中动态分配了10个整型存储区域,然后进行赋值并打印。例中if((array(int *) malloc (10*sizeof(int)))==NULL)语句可以分为以下几步:
1)分配10个整型的连续存储空间,并返回一个指向其起始地址的整型指针
2)把此整型指针地址赋给array
3)检测返回值是否为NULL
3. malloc()工作机制
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足 用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传 给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片 段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检
查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
4. malloc()在操作系统中的实现
在 C 程序中,多次使用malloc () 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。
在大部分操作系统中,内存分配由以下两个简单的函数来处理:
void *malloc (long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。
malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址 ,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量 :
清单 1. 我们的简单分配程序的全局变量
int has_initialized = 0;
void *managed_memory_start;
void *last_valid_address;
如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX ? 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。
sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:
清单 2. 分配程序初始化函数
/* Include the sbrk function */
#include
void malloc_init()
{
/* grab the last valid address from the OS */
last_valid_address = sbrk(0);
/* we don‘t have any memory to manage yet, so
*just set the beginning to be last_valid_address
*/
managed_memory_start = last_valid_address;
/* Okay, we‘re initialized and ready to go */
has_initialized = 1;
}
现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:
清单 3. 内存控制块结构定义
struct mem_control_block {
int is_available;
int size;
};
现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的 内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。
在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:
清单 4. 解除分配函数
void free(void *firstbyte) {
struct mem_control_block *mcb;
/* Backup from the given pointer to find the
* mem_control_block
*/
mcb = firstbyte - sizeof(struct mem_control_block);
/* Mark the block as being available */
mcb->is_available = 1;
/* That‘s It! We‘re done. */
return;
}
如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:
清单 5. 主分配程序的伪代码
1. If our allocator has not been initialized, initialize it.
2. Add sizeof(struct mem_control_block) to the size requested.
3. start at managed_memory_start.
4. Are we at last_valid address?
5. If we are :
A. We didn‘t find any existing space that was large enough
-- ask the operating system for more and return that.
6. Otherwise:
A. Is the current space available (check is_available from
the mem_control_block)?
B. If it is:
i) Is it large enough (check "size" from the
mem_control_block)?
ii) If so:
a. Mark it as unavailable
b. Move past mem_control_block and return the
pointer
iii) Otherwise:
a. Move forward "size" bytes
b. Go back go step 4
C. Otherwise:
i) Move forward "size" bytes
ii) Go back to step 4
我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:
清单 6. 主分配程序
void *malloc(long numbytes) {
/* Holds where we are looking in memory */
void *current_location;
/* This is the same as current_location, but cast to a
* memory_control_block
*/
struct mem_control_block *current_location_mcb;
/* This is the memory location we will return. It will
* be set to 0 until we find something suitable
*/
void *memory_location;
/* Initialize if we haven‘t already done so */
if(! has_initialized) {
malloc_init();
}
/* The memory we search for has to include the memory
* control block, but the users of malloc don‘t need
* to know this, so we‘ll just add it in for them.
*/
numbytes = numbytes + sizeof(struct mem_control_block);
/* Set memory_location to 0 until we find a suitable
* location
*/
memory_location = 0;
/* Begin searching at the start of managed memory */
current_location = managed_memory_start;
/* Keep going until we have searched all allocated space */
while(current_location != last_valid_address)
{
/* current_location and current_location_mcb point
* to the same address. However, current_location_mcb
* is of the correct type, so we can use it as a struct.
* current_location is a void pointer so we can use it
* to calculate addresses.
*/
current_location_mcb =
(struct mem_control_block *)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size >= numbytes)
{
/* Woohoo! We‘ve found an open,
* appropriately-size location.
*/
/* It is no longer available */
current_location_mcb->is_available = 0;
/* We own it */
memory_location = current_location;
/* Leave the loop */
break;
}
}
/* If we made it here, it‘s because the Current memory
* block not suitable; move to the next one
*/
current_location = current_location +
current_location_mcb->size;
}
/* If we still don‘t have a valid location, we‘ll
* have to ask the operating system for more memory
*/
if(! memory_location)
{
/* Move the program break numbytes further */
sbrk(numbytes);
/* The new memory will be where the last valid
* address left off
*/
memory_location = last_valid_address;
/* We‘ll move the last valid address forward
* numbytes
*/
last_valid_address = last_valid_address + numbytes;
/* We need to initialize the mem_control_block */
current_location_mcb = memory_location;
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
/* Now, no matter what (well, except for error conditions),
* memory_location has the address of the memory, including
* the mem_control_block
*/
/* Move the pointer past the mem_control_block */
memory_location = memory_location + sizeof(struct mem_control_block);
/* Return the pointer */
return memory_location;
}
这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。
5. malloc()的其他实现
malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷 的选择,其中包括:
分配的速度。
回收的速度。
有线程的环境的行为。
内存将要被用光时的行为。
局部缓存。
簿记(Bookkeeping)内存开销。
虚拟内存环境中的行为。
小的或者大的对象。
实时保证。
每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。
还有其他许多分配程序可以使用。其中包括:
Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。
Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存, 以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料 部分中,有一篇描述
Doug Lea 的 Malloc 实现的文章。
BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的
size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。
Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。
众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。
附::
C++中内存的动态分配与管理永远是一个让C++开发者头痛的问题,本文通过对C++中内存的动态分配释放的基本原理的介绍,让读者朋友能对C++中的内存的动态分配与释放有较为深入的理解,从而更好驾驭C++程序。
1. 函数(Function)
(1) operator new function
1 2 |
上面是C++中operator new function的原型,一个是全局类型的,一个的类成员类型的。全局类型的operator new函数在下面两种情况下被调用:一种是在分配C++内建(built-in)类型的动态内存时,一种是在分配用户没有自己定义operator new成员函数的用户自定义类型的动态内存时。 如果用户在自己定义的类型中,定义了operator new函数,那么用户在用new申请该类型的动态内存时, 便会调用该类型的成员函数operator new, 而不是全局的operator new。
另外,我们注意到,上面的原型中函数的返回值为void *类型, 第一个参数为size_t类型,这个是C++编译器要求的,如果要自己重载operator new函数,返回值必须为void* 类型,第一个参数必须为size_t类型,否则,编译器会返回如下错误信息:
1 |
error: ‘operator new’ takes type ‘size_t’ (‘unsigned int’) as first parameter |
这里需要注意的一点是,我们可以利用operator new function可以重载的特点,可以通过参数传入一些额外的信息,来调试程序,检测内存泄露等。比如,我们可以像下面这样重载,传入调用处的行号,函数名,这样就可以跟踪内存的分配使用情况:
1 2 3 4 5 |
(2) operator delete function
1 2 |
上面是operator delete function的原型。operator delete function也有全局的和类成员的两种。这里需要注意,一个类只能有一个operator delete function做为其成员函数,而且必须为上面两种中的其中一种,没有其它的形式。如果一个类实现了自己的operator delete function成员函数,那么在释放该类型的内存时,编译器便会调用成员operator delete function, 而不是全局的。
上面的两种原型,第一种,在调用的时候,编译器会把要释放的内存的首地址传入,第二种,在调用的时候,编译器会把要释放的内存的首地址和大小都传 入。因此,可以利用这一特性,如果我们在基类中实现第二种形式的operator delete function的成员函数,那么便可以用之来释放子类类型的内存(具体参考最后面的例子)。
2. 运算符(Operator)
(1) new operator
1 2 |
注:上面的’[]‘表示在其中的部分是optional(可选的)
上面是new operator的原型。在C++中,动态内存的分配,通常都是调用new operator来完成的,利用new operator来分配动态内存,编译器要做下面两项工作:
下面来说一说new operator的原型中各部分到底是干什么的:
placement: 如果你重载了operator new function, placement可以用来传递额外的参数
type-name: 指定要分配的内存的类型,可以是内建(built-in)类型,也可以是用户自定义类型
new-initializer: 指定对分配后内存的初始化的参数,也就的构造函数的参数 。这里需要注意一点,在分配一个对象的数组类型的内存时,不能够指定初始化参数;换言之,要想分配一个对象的数组类型的内存,该对象必须有缺省构造函数
(2) delete opeartor
1 2 |
上面是delete operator的原型,第一种用来释放普通的对象(包括内建类型)类型的内存,第二种用来释放对象的数组类型的内存。在C++中,用new operator分配的动态内存,必须调用delete operator来释放,通常用delete operator释放内存编译器要做下面两项工作:
3. 关于new/delete使用过程中一些需要注意的点
(1)如何区别operator new/delete function 与 new/delete operator ?
通过上面的讲述,不难看出,我们分配/释放动态内存,调用的是new/delete operator, 而在调用new/delete的过程中,编译器会自动调用operator new/delete function来完成实际的内存分配/释放的工作
(2) 用delete operator去释放一块不是由new operator释放的内存,结果是不可预料的,因此,切记,operator new与operator delete一定要配对使用,这是写好程序的基础
(3) new operator调用失败会抛出std::bad_alloc异常,前提是你没有自己重载对应的operator new function;delete operator失败,常见的原因是多次delete同一块内存
(4) 如果一块内存被delete后,再对它解引用(Dereference),结果也是不可预测的,很可能导致程序崩溃
(5) delete一个空(NULL)指针是安全的,没有任何害处的
(6) 类成员类型的operator new/delete函数必须为静态(static)函数,因此它们不能为虚函数(virtual function),也遵守public, protected, private的访问权限控制
4. 关于上面所讲的内容的一些例子:
程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
wuzesheng@wuzesheng-ubuntu:~/Program$ ./a.out operator new called constructor called ####### operator new called, line: 68, func: main constructor called ####### operator new called constructor called constructor called constructor called constructor called constructor called constructor called constructor called constructor called constructor called constructor called ####### ~A() delete2: 8 ####### ~B() ~A() delete2: 16 ####### ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() ~A() delete1 ####### delete1 |
上面的程序很简单,我在这里不做过多的解释,感兴趣的朋友可以自己分析一下。
通过我上面的讲解,相信大多数朋友应该对C++中内存的动态分配与释放有了较为深入的理解。后续我还有可能写一些关于C++中内存管理的文章,只有把本文所讲的内容与后续的内存管理的一些常见的方法结合起来,我们才写出更加健壮的C++程序。欢迎读者朋友留言一起交流!
标签:
原文地址:http://blog.csdn.net/wangiijing/article/details/52290662