动态内存管理详细介绍

文章目录

  • 1.为什么存在动态内存分配?
  • 2.各种动态内存函数介绍
    • 2.1 malloc
        • 2.1.1声明与解释
        • 2.1.2 使用
    • 2.2 free
    • 2.3 calloc
    • 2.4 realloc (王者出场,真正的调整数组长度)
      • 2.4.1 realloc的空间开辟方式
  • 3.常见的动态内存错误
    • 示例1
    • 示例2
    • 示例3
    • 示例4
    • 示例5
    • 示例6
  • 4.几个经典笔试题
    • 题目一:
      • 第一种改法:
      • 第二种改法:
    • 题目二:
      • 第一种改法:
      • 第二种改法:
    • 题目三:
  • 5.柔性数组
    • 5.1具有柔性数组的结构大小计算:
    • 5.2 柔性数组的使用
      • 总结

1.为什么存在动态内存分配?

目前为止,我们已经掌握的内存开辟方式有两个,且知道在内存中,我们主要使用的是 栈区,堆区,静态区.其中三者的作用分别如下:

(1)静态存储区:主要存放static静态变量、全局变量、常量。这些数据内存在编译的时候就已经为他们分配好了内存,生命周期是整个程序从运行到结束。

(2)栈区:存放局部变量。在执行函数的时候(包括main这样的函数),函数内的局部变量的存储单元会在栈上创建,函数执行完自动释放,生命周期是从该函数的开始执行到结束

(3)堆区:程序员自己申请一块任意大小的内存—也叫动态内存分配。这块内存会一直存在知道程序员释放掉。C语言中,用malloc or new动态地申请内存,用free or delete释放内存。良好习惯:若申请的动态内存不再使用,要及时释放掉,否则会造成内存泄露

示例:

第一:创建局部变量,函数调用

void print()
{
     
    char str[] = "I'm your father";
    printf("%s",str);
}

int main()
{
     
    int a = 0;   
    
    print();
    return 0;
}

例如上面,就是创建了局部变量 str和a,以及进行了函数调用.而他们是在 栈区使用内存

第二: 全局变量,常量等

int b = 20;
int main()
{
     
    const int c = 10;
    return 0;
}

例如上面,就是创建了 全局变量b,常量c, 而他们是在 静态区使用


但是上述的内存开辟方式却有两个特点:

  1. 空间开辟大小是固定的.

  2. 数组在声明的时候,必须指定数组长度,且此后长度固定,无法修改.它所需要的内存在编译时进行分配


而最后剩下的一种----- 堆区,便是我们此篇文章的核心与主角.

**引言:**有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时才开辟空间的方式就不能满足,这时候就只能试试动态存开辟,即使用 堆区内存

比如下面的例子:

struct info
{
     
    char name[20];
    int age;
};

int main()
{
     
    int n = 0;
    scanf("%d",&n);
    struct info arr[50];
    return 0;
}

上面的结构体 struct info是一个人(学生)的信息(姓名,年龄).下面创建了一个大小为50的数组arr用来存各个学生的信息.

但是我们难免会碰到一些情形,比如一些学生因为特殊原因不来上学,就不会记录信息.或者因为生源太好,名额超出50.那么最开始设置的50个名额,就不会够用,或者空间浪费.因此我们便需要对数组长度进行动态调整.

2.各种动态内存函数介绍

2.1 malloc

官方声明:void* malloc (size_t size)

  • size 需要开辟空间的字节大小
  • void* 开辟的空间的地址

2.1.1声明与解释

作用: 开辟一块数组空间,返回地址.

使用:

  • 如果开辟成功,则返回一个指向开辟成功的空间的指针

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值必须检查

  • 返回值类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

  • 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。

2.1.2 使用

例子:

  1. 向内存申请10个整形空间
int* p = (int *)malloc(sizeof(int) * 10);

可以看见size的实参是 sizeof(int) * 10

最后返回时,指针void*被转化为整型指针.

  1. 在申请的时候可能也会申请失败,比如:
#include
#include 
int main()
{
     
    double* p = (double*)malloc(sizeof(double) * 1000000000000);
    if (p == NULL)
        perror("申请失败原因:");
    return 0;
}

结果:

动态内存管理详细介绍_第1张图片

  1. 使用空间
#include
#include 
int main()
{
     
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL)
        perror("申请失败原因:");
    else
    {
     
        for (int i = 0; i < 10; i++)
        {
     
            p[i] = i;
            printf("%d ",p[i]);
        }
    }
    return 0;
}

动态内存管理详细介绍_第2张图片

上面已经介绍了如何在 堆区申请空间,判断申请是否成功,如何使用malloc .

