动态内存管理例题

本文介绍了动态内存管理的重要性,详细讲解了C语言中的malloc、free、calloc和realloc函数,阐述了动态内存错误和内存泄漏问题,以及内存区域的划分。通过经典笔试题解析加深了对动态内存的理解。
摘要由CSDN通过智能技术生成


为什么存在动态内存分配

我们已经掌握的内存开辟方式有:

	int val = 20;
	char arr[10] = {0};

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

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

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


一、动态内存函数的介绍

malloc和free

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

	void* malloc (size_t size);

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

在这里插入图片描述

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

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

	void free (void* ptr);

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

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

常见的动态内存错误

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

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

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

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

使用free释放一块动态开辟内存的一部分。原因是p++之后,已经不指向这段内存空间的首地址了,释放了一部分动态内存。

	char* p = (char*)malloc(10);
	p++;
	free(p);

并且,有些编译器(例如GCC)使用malloc的时候,会在malloc返回的首地址的前面若干个字节中存放该内存的长度。若p++,则读取不到正确的长度信息,导致未定义行为。
在这里插入图片描述

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

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

内存泄漏

如果malloc了之后,没有调用free释放会怎么样呢?

内存泄漏”。实际开发中,尤其是服务器开发里,特别害怕内存泄漏。典型的原因就是服务器往往是7*24持续运行,运行过程中需要处理很多的请求,每次请求处理时如果出现内存泄漏,会积少成多。内存泄漏到一定程度时,会导致操作系统没有足够的内存可以分配,从而导致程序崩溃。

内存泄漏的问题不怕快,就怕慢,泄露慢就很难定位。因此,常使用“例行重启”来解决这个问题。

C++中引入了“智能指针”,但是不够智能,智能一定程度地缓解内存泄漏问题,不能彻底解决。

相比之下,Java/Python/JS/Go…里面大多都集成了垃圾回收机制(GC),能够更好地解决内存泄漏问题。但是,GC中也存在一个致命的问题 STW “stop the world”。尽管如此,Java中对于STW问题其实是进行了一系列的探索和优化的,可以把STW的时间降低到 < 1ms。

下面我们来看一个典型的内存泄漏的例子:

	void* p = malloc(10);
	p = malloc(10);
	free(p);

第一次调用malloc后,指针p中存放了这段内存空间的首地址;第二次调用malloc,开辟了一段新的内存空间,它的首地址存入p,覆盖了第一段空间的首地址。旧地址“丢了”,也就无法释放。

calloc

C语言还提供了一个函数叫calloc,与malloc的区别在于calloc会在返回地址之前将申请空间的每个字节初始化为零。原型如下:

	void* calloc (size_t num, size_t size);

realloc

realloc函数的出现让动态内存管理更加灵活。它可以做到对动态开辟的内存的大小进行调整。函数原型如下:

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

ptr是要调整的内存地址
size是调整之后的新大小
返回值为调整之后的内存起始位置

如果realloc的时候发现内存的空隙不足以容纳下新增的数
据,此时就会触发搬运操作,在内存中重新找一个更大的,连续的空间,然后把原来的内存的数据给自动的拷贝过去
原内存空间就被自动释放了。
在这里插入图片描述
整体搬运:
在这里插入图片描述

二、内存区域的划分

一个计算机(操作系统)上,同一时刻运行着很多个程序/进程。操作系统会给每个进程划分一定的内存空间。每个进程的内存空间,又会分成几个大的区域。
按照地址高低可以这样划分:

低地址

代码段:存放二进制指令

数据段:存放全局变量和static修饰的变量

:malloc等操作动态申请的内存

:存放局部变量和函数之间的调用关系

高地址

对于递归函数来说,如果没有递归结束条件,就会出现栈溢出(stack overflow)的情况。

堆有多大?
内存有多大,堆就有多大(一个程序的内存空间绝大部分都给了堆)。例如256G内存,堆至少能占250G。

栈的空间比较小,堆的空间比较大。
栈上申请释放内存速度极快(本质就是一条机器指令)。堆上申请释放内存很慢(涉及到操作系统内核中的一些工作)

在实际开发中,
如果某个内存较小,并且需要频繁申请释放,使用栈。如果某个内存较大,并且不需要频繁申请释放,使用堆。
如果某个内存较小,也不需要频繁申请释放,都行。
如果某个内存较大,也需要频繁申请释放,只能使用堆。

三、四个经典笔试题

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

解答:
由于p是局部变量,是实参的一份拷贝,因此,对这份拷贝所作的任何操作都无法影响实参。所以,经过GetMemory之后,str仍然是NULL,试图将一个字符串拷贝到空指针里必然会导致内存访问越界。
此外,还有以下问题:
(1)没有free,内存泄漏。
(2)malloc返回结果没有判空。
(3)GetMemory函数的参数没有判空。

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

解答:
显而易见,p是局部变量,函数执行完之后,p这个变量的内存就被释放了,试图打印的话就是未定义行为。

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

解答:
有了第1题的经验,我们可以使用二级指针实现开辟空间。由于p是局部变量,是实参的一份拷贝,所以任何对其的直接操作都是没有办法传出函数的范围的。但是对其进行解引用操作可以将变化传出去。因此这份代码是可以成功开辟空间并且打印出hello的。然而,内存泄漏和没有判空的问题依然存在。

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

解答:
free操作不会把指针中存放的地址变为NULL,满足判断条件,因此可以打印出world。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值