动态内存分配及管理——C语言

目录

一、为什么存在动态内存分配

二、动态内存函数介绍

2.1 malloc

2.2 free

2.3 calloc

2.4 realloc

三、常见的动态内存错误

3.1 对NULL指针的解引用操作

3.2 对动态开辟空间的越界访问

3.3 对非动态开辟内存使用free释放

3.4 使用free释放一块动态开辟内存的一部分

3.5 对同一块动态内存多次释放

3.6 动态开辟内存忘记释放(内存泄漏)


        今天,博主给大家带来的是动态内存分配的学习和讲解。在之前,我们学习了通讯录,文章中利用到一些动态内存分配的一些知识,有些可能大家会看不懂,那么相信通过今天的这篇文章,大家的问题就会迎刃而解。本篇,我们将从“为什么存在内存分配”,“动态内存函数介绍”,以及“常见的动态内存错误”三个板块来为大家一 一解答。

一、为什么存在动态内存分配

在之前,我们学过的内存开辟有哪些呢?比如,创建一个变量,或者创建一个数组。

    int a = 10;//在栈空间开辟四个字节
	char arr[10] = { 0 };//在栈空间开辟十个字节的连续空间

上面两种开辟方式是我们常用的开辟内存的方式,但是这两种开辟内存空间的方式有两个特点:

① 空间开辟大小是固定的

② 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是由于空间的需求,有时候我们需要空间的大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足我们的需求了,这时就要试试动态内存开辟的方式了。

二、动态内存函数介绍

动态内存分配及管理——C语言_第1张图片

在学习动态内存函数之前,我们需要知道动态内存开辟的空间是放在堆区的,如上图所示,局部变量和形式参数占用的空间是在栈区的,全局变量以及静态变量开辟的空间是在静态区的。 

2.1 malloc

C语言提供了一个动态内存开辟函数

void* malloc  (   size_t    size   ) ;
这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。
① 如果开辟成功,则返回一个指向开辟好空间的指针。
② 如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查。
③ 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
④ 如果参数 size 为 0 malloc 的行为是标准是未定义的,取决于编译器。
举个代码例子来解释吧!
int main()
{
	int* p = (int*)malloc(40);//开辟40个字节的空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}

	return 0;
}
因为返回的类型是void*,所以我们要根据自己的需求来进行强制类型转换,其次,我们需要判断是否开辟成功,如果返回值为NULL指针,那么就结束了,反之则是开辟成功。然后我们打印一下看看开辟成功的空间里面是什么。
动态内存分配及管理——C语言_第2张图片

此时我们发现开辟的空间里面存的是一堆没见过的随机数数,其实是malloc函数申请的空间,在申请成功后,直接返回这片空间的起始地址,不会初始化空间的内容。 

2.2 free

C 语言提供了另外一个函数 free ,专门是用来做动态内存的释放和回收的,函数原型如下:
void free void*   ptr  ) ;
上面我们学习了malloc函数,我们发现,malloc只负责申请空间,那么申请的这个空间当我们使用完之后会怎么样呢?其实这块空间并不会主动的还给操作系统,除非程序结束,否则这块空间将会一直存在堆区。这个时候就需要另一个内存函数了。
总的来说: malloc申请的内存空间,当程序退出时,还给操作系统,当程序不退出时,动态申请的内存不会主动释放。需要free函数来释放空间。
具体怎么使用呢,直接把我们动态开辟的空间的起始地址传入free函数中即可,代码如下图所示:
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功
	free(p); //释放开辟的空间
	p = NULL;  //置空
	return 0;
}

注意:p本来指向的空间被释放后,p就变成野指针了,比较危险,这时候我们需要主动将p置为空指针。

free只能释放动态开辟的空间,不能释放静态区或者栈区开辟的空间(标准未定义) 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

② 如果参数 ptr NULL 指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中

2.3 calloc

C 语言还提供了一个函数叫 calloc calloc 函数也用来动态内存分配。原型如下:
void*   calloc size_t   num  ,   size_t   size  ) ;
① 函数的功能是为 num 个大小为 size 的元素开辟一块空间 ,并且把空间的每个字节初始化为 0
② 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0
举个例子吧:
int main()
{
	int* p = (int*)calloc(10, sizeof(int));//开辟十个连续的sizeof(int)大小的空间
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//打印数据
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

当我们打印完之后,发现calloc会把每个字节初始化为0。

总的来说:calloc函数和malloc函数很相似,功能也是一样,唯一不同的就是,会把开辟的每个字节都初始化为0。

        所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc 函数来完成任务。

2.4 realloc

realloc 函数的出现让动态内存管理更加灵活。
        有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void*   realloc  void*   ptr  ,   size_t   size  ) ;
