动态内存管理——malloc、calloc、realloc、柔性数组

一、为什么存在动态内存管理

  • 堆区能够申请大块内存。
  • 数组开辟空间实在程序编译时进行的,且申请空间是固定的,而有些情况下,只有在程序运行时才能知道所需内存的大小。
  • malloc、calloc等都是函数,所以对空间的申请是在程序运行阶段进行的。

二、动态内存函数

malloc和free

malloc是C提供的动态内存开辟的函数
在这里插入图片描述
函数的参数是一个无符号整型,表示要开辟的空间的字节数;函数的返回值是void*类型,指向开辟的空间的首地址。
开辟失败时会返回NULL,所以使用malloc函数必须判断是否申请成功。
free函数用来释放动态开辟的内存
在这里插入图片描述
这里的第四点,free并没有改变指针原来的指向,空间被free以后,指针仍然指向原来的地方,只是取消了指针内部的地址和堆空间的关系。
malloc和free有几点需要注意:

  • 申请空间时必须整体申请,释放时也必须整体释放
  • 如果申请内存没有释放,会造成内存泄漏
  • 申请空间时,实际申请到的空间会大一些,大出来的部分保存本次申请的“元信息”(属性信息,包含本次申请的大小等),即内存cookie信息(所以每次释放空间能准确释放)。

calloc

calloc与malloc功能相似,区别是会在申请空间时将空间内容按字节初始化为0。
在这里插入图片描述
函数的两个参数都是无符号整型,第一个参数表示要申请的元素个数,第二个参数表示每个元素的大小,所以最终申请的空间大小是num*size个字节。
calloc的使用:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
int main()
{
    
    
    int* p = calloc(5, sizeof(int));
    if (NULL != p)//判断是否申请成功
    {
    
    
        for (int i = 0; i < 5; i++)
        {
    
    
            printf("%d ", p[i]);
        }
    }
    free(p);//释放空间
    system("pause");
    return 0;
}

realloc

realloc函数用于调整已经申请的内存空间的大小
在这里插入图片描述
第一个参数ptr是已申请的空间的指针,第二个参数size是调整之后的内存空间的大小。
有几点需要注意

  • 由于堆空间必须是连续的空间,所以如果与原有空间连续的后面没有足够大的空间,那么realloc后的返回值不再是之前的地址。
  • 使用realloc函数要用一个临时指针保存旧空间的地址,如果之间将realloc结果赋值给旧的指针,当realloc申请失败会返回NULL,导致旧的空间找不到,造成内存泄漏。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
int main()
{
    
    
    int* ptr = malloc(sizeof(int)*5);
    if(NULL==ptr)
    {
    
    
        exit(EXIT_FAILURE);
    }
    //代码1
    //ptr = realloc(ptr, 10);//将原来申请的空间扩展为10个字节

    //代码2
    int* p = NULL;//定义临时变量
    p = realloc(ptr, 10);//用临时变量保存realloc的返回值
    if (p != NULL)
    {
    
    
        ptr = p;
    }
    free(ptr);//释放空间
    system("pause");
    return 0;
}

代码1是常见的错误写法,将realloc的值直接赋给原来空间的指针。

动态内存试题

题目1

void GetMemory(char *p)
{
    
    
 p = (char *)malloc(100);
}

void Test(void)
{
    
    
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
}

这个程序本意是将str传给函数GetMemory,为其动态开辟内存,但是此处忽略了一点,str本身是一个指针变量,要在函数内操作这个指针变量的内容要给函数传入其地址,即二级指针。与一般变量相同,要在函数内改变变量的值,需要传值传参,否则改变的只是临时拷贝的变量。
正确写法:

void GetMemory(char** p)
//用二级指针接收参数
{
    
    
    *p = (char*)malloc(100);//
}
void Test(void)
{
    
    
    char* str = NULL;
    GetMemory(&str);//传str的地址,才能在GetMemory函数内改变其值
    strcpy(str, "hello world");
    puts(str);
}

除了二级指针方案外,还可以将函数返回值设为char*类型,开辟空间后将空间地址作为返回值。
总结一句话:函数传参时,要修改谁的内容,就传谁的地址
题目2

