阅读:0

内存分配函数及使用注意事项,C语言内存分配函数完全攻略

C 语言主要提供 malloc、realloc、calloc、alloca 与 aligned_alloc 等内存分配函数来实现对内存的分配功能。

1) malloc 函数原型如下:

void * malloc (size_t size);

该函数用于从堆中分配内存空间,内存分配大小为 size。如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL。

2) calloc 函数原型如下:

void * calloc (size_t num, size_t size );

该函数用于从堆中分配 num 个相邻的内存单元,每个内存单元的大小为 size。如果内存分配成功则返回第一个内存单元的首地址;否则内存分配失败,则返回 NULL。

从功能上看,calloc 函数与语句“malloc(num*size)”的效果极其相似。但不同的是,在使用 calloc 函数分配内存时,会将内存内容初始化为 0。

3) realloc 函数原型如下:

void * realloc (void * ptr, size_t size );

该函数用于更改已经配置的内存空间,它同样是从堆中分配内存的。当程序需要扩大一块内存空间时,realloc 函数试图直接从堆上当前内存段后面的字节中获得更多的内存空间,即它将首先判断当前的指针是否有足够的连续存储空间,如果有,则扩大 ptr 指向的地址,并且将 ptr 返回(返回原指针);如果当前内存段后面的空闲字节不够,那么将先按照 size 指定的大小分配空间(使用堆上第一个能够满足这一要求的内存块),并将原有数据从头到尾拷贝到新分配的内存区域,然后释放原来 ptr 所指内存区域,同时返回新分配的内存区域的首地址,即重新分配存储器块的地址。

需要注意的是,参数 ptr 为指向先前由 malloc、calloc 与 realloc 函数所返回的内存指针,而参数 size 为新分配的内存大小,其值可比原内存大或小。其中:
  • 如果 size 值比原分配的内存空间小,内存内容不会改变(即新内存保持原内存的内容),且返回的指针为原来内存的首地址(即 ptr)。
  • 如果 size 值比原分配的内存空间大,则 realloc 不一定会返回原来的指针,原内存的内容保持不变,但新多出的内存则设为初始值。
最后,如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL。

4) alloca 函数原型如下:

void * alloca (size_t size);

相对与 malloc、calloc 与 realloc 函数,函数 alloca 是从栈中分配内存空间,内存分配大小为 size。如果内存分配成功,则返回首地址;如果内存分配失败,则返回 NULL。也正因为函数 alloca 是从栈中分配内存空间,因此它会自动释放内存空间,而无需手动释放。

5) aligned_alloc 函数原型如下:

void * aligned_alloc (size_t alignment,size_t size);

该函数属于 C11 标准提供的新函数,用于边界对齐的动态内存分配。该函数按照参数 alignment 规定的对齐方式为对象进行动态存储分配 size 个 size_t 类型的存储单元。如果内存分配成功,则返回首地址;否则内存分配失败,则返回 NULL。

相对于 malloc 函数,aligned_alloc 函数保证了返回的地址是能对齐的,同时也要求 size 参数是 alignment 参数的整数倍。从表面上看,函数 calloc 相对 malloc 更接近 aligned_alloc,但 calloc 函数比 aligned_alloc 函数多了一个动作,那就是会将内存内容初始化为 0。

对内存分配函数的返回值必须进行检查

在 C 语言中,常见的内存分配函数的返回值情况如表 1 所示。

表 1 内存分配函数的返回值
函数名 成功返回 失败返回 errno
malloc 指向被分配内存的指针 NULL ENOMEM
aligned一alloc 指向被分配内存的指针 NULL ENOMEM
calloc 指向被分配内存的指针 NULL ENOMEM
realloc 指向重新分配内存的指针 NULL ENOMEM

在调用表 1 中的这些内存分配函数时,必须进行返回值检查,以便能够及时得到内存分配是否成功与失败(如果分配失败则返回 NULL 指针),这样也可以避免因为内存分配错误而导致的不可预知和意外程序行为发生,如下面的示例代码所示:
char *p = (char *)malloc(100);
if (p == NULL)
{
    /*处理内存分配错误,并返回错误状态*/
    return -1;
}
除通过使用“if(p==NULL)”或者“if(p!=NULL)”语句进行简单防错处理之外,如果指针 p 是函数的参数,那么还可以在函数的入口处用 assert(p !=NULL) 进行检查,从而避免发生内存分配未成功却使用了它的情况。