但是我们最开始就说了,堆区申请的空间是 程序员自己手动释放,而放眼看前面,我们释放了吗??

并没有,因此,引出了下面的函数 free


2.2 free

上述已经学会了使用 malloc ,现在就应该还回去我们所申请的空间了.那怎么还回去呢? 答案是 使用 free函数

官方文档: void free(void *memblock);

memblock 需要释放的空间的地址

使用:

#include
#include 
int main()
{
     
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL)
        perror("申请失败原因:");
    else
    {
     
        for (int i = 0; i < 10; i++)
        {
     
            p[i] = i;
            printf("%d ",p[i]);
        }
    }
    free(p);    // 成功释放
    return 0;
}

上面已经成功释放了,但是还有个小问题,比如我们在free后面和最开始申请空间语句后面加一条语句 printf("%p\n",p);

image-20210518102455618

会发现,申请的空间虽然被释放,但是p的值并没有改变,仍然记住了,此时p成了野指针,及其危险,所以我们需要在free(p)后面加上 p = NULL;

比如:

#include
#include 
int main()
{
     
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL)
        perror("申请失败原因:");
    else
    {
     
        for (int i = 0; i < 10; i++)
        {
     
            p[i] = i;
            printf("%d ",p[i]);
        }
    }
    free(p);    // 成功释放
    p = NULL;    //置为空
    return 0;
}

助记:

此时的p就相当于男女朋友中的男生,当他们分手了以后,男生还记得女生的所有信息(p仍然记住了原地址) ,女生为了防止自己以后不被骚扰,就给了男的当头一棒,让男的失忆(p = NULL)

总结:

malloc,free,NULL 需要成对使用.

free函数用来释放的是动态开辟的内存,如果参数是指向非动态开辟的内存,那么free函数未定义.

如果free的参数是空指针NULL,则free函数什么也不做


2.3 calloc

calloc函数的用法与malloc很像,但是callocmalloc函数多了一个功能---------可以在开辟空间时候,给数组每个位置初始化为0

官方文档: void* calloc (size_t num , size_t size);

num ------>>> 数组长度

size------>>> 数组元素类型的字节大小

开辟成功返回数组地址,不够返回NULL

示例:

#include 
#include 
int main()
{
     
 	int* p = (int* )calloc(10,sizeof(int));  //申请空间
    if(p == NULL)                           //检测是否开辟失败
        perror("错误原因:");
    else
        for(int i = 0;i<10;i++)            //打印数组每个值
            printf("%d ",p[i]);
    free(p);                               //释放空间
    p = NULL;                              //置为空指针
    return 0;
}

动态内存管理详细介绍_第3张图片


2.4 realloc (王者出场,真正的调整数组长度)

前面我们介绍了malloccalloc,但是好像都是在申请一块空间,并没有与我们开头介绍的数组长度可变有半毛钱关系,于是顺应我们的要求,王者老大哥---------realloc出场.

realloc的出现使内存管理更加灵活,可以调整动态内存开辟的大小.

官方文档: void* realloc (void* memblock,size_t size)

  1. memblock 已经开辟的内存块的地址

  2. size 新的空间大小

  3. 开辟成功就会返回开辟的空间地址

  4. 失败就会返回NULL

  5. realloc的返回值需要用一个新的变量接收

示例:

#include 
#include 
int main()
{
     
 	int* p = (int* )malloc(sizeof(int)*10);  //申请空间
    if(p == NULL)                           //检测是否开辟失败
        perror("错误原因:");
    else
        for(int i = 0;i<10;i++)            //打印数组每个值
            printf("%2d ",p[i] = i);
    printf("\n");
    
    
    //调整空间
	int* p1 = (int*) realloc(p, sizeof(int) * 20);
    if(p1 == NULL)              //检测开辟是否成
        perror("错误原因:");
    else
    {
     
        p = p1;      //重新交给p管理申请的空间
        for(int i = 10;i<20;i++)
            printf("%d ",p[i] = i);
    }
    
    
    free(p);                               //释放空间
    p = NULL;                              //置为空指针
    return 0;
}

动态内存管理详细介绍_第4张图片

可以看见,之前malloc只开辟了10个长度,但是现在却可以有20个空间.

2.4.1 realloc的空间开辟方式

但是看了上面的使用示例,大家是否有什么疑问呢? 比如:

  • 为什么一定要用新的指针变量来接收realloc

  • 后面为什么又重新交给原来的指针变量p

  • 释放空间时候,为什么只需要释放p,而不用管p1了.

在回答这几个问题之前我们需要知道realloc开辟空间的方式.以下面的图为例

动态内存管理详细介绍_第5张图片

