感觉这章主要就是在将如何对标准C库中malloc,calloc,realloc,和free函数进行封装,重新组织成Mem接口,还有就是对它们运行已检查的错误的跟踪和报告。
所有非平凡的C程序都会在运行时分配内存。标准C库提供了4个内存管理例程:malloc,calloc,realloc,和free。Mem接口将这些例程重新包装为一组宏和例程,使之不那么容易出错,并提供了一些额外的功能。
1、如
p=malloc(nbytes);
...
free(p);
调用malloc分配nbytes长的内存块,将该内存块第一个字节的地址赋值给p,使用p和它指向的内存块,最终释放该内存块。在调用free之后,p包含一个悬挂指针---指向逻辑上不存在的内存的指针。接下来反引用p是一个错误,但是如果该内存块没有因其他原因而再次分配出去,这个错误可能不会被检测到。这种行为时使得此类内存访问错误难于诊断的原因:在检测到错误时,错误暴露的时间和位置可能与错误的来源距离颇远。
2、Mem接口中的宏和例程针对上述各种内存管理错误提供了一些保护。但是它们不能消除所有这些错误。例如,它们无法防止反引用已破坏的指针或者使用指针指向超出作用域的局部变量。C语言初学者经常犯后一个错误,下面给出一个例子:
char *itoa(int n)
{
char buf[43];
sprintf(buf,"%d",n);
return buf;
} /*itoa返回其局部数组buf的地址,但在itoa返回后,buf就不再存在了。*/
3、Mem接口提供了宏,将内存分配和赋值封装起来:
#define NEW(p) ((p)=ALLOC((long)sizeof *(p)))
#define NEW0(p) ((p)=CALLOC(1,(long)sizeof *(p)))
NEW(p)分配了一个未初始化的内存的内存块以容纳*p,并p设置为该块的地址。NEW0(p)完成的工作类似,还将内存块清零。提供NEW时有下述假设:大多数客户程序在分配内存块后会立即初始化。传递给编译时运算符sizeof的参数只是用于获取其类型,运行时不会计算其值。因此NEW和NEW0只会计算p一次,使用具有副效应的表达式作为这两个宏的实参是安全的,例如NEW(a[i++])。
4、例程将对标准库中内存管理函数的调用封装到通过Mem接口规定的更安全的软件包中。(malloc,calloc,realloc,和free)
#include<stdlib.h>
#include<stddef.h>
#include"assert.h"
#include"except.h"
#include"mem.h"
const Except_T Mem_Failed={" Allocation Failed"};
①Mem_alloc调用malloc,并在malloc返回NULL指针时引发Mem_Failed异常:
void *Mem_alloc(long nbytes, const char *file,int line)
{
void *ptr;
assert(nbytes>0);
ptr=malloc(nbytes);
if(ptr==NULL)
{
if(file==NULl)
{
RAISE(Mem_Failed);
}
else
{
Except_raise(&Mem_Failed,,file,line);
}
}
}
②在count或nbytes为零时,calloc的行为是由具体实现定义的。
void *Mem_calloc(long count, long nbytes, const char *file, int line)
{
void *ptr;
assert(count>0);
assert(nbytes>0);
ptr=calloc(count, nbytes);
if(ptr== NULL)
{
if(file==NULl)
{
RAISE(Mem_Failed);
}
else
{
Except_raise(&Mem_Failed,,file,line);
}
}
return ptr;
}
③Mem_resize的规格说明比realloc简单得多。Mem_resize唯一的目的就是改变某个现存内存块的长度。realloc也完成了同样的工作,但是它在nbytes为零时会释放内存块,而在ptr是NULL指针时会分配内存块。这些额外的功能与修改现存内存块的长度只有松散的关联,很容易引入bug。
void *Mem_resize(void *ptr, long nbytes, const char *file, int line)
{
assert(ptr);
assert(nbytes>0);
ptr=realloc(ptr,nbytes);
if(ptr==NULL)
{
if(file==NULl)
{
RAISE(Mem_Failed);
}
else
{
Except_raise(&Mem_Failed,,file,line);
}
}
return ptr;
}
④
void Mem_free(vid *ptr, const char *file, int line)
{
if(ptr)
free(ptr);
}
5、分配函数从来不返回同一地址两次的条件,可以通过从不释放由分配函数返回的内存块。通过维护一个保存这种内存块地址的表,即可实现集合S。
这种方案的内存分配器,可以基于标准库函数实现。该分配器维护了块描述符的一个哈希表。
static struct descriptor
{
struct descriptor *free;
struct descriptor *link;
const void *ptr;
long size;
const char *file;
int line;
} *htab[2048];
ptr是块的地址,在代码中其他地方分配,size是块的长度。file和line是该块的分配“坐标”,即客户程序中调用相关分配函数的源代码所处的位置(也会作为参数传递给分配函数)。这些值并不使用,但会保存在描述符中,以便调试器在调试会话期间输出相关信息。
Mem_alloc使用最先适配算法分配内存,这是诸多内存分配算法之一。它会搜索freelist来查找第一个能够满足请求的足够大的空闲块,并划分该块来满足请求。如果freelist不包含适当的块,Mem_alloc调用malloc分配比nbytes大的一个内存块,将该块添加到空闲链表,然后再次尝试。因为新的内存块不nbytes大,这一次将使用该块来满足请求。