实际上,在使用 malloc 等分配内存的函数时,一定要检查其返回值是否为“空指针”,并以此作为检查分配内存操作是否成功的依据,这种 Test-for-NULL 代码形式是一种良好的编程习惯,也是编写可靠程序所必需的。

内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行

在 C 语言中,如果内存的分配和释放在不同的模块或抽象层内,不仅会加大程序员追踪内存块生命周期的负担,而且可能会导致内存泄漏、内存双重释放(double-free)、非法访问已经释放的内存、写入已释放或未分配的内存区域等问题。

看下面一段示例代码:
#define MIN_MEM_SIZE 10
int CompareMemorySize(char *p, size_t size)
{
    if (size < MIN_MEM_SIZE)
    {
        free(p);
        p = NULL;
        return -1;
    }
    return 0;
}
void AllocMemory(size_t size)
{
    char *p = (char *)malloc(size);
    if (p == NULL)
    {
        /*...*/
    }
    if (CompareMemorySize(p, size) == -1)
    {
        free(p);
        p = NULL;
        return;
    }
    /*...*/
    free(p);
    p = NULL;
}
在上面的示例代码中,p 的内存是在 AllocMemory 函数中进行分配的,然后再将它通过语句“CompareMemorySize(p,size)”传给 CompareMemorySize 函数。在 CompareMemorySize 函数中,首先通过语句“if(size<MIN_MEM_SIZE)”检查 p 所分配的内存长度,如果内存长度小于最小值(MIN_MEM_SIZE),则释放 p。

然后,再将 CompareMemorySize 函数的返回值“-1”返回给调用者 AllocMemory 函数。在 AllocMemory 函数中执行语句“if(CompareMemorySize(p,size)==-1)”条件成立,再次释放 p。

很显然,这样不仅违背了“内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行”的原则,同时导致了内存的双重释放。因此,需要对代码做如下修改:
#define MIN_MEM_SIZE 10
int CompareMemorySize(size_t size)
{
    if (size < MIN_MEM_SIZE)
    {
        return -1;
    }
    return 0;
}
void AllocMemory(size_t size)
{
    char *p = (char *)malloc(size);
    if (p == NULL)
    {    
        /*...*/
    }
    if (CompareMemorySize(size) == -1)
    {
        free(p);
        p = NULL;
        return;
    }
    /*...*/
    free(p);
    p = NULL;
}
现在,函数 CompareMemorySize 与 AllocMemory 的职责很清楚了。其中,Compare-MemorySize 函数只负责检查内存分配的长度,而内存的分配与释放都放在 AllocMemory 函数内进行。这样不仅不会导致内存的双重释放,而且完全遵从“内存资源的分配与释放应该限定在同一模块或者同一抽象层内进行”原则。

必须对内存分配函数的返回指针进行强制类型转换

在 C 语言中,“void”被称为“无类型”,而“void*”则被称为“无类型指针”。之所以称“void*”为“无类型指针”,是因为它可以指向任何数据类型。因此,对于任何类型“T*”都可以转换为“void*”,而“void*”也可以转换为任何类型“T*”。

也正是因为“void”的这个特征,它常被用在如下两个方面:
  • 对函数返回的限定,即如果函数没有返回值,那么应将其声明为 void 类型。
  • 对函数参数的限定,即如果函数无参数,那么声明函数参数为 void。
当然,内存管理函数也不例外,如 malloc、realloc、calloc、alloca 与 aligned_alloc 函数的返回都是“void*”类型。但需要特别注意的是,在使用这些内存管理函数进行内存分配时,必须将返回类型“void*”强制转换为指向被分配类型的指针。如下面的代码所示:
char *p = (char *)malloc(10 * sizeof(char));
当然,为了能够简单调用,也可以将 malloc 函数使用 define 定义成如下形式:
#define MALLOC(type) ((type *)malloc(sizeof(type)))
/*或者*/
#define MALLOC(number,type) ((type *)malloc((number) * sizeof(type)))
现在,调用就简单多了,如下面的代码所示:
char *p = MALLOC(char);
/*或者*/
char *p = MALLOC(10, char);
下面的宏为大家提供了更多方便:

/*malloc*/
#define MALLOC_ARRAY(number, type) \((type *)malloc((number)* sizeof(type)))
#define MALLOC_FLEX(stype, number, etype) \((stype *)malloc(sizeof(stype) \
+ (number)* sizeof(etype)))
/*calloc*/
#define CALLOC(number, type) \((type *)calloc(number, sizeof(type)))
/*realloc*/
#define REALLOC_ARRAY(pointer, number, type) \((type *)realloc(pointer, (number)* sizeof(type)))
#define REALLOC_FLEX(pointer, stype, number, etype) \((stype *)realloc(pointer, sizeof(stype) \
+ (number)* sizeof(etype)))

确保指针指向一块合法的内存

在 C 语言中,只要是指针变量,那么在使用它之前必须确保该指针变量的值是一个有效的值,它能够指向一块合法的内存,并从根本上避免未分配内存或者内存分配不足的情况发生。

看下面一段示例代码:
struct phonelist
{
    int number;
    char *name;
    char *tel;
}list,*plist;
int main(void)
{
    list.number = 1;
    strcpy(list.name, "Abby");
    strcpy(list.tel, "13511111111");
    /*...*/
    return 0;
}
对于上面的代码片段,在定义结构体变量 list 时,并未给结构体 phonelist 内部的指针变量成员“char*name”与“char*tel”分配内存。这时候的指针变量成员“char*name”与“char*tel”并没有指向一个合法的地址,从而导致其内部存储的将是一些未知的乱码。

因此,在调用 strcpy 函数时,如“strcpy(list.name,"Abby")”语句会将字符串"Abby"向未知的乱码所指的内存上拷贝,而这块内存 name 指针根本就无权访问,从而导致程序出错。

既然没有给指针变量成员“char*name”与“char*tel”分配内存,那么解决的办法就是为指针变量成员分配内存,使其指向一个合法的地址,如下面的示例代码所示:
list.name = (char*)malloc(20*sizeof(char));
strcpy(list.name, "Abby");
list.tel = (char*)malloc(20*sizeof(char));
strcpy(list.tel, "13511111111");
除此之外,下面的错误也是大家经常容易忽视的:
struct phonelist
{
    int number;
    char *name;
    char *tel;
}list, *plist;
int main(void)
{
    plist = (struct phonelist*)malloc(sizeof(struct phonelist));
    if (plist != NULL)
    {
        plist->number = 1;
        strcpy(plist->name, "Abby");
        strcpy(plist->tel, "13511111111");
            /*...*/
    }
    /*...*/
    free(plist);
    plist = NULL;
    return 0;
}
不难发现,上面的代码片段虽然为结构体指针变量 plist 分配了内存,但是仍旧没有给结构体指针变量成员“char*name”与“char*tel”分配内存,从而导致结构体指针变量成员“char*name”与“char*tel”并没有指向一个合法的地址。因此,应该做如下修改:
plist->name = (char*)malloc(20*sizeof(char));
strcpy(plist->name, "Abby");
plist->tel = (char*)malloc(20*sizeof(char));
strcpy(plist->tel, "13511111111");
由此可见,对结构体来说,仅仅是为结构体指针变量分配内存还是不够的,还必须为结构体成员中的所有指针变量分配足够的内存。

确保为对象分配足够的内存空间

对于上面的结构体指针变量 plist 的内存分配语句:
plist = (struct phonelist*)malloc(sizeof(struct phonelist));
如果不小心误写成如下形式会怎么样呢?
plist = (struct phonelist*)malloc(sizeof(struct phonelist*));
虽然这里只是简单地将“sizeof(struct phonelist)”误写成了“sizeof(struct phonelist*)”,但将会因为结构体指针变量 plist 内存分配不足而导致程序的内存错误发生。类似的示例还有许多,如下面的代码所示:
void f(size_t len)
{
    long *p;
    if (len == 0 || len > SIZE_MAX / sizeof(long))
    {
        /*溢出处理*/
    }
    p = (long *)malloc(len * sizeof(int));
    if (p == NULL)
    {
        /*...*/
    }
    /*...*/
    free(p);
    p = NULL;
}
在上面的示例代码中,内存分配语句“p=(long*)malloc(len*sizeof(int))”使用了“sizeof(int)”来计算内存的大小,而不是 sizeof(long),这显然是不对的,应该修改成 sizeof(long),当然,也可以用“sizeof(*p)”。