在堆区,空间占有比较混乱,如图中所示malloc已经成功开辟了一块空间,但是可以清晰的看到,在malloc开辟的地方和右上角其他占用地区中间还有一块小缝隙.在下方的两块其他占用区中间也有一块空白的地方.

reallocmalloc申请的空间进行调整时候,会首先自动检测新的需要开辟的空间,如果直接跟在mallo后是否够用,比如下图:

动态内存管理详细介绍_第6张图片
  1. 比如此时realloc还需要三个整型长度----------realloc (p, sizeof(int)*13); 检测到malloc后面还剩下4可用,于是便直接在其后追加三个空间,然后依然返回p的地址.

  2. 而倘若realloc还需要8个整型长度----------------realloc(p,sizeof(int)*18); 检测到malloc后面只有4个空间,并不够用,于是便再另起山头,在下面看到了一个可以开辟20整型长度的空地,于是便在这里重新开辟 18个整型空间,开辟好后,便把原malloc开辟空间内容拷贝一份放到新开辟的18个空间.然后返回新开辟的空间地址,返回时候,便同时自动释放掉原来malloc开辟的空间

  3. 倘若上面两种都开辟失败,就会返回NULL

因此,这就是我们为何说,一定要用新的指针接收realloc的返回值,因为如果开辟失败,非但没有得到新的空间,还返回了一个NULL给原来管理malloc开辟的空间的变量p,导致p无法再管理malloc. -------------这种危险写法p = (int*)realloc(p,sizeof(int)*18);

而如果开辟成功了,再重新把新的地址交给p管理,p就有两种可能.

  • 第一,仍然接管p原来空间(现在增大了)
  • 第二,接管了新的空间.

会发现,无论上面哪种情况,最后释放p时,都会完全释放两块空间(因为新开辟时候,会自动释放原malloc开辟空间).


3.常见的动态内存错误

示例1

int main()
{
     
     int *p = (int *)malloc(INT_MAX/4);
     for(int i = 0;i<10;i++)
         *(p+i) = i;
     free(p);
    return 0;
}

上面的例子有什么错误??

答案:

  1. 没有判断是否成功开辟空间,如果开辟失败p将会是NULL,那么后面的*(p+i) = i;就是在对 NULL指针进行解引用操作
  2. p最后未置为NULL,是不安全的,正如博主一开始写的男女朋友关系那个例子

示例2

int main()
{
     
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));
    if(NULL == p)
    {
     
    	perror("开辟失败原因:");
        return -1;
    }
    for(i=0; i<=10; i++)
    {
     
    	*(p+i) = i;
    }
    free(p);
    p = NULL;
    return 0;
}

上面的例子有什么错误??

答案:

数组越界,10个空间的数组,索引最大只能9.

示例3

int main()
{
     
    int a = 10;
    int *p = &a;
    free(p);
    p = NULL;
    return 0;
}

上面的例子有什么错误??

答案:

free前面已经讲到只能对管理堆区的内存进行释放,而这里的p管理的是栈区

小插曲:


| 之前忘记讲了,我们知道realloc是对动态开辟的内存进行管理,但是我们如果把realloc的第一个参数给一个NULL |

| 那么,此时的realloc就完全是个malloc , 比如 realloc(NULL, sizeof(int)*10) 等价于 malloc(sizeof(int) * 10)|


示例4

#include 
#include 
int main()
{
     
	int* p = (int*)malloc(sizeof(int)*20);
	if (p != NULL)
	{
     
		for (int i = 0; i < 10; i++)
			*p++ = i;
	}
	free(p);
	p = NULL;
	return 0;
}

上面的例子有什么错误??

答案:

并没有完全释放堆区内存,下图进行解释

动态内存管理详细介绍_第7张图片

在执行循环后,free释放的只是后面橙色的空间,黄色空间并没有释放.

示例5

int main()
{
     
    int *p = (int *)malloc(100);
    if(p == NULL) 
        perror("错误原因");
     else
     {
     
         for(int i= 0;i<25;i++)
             printf("%d ",p[i] = i);
     }
    free(p);
    ......  //省略几万行代码
    ......  //省略几万行代码  
    free(p);
    return 0;
}

上面的例子有什么错误??

答案:

多次释放同一块内存,原因是当代码功能过多时候,我们会忘记前面是否释放过,而导致重复释放.

第一次释放后,空间已经没有了,但是p还记忆着在堆区的原地址,再次释放,如果此时该地址其他东西被占用,将会导致内存管理出错,或者程序挂掉.

防止重复释放的解决办法: 每次释放后记得加 p = NULL,free对空指针NULL不做处理

示例6

int main()   
{
     
    while(1)
    {
     
        malloc(1);
    }
    return 0;
}