char *GetMemory(void)
{
    
    
 char p[] = "hello world";
 return p;
}
void Test(void)
{
    
    
 char *str = NULL;
 str = GetMemory();
 printf(str);
}

p是局部变量,在GetMemory函数被调用结束后,其形成的栈帧会被释放掉,而对于返回值,函数数调用方在栈上额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这个程序中,指针p被临时保存在额外的空间,但是其指向的数组内容已将不存在了。
计算机在释放空间时,不会将空间的内容清空,只是这些空间的内容已经无效,可以被重新写入,所以最终str拿到的返回值依然指向原来的位置,但是该位置已经被调用printf函数形成的栈帧所覆盖,导致最终打印结果是乱码。
题目三

void GetMemory(char **p, int num)
{
    
    
 *p = (char *)malloc(num);
}
void Test(void)
{
    
    
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
}

本题与题目一相似,这里为GetMemory传入的是str的地址,所以可以改变str指针,让其指向开辟的堆内存。这里存在的问题是,没有判断是否申请成功,且程序执行完没有释放空间。
题目四

void Test(void)
{
    
    
 char *str = (char *) malloc(100);
 strcpy(str, "hello");
 free(str);
 if(str != NULL)
 {
    
    
 strcpy(str, "world");
 printf(str);
 }
}

因为free函数的作用是解除指针与堆空间的关系,并不会将指针清空,所以此时指针str依然不为空,这里会将"world"拷贝到str(准确的说法是将"world"首元素的地址拷贝给变量str,且会拷贝’\0’)所以打印结果是"world"。

三、柔性数组

**定义:**结构体中的最后一个元素允许是未知大小的数组,这就叫作柔性数组成员。如:

typedef struct st
{
    
    
    int i;
    int a[0];
}st;

或另一种写法:

typedef struct st
{
    
    
    int i;
    int a[];
}st;

特点:

  • 结构中的柔性数组前面必须至少有一个成员(否则柔性数组便没有了意义,不如直接使用malloc开辟空间)。
  • 柔性数组成员不影响结构体的大小,但是会影响内存对其。
  • 柔性数组的数组名是起内存标识作用的一种占位符。

使用:

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<assert.h>

typedef struct st
{
    
    
    int i;
    int a[0];//柔性数组成员
}st;

int main()
{
    
    
    st* p = (st*)malloc(sizeof(st) + 10 * sizeof(int));//动态内存开辟


    if (NULL == p)
    {
    
    
        assert(1);//判断是否申请成功
    }
    p->i = 10;
    //使用开辟的空间
    for (int j = 0; j < 10; j++)
    {
    
    
        p->a[j] = j;
    }
   
    for (int j = 0; j < 10; j++)
    {
    
    
        printf("%d\n", p->a[j]);
    }
    free(p);//释放堆空间
    system("pause");
    return 0;
}

这个程序使用了柔性数组,在堆区开辟了空间并赋值使用,通过柔性数组下标可以访问到其后面开辟的所有空间。

有些时候,我们为了在结构体中有一个变长数组,也可以在结构体中定义一个指针,让这个指针指向动态开辟的内存:

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<assert.h>

typedef struct st
{
    
    
    int i;
    int* p;
}st;

int main()
{
    
    
    int *p = (int*)malloc(sizeof(int) * 10);
    if (NULL == p)
    {
    
    
        assert(1);//判断是否申请成功
    }
    st mem = {
    
     10,p };//初始化了一个结构体变量mem

    for (int j = 0; j < 10; j++)
    {
    
    
        p[j] = j;
    }
   
    for (int j = 0; j < 10; j++)
    {
    
    
        printf("%d\n", p[j]);
    }
    free(p);//释放堆空间
    system("pause");
    return 0;
}

相比之下,使用柔性数组更加方便,而且由于结构体是封装的类型,其内部元素容易被忽略,可能使用后会忘记释放内存,导致内存泄漏。另一方面,柔性数组使用的是连续的堆空间,有利于提高访问速度。

猜你喜欢

转载自blog.csdn.net/qq_44631587/article/details/121344460