ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

【C语言】-动态内存管理详解+笔试题练习

2021-01-22 10:32:07  阅读:190  来源: 互联网

标签:malloc 函数 int C语言 详解 动态内存 空间 include 开辟


动态内存管理

为什么存在动态内存分配

我们常见的内存开辟方式有:

第一种

int  val = 20;

在栈空间上直接开辟四个字节

第二种

char arr[10] = {0};

在栈空间上开辟十个字节的连续空间

上述的开辟空间方式有两个特点:

1. 空间开辟的大小是固定的。
2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

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

动态内存函数的介绍

malloc

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

函数声明:

void* malloc(size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者字节来决定。
- 如果参数size为0,malloc的行为是标准未定义的,如何开辟取决于编译器。

举一个malloc开辟空间的例子:

#include <stdio.h>

int main()
{
	//动态内存开辟
	int* p = (int*)malloc(25*sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
	}
	else
	{
		int i = 0;
		for (i = 0; i < 25; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 25; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	return 0;
}

上述代码中,malloc函数向内存申请了25个整型的空间,接下来我们先判断是否申请成功。前面我们提到了,如果申请成功则返回所申请空间的起始地址,失败则返回一个空指针。下面用一个if else语句,如果返回空指针,则说明开辟失败,既然失败了,我们就利用perror函数将错误信息打印出来,如果开辟成功,就给这25个整型空间赋值并打印。

运行结果:
在这里插入图片描述
可以看到这次malloc函数成功开辟了25个整型空间。

接下来肯定有人想问,如果开辟失败是什么样的情况,会打印出哪些错误信息呢?

这回我们来看一次开辟失败的例子:

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

int main()
{
	//动态内存开辟
	int* p = (int*)malloc(25555555555555*sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
	}
	else
	{
		int i = 0;
		for (i = 0; i < 25; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 25; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	return 0;
}

还是刚才的代码,不过这次我们所开辟的空间是一个很大的值,下面来看运行结果:
在这里插入图片描述
可以看到这次的结果是perror函数打印的错误信息,说明内存开辟失败,提示没有足够的空间。

下面我们再通过调试来观察malloc函数的返回值。
在这里插入图片描述
可以看到malloc函数的返回值为一个空指针。

看到这里,我想告诉大家的是,其实我们上面所写的代码是有问题的。

刚才我说了,malloc函数开辟的是动态内存,这和数组开辟空间是不一样的。数组所开辟的空间是向内存中的栈区申请的,栈空间不需要维护,变量自动开辟,自动消失。而malloc函数开辟的动态内存是向堆区申请的,堆空间是手动开辟,用完后要手动释放,要不然就会内存泄漏

这里来解释一下内存泄漏:假设你所写的程序某一步开辟了一块动态内存存放过一些信息,你在用完这些信息之后没有将这块内存释放掉,而我们知道动态内存在开辟的时候使用的函数会返回这块空间的地址。那么别人就有可能会拿到这个指针,从而找到你最初存放信息的动态内存,这样你所写在的内存的信息就会泄漏掉。

还有一点我们考虑,如果一块空间在程序执行过程中使用完还不销毁,是不是就会一直占据内存,所写的程序效率也会大大降低。(当然,在程序结束之后,该动态内存还是会还给操作系统的)

为了安全和效率考虑,动态内存手动开辟在使用完之后就要手动的将它释放掉。

free

这里C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

free函数用来释放动态开辟的内存。

  • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数ptr是NULL指针,则函数什么事都不做。

malloc和free函数都声明在<stdlib.h>头文件中。

下面我们将上述代码改正过来:

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

int main()
{
	//动态内存开辟
	int* p = (int*)malloc(25*sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
	}
	else
	{
		int i = 0;
		for (i = 0; i < 25; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 25; i++)
		{
			printf("%d ", *(p + i));
		}
		//回收-释放内存
			free(p);
			p = NULL;
	}
	return 0;
}

这里可以看到我们在释放完动态地址之后,还将存放该地址的指针置为了空指针,这样做的目的也是为了防止内存泄漏。

原因是当free函数释放完空间之后,是将该空间还给了操作系统,但此时空间里的内容还在里面,如果这块空间被调用,那么新的信息会覆盖这块空间,你原有的信息才会彻底消失。但如果没有被调用,p指针还是指向这块空间,而这块空间的内容还在里面,有人发现这个p指针里有一个地址,就可能通过这个地址访问到你释放掉空间的内容,非法访问内存。所以为了避免这种情况,在释放完之后,我们还应该将返回的指针置为空指针。

说到这里大家可能也感受到了,free函数的功能还是不够彻底。

calloc

C语言中还提供了一个函数叫calloc,calloc函数也用来动态内存分配。原型如下:

void* calloc(size_t num, size_t size);
  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
  • 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0.

举个例子:

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

int main()
{
	int* p = calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 0;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;

	return 0;
}

我们使用calloc函数来开辟十个整型空间,看看开辟完之后所申请空间的值为什么:
在这里插入图片描述
可以看到这40个字节的值全部被初始化为了0.

接下来我们再来和malloc函数来对比一下:
在这里插入图片描述
可以看到malloc所申请的40个字节的空间全为随机值,并没有被初始化。

所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
当然如果不需要对内容初始化,我还是建议大家使用malloc来申请空间,其实简单想一下也能想到,calloc函数在初始化的时候是不是还要执行多余的代码,所以calloc函数的效率就不如malloc函数高了,malloc函数耽误的时间肯定是少的。

realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时我们会发现过去申请的空间太小了,有时我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr是要调整的内存地址
  • size是调整之后新的大小
  • 返回值为调整之后的内存起始位置
  • 这个函数在调整原内存大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存的时候存在以下两种情况:

情况1:原有空间之后有足够大的空间
在这里插入图片描述
ptr所指向动态内存空间后面有足够的空间,要扩展内存就直接在原有内存之后追加空间,原来空间的数据不发生变化,返回值还是原来空间起始地址的值ptr。

如图橙色代表原有空间,绿色代表追加空间,黄色代表其他空间。

情况2:原有空间之后没有足够大的空间
在这里插入图片描述
当原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另外找一个合适大小的连续空间来使用。这样函数返回的是一个新内存地址。

如图,橙色代表原有空间,黄色代表其他空间,原有空间后续的空间不够扩展,于是找了一块大小合适的空间来使用,下方橙色+绿色部分,然后将原有空间的内容拷贝到新的空间中去,返回新空间的起始地址。

接下来看几个realloc函数开辟空间的例子:

代码1:

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

int main()
{
	int *p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	/*调整大小*/
	int* ptr = (int*)realloc(p, 20*sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
	}
	else
	{
		p = ptr;
		ptr = NULL;
	}
	for (i = 10; i < 20; i++)
	{
		p[i] = i + 1;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", p[i]);
	}
	//释放
	free(p);
	p = NULL;

	return 0;
}

上述代码的实现的的功能就是先用malloc函数开辟10个整型的空间,然后使用,再用realloc函数将这个10个整型的空间扩展为20个整型的空间,下面我们来通过调试看看会发生哪一种情况。
在这里插入图片描述
可以看到发生的是第一种情况,代码原有空间之后的空间足够用,所以直接在原有空间之后追加10个整型空间,返回的还是原来的地址。

代码2:

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

int main()
{
	int *p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	/*调整大小*/
	int* ptr = (int*)realloc(p, 200000*sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
	}
	else
	{
		p = ptr;
		ptr = NULL;
	}
	for (i = 10; i < 20; i++)
	{
		p[i] = i + 1;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", p[i]);
	}
	//释放
	free(p);
	p = NULL;

	return 0;
}

这段代码的实现的的功能就是先用malloc函数开辟10个整型的空间,再用realloc函数将这个10个整型的空间扩展为200000个整型的空间,下面我们来通过调试看看会发生哪一种情况。
在这里插入图片描述
可以看到这次出现的是第二种情况,原有空间后面的空间不够用了,于是重新找了一块空间来扩展,返回的是新空间的地址。

这里有一点不知道大家有没有发现,我在写上述两个代码的时候,malloc函数返回的地址我用的是指针p来接收的,而realloc函数返回的地址我用了一个新的指针ptr来接收。这里我想问大家,既然这两个指针指向的都是同一块空间,那么可不可以直接使用指针p来接收realloc函数返回的地址呢?

不建议大家这样做。因为还有一种情况可能会发生,就是malloc函数成功开辟了一块空间并返回了该空间的地址。而当我们用realloc函数扩展的时候,开辟空间失败了,这个时候返回一个空指针我们用存放malloc地址的指针接收了,那这个时候malloc函数开辟的地址不就找不到了。也就是说realloc函数犯的错让malloc函数承担了,这显然是不好的。所以建议大家两个函数的返回值用不同的指针来接收。

到这里动态内存分配的四个重要函数就介绍完了,下面我们再来看几种常见的动态内存错误

常见动态内存分配错误

1.对空指针的解引用操作

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

int main()
{
	int *p = (int*)malloc(INT_MAX);
	//可能存在的对NULL指针的解引用操作
	*p = 0;

	return 0;
}

如上述代码中,在使用malloc函数开辟完空间之后,没有判断返回值,而是直接使用,这样万一开辟空间失败就会返回一个空指针,而给一个空指针赋值肯定就会出错了。

上述代码开辟的空间大小是INT_MAX,这是有符号整型正值的最大值,查看一看它的定义就会发现它是一个很大的值。
在这里插入图片描述

这样大的内存肯定开辟失败返回一个空指针,而未加判断给空指针赋值就会出错。

运行结果:

在这里插入图片描述

2.对开辟空间的越界访问

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

int main()
{
	int*p = malloc(10*sizeof(int));
	if (p == NULL)
	{
		return 0;
	}
	int i = 0;
	//对动态开辟内存的越界访问
	for (i = 0; i <= 10; i++)
	{
		p[i] = i;
	}
	for (i = 0; i <= 10; i++)
	{
		printf("%d ", p[i]);
	}
	free(p);
	p = NULL;

	return 0;
}

上述代码中开辟了10个整型的空间,而在访问的时候赋值的时候赋值了11整型的空间,这就发生了越界访问的情况,就会出错。

在这里插入图片描述

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

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

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

有的人在学会使用动态内存开辟之后就变得魔怔了,看到一个指针使用完就想用free释放掉。如上述代码所示,p指针是一个栈区的指针,使用完之后就会自动销毁掉,而free释放的是动态内存开辟的指针。这里拿free释放栈区的p指针就会出错。

在这里插入图片描述

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

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

int main()
{
	int*p = (int*)malloc(10*sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 0;
	}
	int i = 0;
	//释放动态开辟内存空间的一部分-err
	for (i = 0; i < 5; i++)
	{
		*p = 1;
		p++;
	}

	free(p);
	p = NULL;

	return 0;
}

这段代码在开辟内存成功之后,给这块空间在赋值的时候改变了p指针指向的内容。p原先存放的是malloc函数开辟空间的起始地址,而这里给这块空间赋完值之后,p指向的是这10块整型空间第6块整型空间,而这个时候我们释放p的时候就不能释放我们所开辟的动态内存了。

在这里插入图片描述

5.对一块动态内存多次释放

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

int main()
{
	int*p = (int*)malloc(10*sizeof(int));
	free(p);
	free(p);

	return 0;
}

这段代码在先释放掉一次动态空间之后,又释放了一次,第二次释放的时候这块空间已经不属于这个程序了,所以会出错。这种问题一般发生在多个函数传参的问题中,就是我们在一个函数中把动态内存释放了,但是在另一个空间中又释放了一次,所以就会出错了。

在这里插入图片描述

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

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

int main()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
	return 0;
}

内存泄漏的问题前面我已经很详细的说过了,正如上述代码,开辟完空间之后忘记释放,就会造成内存泄漏问题。

切记:动态开辟函数和free函数应该成对出现,正确释放。

当然即使是成对出现但还是有一种情况会发生内存泄漏,举一个例子:

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

void test()
{
	int*p = (int*)malloc(12);
	if (p == NULL)
		return;
	//使用空间
	if (1)
		return;
	free(p);
	p = NULL;
}

int main()
{
	test();
	return 0;
}

上述代码中,我们在test函数中开辟了12个字节的空间,在使用这块空间的时候,当函数发现所满足的条件成立,就直接return结束函数。而free的释放是放在这些判断语句后面的,所以如果这个函数在free函数使用前就结束的话,那么该内存就没有被释放,这样虽然free和malloc是成对出现的,但还是会造成内存泄漏。

所以在使用动态内存开辟空间的时候一定要严谨!!

几个经典的笔试题

题目1:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

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

int main()
{
	Test();
	return 0;
}

请问运行Text函数会有什么样的结果?

运行结果:
在这里插入图片描述
可以看到访问位置发生冲突。

分析:
是这样的,先在Test函数中创建了一个指针变量存放了一个空指针,这个时候把这个指针变量作为参数传给了GetMemory函数。问题就出在这里,虽然他是一个指针变量,但传递的是它的变量名,而不是地址。函数传参的时候如果传递的是变量,那么形参就会重新开辟一块和实参大小一样的空间来存放实参的内容,而接下来在函数内部操作的是形参里的内容,和实参并无任何关系。所以接下来在GetMemory函数中把动态内存开辟的地址传给了形参p,和实参str并没有半点关系,实参的内容还是空指针。所以Text函数中把字符串"hello world"拷贝给str,实际上是拷贝给一个空指针,所以访问会出错。

还有一个问题相信大家也发现了,我们刚刚才讨论过,动态内存的开辟malloc函数和free函数要成对出现,而这里并没有释放内存,显然造成了内存泄漏的问题。

下面我们将这两个问题解决掉,重新来改进一下这段代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
}

int main()
{
	Test();
	return 0;
}

这次我们将str的地址传过去,用一个二级指针来接收,这下就可以在GetMemory中,将malloc函数开辟的地址传个str,然后在Text函数中,利用strcpy函数将字符串"hello world"拷贝到这块动态空间中。最后还要将这块动态空间给释放掉。

运行结果:
在这里插入图片描述

题目2:

#include <stdio.h>

char *GetMemory(void)
{
	//返回栈空间地址的问题
	char p[] = "hello world";
	return p;
}

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

int main()
{
	Test();
	return 0;
}

请问运行Text函数会有什么样的结果?

运行结果:
在这里插入图片描述
可以看到这个结果看不懂他是什么,实际上这是一个随机值,显然这段代码也是有问题的。

分析:
这段代码在GetMemory函数中创建了一个数组,然后把这个数组的地址作为返回值赋值给在Text函数中定义的指针变量,然后想把这个字符串数组打印出来。这里的问题是这样的,数组创建的内存是在栈区创建的,栈区创建的内存在函数使用完之后就会销毁掉,所以这块空间的地址虽然传递了出去,但这块空间已经还给操作系统了,那么这个指针也就变成了一个野指针,打印一个野指针的结果就是随机值。

希望通过这个例子大家能够感受到动态内存和数组内存的不同之处。

题目3:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

请问Text函数会有什么样的结果?

这段代码简单分析不难发现,实际上就是前面我们对题目1的改进代码,是完全没有问题的,最终也会成功的将字符串"hello"赋值给所开辟的动态内存空间,并且打印出来。

运行结果:
在这里插入图片描述

题目4:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void Test(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);

	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

int main()
{
	Test();
	return 0;
}

请问运行Text函数会有什么样的结果?

这段代码看着没有问题,实际上是有问题的。

分析:
用一个指针str接收malloc函数创建的100字节的内存,然后紧接着将字符串"hello"拷贝到这块空间去,然后用free函数将这块空间释放掉。但是这里有一个问题,使用free函数释放,说明我们已经不想要这块空间了,想把这块空间里的内容hello给销毁掉。但是我们使用完free函数之后,并没有将该指针置为空指针,所以后面的if语句通过这个指针访问到了这块空间,并将这块空间的内容给变成了world,这就是内存泄漏的问题。

运行结果:
在这里插入图片描述
下面我们改进一下这段代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void Test(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);
	str = NULL;

	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

int main()
{
	Test();
	return 0;
}

改进方法其实很简单,只需在使用完free函数后,把指针str置为空指针即可,这样后面的if语句就不会通过这个指针再来访问到这块空间了。
运行结果:
在这里插入图片描述
如果大家真的仔细看了这四道题,我想有一个问题可能在大家心中有点困惑。那就是printf(str)这种打印方式正确吗?

确实这种打印方式不太常见,但是我想告诉大家的是这种打印方式是正确的。大家肯定打印过这种信息printf(“请输入:”),这里的这三个汉字实际上就是一个字符串,字符串在C语言中是一个比较神奇的东西。printf打印这三个字的时候,实际上就是把这个字符串的地址传给了printf函数,然后进行的打印。所以说我们在打印字符串的时候,只要把他的地址用双引号括起来传给printf函数就能把这个字符串打印出来了,这里的str就是字符串的地址。当然,这个操作仅限于字符串才能实现。

所以以后万一遇到这种问题,别人问你这段代码哪里有问题,你可千万不要直接说printf函数的打印有问题,这就会显得有点丢人了啊。

希望这篇文章可以为大家带来帮助。

标签:malloc,函数,int,C语言,详解,动态内存,空间,include,开辟
来源: https://blog.csdn.net/m0_50595617/article/details/112977090

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有