上面的例子有什么错误??

答案:

在疯狂的开辟空间,并没有还回去,导致自己不用了,别人也不能用(内存泄露).

这会导致整个计算机最后崩溃.

4.几个经典笔试题

题目一:

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

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

会出现什么结果???

答案:

程序会崩溃.原因:

GetMemory属于传值调用,不是传址调用.
即p虽然获取了堆区申请地址,可以进行管理,但由于GetMemory函数结束后,p的生命周期也就结束了.管理不了该空间,且下面用的还是str,不是p
最后,我们字符串拷贝的第一个参数是str,传值调用时,str的值并没有改变,仍然是NULL.
那么最后指针str还是一个空指针,便不能进行拷贝.程序出错.

其次,在函数GetMemory内部并没有释放内存.

怎么修改??

第一种改法:

void GetMemory(char* * p)  //修改形参
{
     
     *p = (char *)malloc(100);
}

void Test(void)
{
     
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    
    free(str);//释放内存
    str = NULL;
}

第二种改法:

char * GetMemory(char* p)
{
     
    p = (char *)malloc(100);
    return p;       //直接返回值.
}

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

题目二:

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

void Test(void)
{
     
     char *str = NULL;
     str = GetMemory();
     printf(str);
}

答案:

烫烫烫烫烫烫烫烫烫…

解析:

GetMemory函数内部的局部变量的地址是在栈区,当GetMemory函数调用结束,字符数组p的生命周期也就结束,里面的内容也不复存在.而其却返回了p的地址,因为不复存在,系统便给未初始化的栈空间一个中文GB2312编码中0xccdd,正好对应

怎么修改?

第一种改法:

char *GetMemory(void)
{
     
     static char p[] = "hello world";  //变为静态变量,延长生命周期
     return p;
}

void Test(void)
{
     
     char *str = NULL;
     str = GetMemory();
     printf(str);
}

第二种改法:

char *GetMemory(void)
{
     
    char* p = (char*)malloc(100);   //在堆区申请空间,他的生命周期就由程序员结束
    p = "hello world";
    return p;
}

void Test(void)
{
     
    char *str = NULL;
    str = GetMemory();
    printf(str);
    free(str);
    str = NULL;
}

题目三:

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

答案:

world 原因,虽然提前释放了空间,但是 str会有记忆功能(前面说的男女朋友中的男性),所以还是会成功拷贝.

5.柔性数组

c99中,结构体中的最后一个元素允许是未知大小的数组,叫做柔性数组成员

动态内存管理详细介绍_第8张图片

5.1具有柔性数组的结构大小计算:

struct stu
{
     
    int a;
    char c;
    int num[];
};

int main()
{
     
    printf("%d ",sizeof(struct stu));
    return 0;
}

答案:

结构体大小计算博主在自定义类型中已经讲解过, 这里的答案是8字节.会发现8字节只是前面两个成员所占据的空间,也就是说,含有柔型数组的结构体不会计算柔性数组的大小.其他计算方法与结构体一样

5.2 柔性数组的使用

使用malloc 开辟比 含有柔性数组的结构体的空间 更大的空间,多出的空间就是柔性数组的.比如下面

第一种使用方法:

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

int main()
{
     
    int i = 0;
    type_a* p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));    //开辟空间
   
    p->i = 100;
    for(i=0; i<100; i++)     //为元素赋值
    {
     
     	p->a[i] = i;
    }
    free(p);          //释放
    p = NULL;
    return 0;
}

第二种使用方法:

typedef struct st_type
{
     
     int i;
     int *p_a;
}type_a;

int main()
{
     
    type_a *p = malloc(sizeof(type_a));  //只开辟一个结构的空间
    p->i = 100;
    p->p_a = (int *)malloc(p->i*sizeof(int));  //开辟数组空间
    
    for(i=0; i<100; i++)
    {
     
    	p->p_a[i] = i;
    }
    
    //释放空间
    free(p->p_a);
    p->p_a = NULL;    //必须先释放内部的空间
    
    free(p);
    p = NULL;

    return 0;
}

上述两种使用方法可以完成同样的功能,但是 方法1 的实现有两个好处:

  • 第一个好处是:方便内存释放 如果我们的代码是在一个给别人用的函数中,方法二在里面做了二次内存分配,并把整个结构体返回给用户。用 户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个 结构体 指针,用户做一次free就可以把所有的内存也给释放掉。

  • 第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片(前面说过堆区使用内存比较混乱,有很多空闲)。

总结

柔性数组的特点:

  • 结构中的柔性数组成员前面必须至少一个其他成员
  • sizeof返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应 柔性数组的预期大小。

你可能感兴趣的:(c,c++)