① ptr 是要调整的内存地址(也就是被调整空间的起始地址,这块空间之前已经开辟好了)
如果ptr为空指针,那么它的功能和malloc就是一样的,开辟一个新的空间。
② size 调整之后新大小 (需要调整的新的空间的大小)
③ 返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。
④ realloc 在调整内存空间的是存在两种情况:
情况1 :原有空间之后有足够大的空间
情况2: 原有空间之后没有足够大的空间
动态内存分配及管理——C语言_第3张图片
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,同时把原来空间的内容拷贝过来,然后自动释放原来的内存空间,这样函数返回的是一个新的内存地址。
由于上述的两种情况,realloc函数的使用就要注意一些,我们这里用代码来举例子吧:
 
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//初始化为1~10
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	//增加一些空间
	int* ptr = (int*)realloc(p, 80);
	if (ptr != NULL)  //开辟成功
	{
		p = ptr;
		ptr = NULL;
	}
	else  //开辟失败
	{
		perror("realloc");
		return 1;
	}
	//开辟成功,打印数据
	for (i = 0; i < 20; i++)
	{
		printf("%d ", p[i]);
	}
	free(p);  //释放空间
	p = NULL;  //置空
	return 0;
}

注意:考虑到可能开辟失败,所以我们需要先进行判断一下,如果开辟成功,则将返回的指针赋值给p来维护。

当我们开辟完之后,打印一下看看效果。

当我们使用完动态开辟的内存之后,仍然需要手动去释放内存空间。

动态内存分配及管理——C语言_第4张图片

 当然,上面情况只是减少增加空间,如果要减少空间就比较简单了,直接在原来的基础上减少,地址返回的也是原来的地址。

好了,到这里动态内存管理的基本知识就介绍清楚了,实际上把这四个内存函数了解清楚,基本上对动态内存的知识点也就基本掌握了。接下来,我们来了解一下动态内存管理常见的内存错误,通过解释这些错误,来更清楚更深入的了解动态内存管理。

三、常见的动态内存错误

3.1 对NULL指针的解引用操作

当我们动态内存开辟的时候,会存在开辟失败的情况,此时返回的就是空指针。

如下代码就是一个典型的例子:

void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
}

此时动态内存开辟可能失败了,就导致返回的指针为空指针,也就是p为空指针,如果再对p这个空指针进行解引用操作,那么就会报错 。为了解决这种问题,我们在平时写代码的时候,为了避免空指针异常,要对动态开辟的空间返回的指针进行空指针判断检查。(好习惯)

3.2 对动态开辟空间的越界访问

这里直接拿代码来解释吧!

void test()
{
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));
    if(NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for(i=0; i<=10; i++)
    {
        *(p+i) = i;//当i是10的时候越界访问
    }
    free(p);
}

这段代码中,我们使用malloc开辟40个字节大小的空间,然后进行空指针检查判断,紧接着,在我们赋值的时候,我们最多只能访问到p[9]这块空间,代码中我们i的最大值为10,此时很明显,就出现了越界访问。

总的来说:开辟多少空间,就只能使用多少空间,没有开辟的,属于操作系统的空间,我们不可以随便进行操作访问。

3.3 对非动态开辟内存使用free释放

void test()
{
    int a = 10;
    int *p = &a;
    free(p);//ok?
}

在前面讲free的时候说过,free释放的空间,只能是动态内存开辟的空间,不能是静态区或者栈区开辟的空间,上面代码的例子中,a是局部变量,不是动态开辟的空间,所以不能被free释放。

3.4 使用free释放一块动态开辟内存的一部分

int main()
{

	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p = i + 1;
		p++;  //此时p不再指向malloc开辟的空间的起始地址
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

上面代码中,我们使用malloc开辟一块空间,然后将起始地址返回给p,也就是说p指向molloc动态开辟的空间的起始地址,紧接着进行空指针检查判断,然后给p指向的空间进行赋值,但是,在赋值的过程中,p的指向发生了变化(如下图),不再指向malloc开辟的空间的起始地址,此时在对p指向的空间进行释放,这种做法是不被允许的,会报错。

动态内存分配及管理——C语言_第5张图片

总结:free中的参数只能是动态内存开辟空间的起始地址,对于动态开辟的空间的起始地址不要随便的更改指向。

3.5 对同一块动态内存多次释放

void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放
}

上面这种情况也是一直很低级的错误,也是不被允许的,会报错,最好的解决办法就是,在释放完之后,将p指针置为空,这样在后面多次释放也不影响,因为p已经是空指针了。

总结:不能对同一块动态内存多次释放,解决办法:在第一次释放完之后,将指针置为空指针(好习惯)

3.6 动态开辟内存忘记释放(内存泄漏)

void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while(1);
}

对于上面这个代码,在test这个函数种,我们开辟了100个字节的动态空间,但是在最后,我们并没有对这块空间进行free释放,这时候当我们跳出函数之后,指针变量p也销毁了,这个时候,p原来指向的空间我们根本找不到了,但是这块空间仍然存在,仍然被占用,只是我们丢失了它的起始地址,不能再对这块空间进行操作或者访问了,这就造成了这块空间仍然存在占用内存,但是我们却访问不到,并无法释放,这就是所谓的内存泄露。

针对这个问题的解决:在我们使用完动态空间之后,一定要进行free释放。

总结:动态开辟的空间一定要释放,并且正确释放(切记)

好了,今天的动态内存分配和管理讲到这里就结束了,听到这里,相信大家的一些关于动态内存分配管理的问题就会迎刃而解了吧。如果哪里有问题,欢迎在评论区留言。如果觉得小编写的还不错的,那么可以一键三连哦,您的关注点赞和收藏是对小编最大的鼓励。谢谢大家!!!

动态内存分配及管理——C语言_第6张图片

你可能感兴趣的:(c语言,开发语言)