除此之外,对于数组对象尤其要注意内存分配的问题,如下面的代码所示:
#define ARRAY_SIZE 10
struct datalist
{
    size_t number;
    int data[];
};
int main(void)
{
    struct datalist list;
    list.number = ARRAY_SIZE;
    for (size_t i = 0; i < ARRAY_SIZE; ++i)
    {
        list.data[i] = 0;
    }
    /*...*/
    return 0;
}
对于上面的示例,当一个结构体中包含数组成员时,其数组成员的大小必须添加到结构体的大小中。因此,上面示例的正确内存分配方法应该按照如下方式进行:
#define ARRAY_SIZE 10
struct datalist
{
    size_t number;
    int data[];
};
int main(void)
{
    struct datalist *plist;
    plist = (struct datalist *)malloc(sizeof(struct datalist)+sizeof(int) * ARRAY_SIZE);
    if (plist == NULL){
        /*...*/
    }
    plist->number = ARRAY_SIZE;
    for (size_t i = 0; i < ARRAY_SIZE; ++i)
    {
            plist->data[i] = 0;
    }
    /*...*/
    return 0;
}
由上面的几个示例代码片段可见,对于 malloc、calloc、realloc 与 aligned_alloc 内存分配函数中长度参数的大小,必须保证有足够的范围来表示对象要存储的大小。如果长度参数不正确或者可能被攻击者所操纵,将可能会出现缓冲区溢出。与此同时,不正确的长度参数、不充分的范围检查、整数溢出或截断都会导致分配长度不足的缓冲区。因此,一定要确保内存分配函数的长度参数能够合法地分配足够数量的内存。

禁止执行零长度的内存分配

根据 C99 规定,如果在程序中试图调用 malloc、calloc 与 realloc 等系列内存分配函数分配长度为 0 的内存,那么其行为将是由具体编译器所定义的(如可能返回一个 null 指针,又或者是长度为非零的值等),从而导致产生不可预料的结果。

因此,为了保证不会将 0 作为长度参数值传给 malloc、calloc 与 realloc 等系列内存分配函数,应该对这些内存分配函数的长度参数进行合法性检查,以保证它的合法取值范围。

如下面的代码所示:
size_t len;
/*初始化len变量*/
if (len == 0)
{
    /* 处理长度为0的错误 */
}
int *p = (int *)malloc(len);
if (p == NULL)
{
    /*...*/
}
/*...*/

避免大型的堆栈分配

C99 标准引入了对变长数组的支持,如果变长数组的长度传入未进行任何检查和处理,那么将很容易被攻击者用来实施攻击,如常见的 DOS 攻击。

看下面的示例代码:
int CopyFile(FILE *src, FILE *dst, size_t bufsize)
{
    char buf[bufsize];
    while (fgets(buf, bufsize, src))
    {
            if (fputs(buf, dst) == EOF)
            {
                    /*...*/
            }
    }
    /*...*/
    return 0;
}
在上面的示例代码中,数组“char buf[bufsize]”的长度将根据 CopyFile 函数的 bufsize 参数来决定,这显然不符合要求的。对于这种情况,可以通过一个 malloc 调用来替换掉这个变长数组。与此同时,如果 malloc 函数内存分配失败,还可以对返回值进行检查,从而防止程序异常终止等情况发生。如下面的示例代码所示:
int CopyFile(FILE *src, FILE *dst, size_t bufsize)
{
    if (bufsize == 0)
    {
        /*...*/
    }
    char *buf = (char *)malloc(bufsize);
    if (buf == NULL)
    {
        /*...*/
    }
    while (fgets(buf, bufsize, src))
    {
        if (fputs(buf, dst) == EOF)
        {
            /*...*/
        }
    }
    /* ... */
    free(buf);
    buf = NULL;
    return 0;
}

避免内存分配成功,但并未初始化

在通常情况下,导致这种错误的主要原因有两个:
  • 没有初始化的观念。
  • 误以为内存的默认初值全部为零,从而导致引用初值错误(如数组)。

其实,内存的默认初值究竟是什么并没有统一的标准。如 malloc 函数分配得到的内存空间就是未初始化的,而它所分配的内存空间里可能包含出乎意料的值。因此,一般在使用该内存空间时,就需要调用函数 memset 来将其初始化为全 0。如下面的示例代码所示:
int * p = NULL;
p = (int*)malloc(sizeof(int));
if (p == NULL)
{
    /*...*/
}
/*初始化为0*/
memset(p, 0, sizeof(int));
对于 realloc 函数,同样需要使用 memset 函数对其内存进行初始化。而对于数组,也别忘了赋初值,即便是赋零值也不可省略,千万不要嫌麻烦。