动态内存管理

目录

动态内存管理产生的原因

动态内存函数的介绍

常见的动态内存错误

经典笔试题讲解

柔性数组


动态内存管理产生的原因

我们在使用数组的时候经常会产生浪费,因为数组是一次性开辟的空间,可能一次开辟了100个字节大小的空间,但是最终只使用了10个,从而造成了浪费。

为了减少这种不必要的浪费,就有了动态内存管理这一概念,我们可以开辟一块动态的、不固定大小的空间,方便我们调整,满了就扩容。

动态内存函数的介绍

一、malloc

 malloc函数只有一个参数,size_t 类型的size,代表的是字节数,意思是malloc申请了多少个字节。返回类型是void* ,因为malloc函数事先不知道为谁开辟的空间,所以交给程序员自己强转,开辟空间。

示例:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(sizeof(int) * 10);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; ++i) {
		*(p + i) = i;
	}
	return 0;
}

给变量p开辟 40个字节大小的空间,p是int*类型的,所以malloc强转成int*类型。

开辟完空间后,要记得判断p是否为空,是的话用strerror(errno)报出错误信息并结束程序。

如果不为空就往p里赋值。

二、free

 free是和malloc、calloc配套使用的,向内存申请了空间,结束时就要返回空间,如果程序一直运行,申请的空间一直占着但也不用,又不返回,就会造成内存泄漏。

什么是内存泄漏?比如一台服务器一直连续运行着一个程序,但程序向内存申请的空间一直没有释放,内存就会一点一点减少,直到不够了出现死机现象。重启恢复正常但运行程序一段时间又重复上述过程,这就是内存泄漏,因为申请空间没有释放。

比如这段问题代码:

	while (1)
	{
		malloc(1);
	}

早期编译器不像现在这么智能,系统会一直给它分配空间,电脑容易死机。

现在有些电脑也一样,我写博客的时候亲测过了,运行6秒电脑直接挂了。

 上面示例一中的代码中没有free,但不会产生内存泄漏,因为在这个程序结束时系统会自动回收内存,或者说这个程序不是一直在运行,而是只执行很短的一段时间,结束时系统就自动回收内存了。

当然我们也可以free一下:

 我们可以看到虽然这里free了p,但是p里的内容没有变,这是很危险的,必须将其置空,防止野指针的问题。

三、calloc

 calloc函数其实就是在malloc的基础上自动将所有元素初始化为0,calloc== malloc+memset

四、realloc

 realloc函数就可以实现扩容,使得动态开辟内存更加灵活,它有2个参数,ptr是指向要扩容空间的地址,size则是扩容后新的空间的大小。  返回值为调整成功返回空间的地址,开辟失败返回空。

示例:

int main()
{
	int* p = (int*)malloc(sizeof(int) * 10);
	if (p == NULL)
		return 1;
	int* ptr = (int*)realloc(p, sizeof(int) * 20);
	if (ptr != NULL)
		p = ptr;
	free(p);
	p = NULL;
	return 0;
}

 注意这里realloc的返回值不能直接交给p,如果扩容太大导致失败返回空指针的话,p接收,本来指向的是40个字节的内容,现在变成了空,就会出现问题,所以先用ptr接收一下,不为空再给p.

realloc在扩容的时候,有两种情形:

一、原空间后面有足够的空间支持扩容,这种直接在后面开辟空间就可以,返回原空间的地址。

二、原空间后面空间不足,不支持直接扩容。

这时realloc会在内存其他地方找一块连续的空间实现扩容,将原空间数据拷贝到新空间,最终返回新空间的地址,原空间realloc会自动释放。

这样原空间和其他程序占用之间会有一块空间存在浪费,所以我们知道动态开辟内存太多的话不仅会造成空间碎片化(很多小块的空间浪费),还会导致效率下降,malloc、realloc都有消耗。

建立内存池来维护创建的空间是一种常用的手段,这是后话了。

常见的动态内存错误

一、对空指针解引用操作

int main()
{
	int* p = (int*)malloc(10);
	*p = 20;
	free(p);
	p = NULL;
	return 0;
}

开辟空间有可能失败,会返回空指针,直接对其解引用就会出现错误。

解决方法:

int main()
{
	int* p = (int*)malloc(10);
	if (p == NULL)
		return 1;
	*p = 20;
	free(p);
	p = NULL;
	return 0;
}

在使用之前一定要先判断。

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

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

 当i == 10的时候越界访问,出现错误。

三、对非动态开辟内存使用free

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

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

int main()
{
	int* p = (int*)malloc(40);
	for (int i = 0; i < 5; ++i)
	{
		*p = i;
		p++;
	}
	free(p);
	p = NULL;
	return 0;
}

 切记不能p++改变p的指向,因为最后释放的是p的地址开始的位置,p移动到p+5的位置,释放动态内存的一部分是不可以的。

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

int main()
{
	int* p = (int*)malloc(40);
	for (int i = 0; i < 10; ++i)
	{
		*(p + i) = i;
	}
	free(p);
	//代码...
	//...
	//...
	free(p);
	return 0;
}

 不能多次释放,所以free完后一定要置空,这样就算再free也没什么影响,free完后p里保存的任然是p原来的地址,是野指针,所以一定要置空。

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

上面已经详细讲过内存泄漏问题了,就不再赘述了,这是最可怕的一种错误,一定要注意。

或者假设p申请了空间,又把空间给了ptr,ptr忘记释放了,也会造成内存泄漏。

经典笔试题讲解

题目一:

问:能否打印str?

首先进入Test函数,给str赋空指针,传参给GetMemory函数,注意,这里传参传的是str的值,而不是地址,是传值调用,所以GetMemory函数中为p开辟了100个字节的空间后,出了函数p就销毁了,str没有改变,还是空指针。strcpy函数使用时要对两个参数解引用,空指针不能解引用,就出现错误了。并且上面GetMemory函数中没有free释放内存,p虽然销毁了,但开辟的空间还在,会造成内存泄漏。

改法1:

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

 改法2:

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

题目二:

问:能不能打印str的内容?

 和上一题类似,GetMemory函数中的p数组也是局部变量,出了函数就销毁了,返回的str其实是个野指针,虽然还是指向p的位置,但其内容随着销毁可能已经被覆盖了,所以打印结果应该不是hello world,而是其他随机内容了,大概率是被覆盖了。

题目三:

问:能不能打印str的内容?

 和题目一我们改的版本基本一样,就是少了free,所以能打印hello,但存在内存泄漏。

题目四:

问:能不能打印str的内容?

 还是野指针的问题,不能打印。

free之前都没问题,但是free后没置空,此时的str就是野指针,将world放入str形成非法访问。

柔性数组

柔性数组只能出现在结构体中,它的长度是不确定的,一般形式写成:

struct S
{
	int n;
	int arr[0];
};

但有的编译器编不过去,最好写成:

struct S
{
	int n;
	int arr[];
};

柔性数组必须是结构体里面最后一个成员,且大小未知

柔性数组的特点:

 第三点,柔性数组成员用malloc函数动态分配内存。

示例1 :

#include<stdlib.h>
struct S
{
	int n;
	int arr[];
}a;
int main()
{
	struct S* a = (struct S*)malloc(sizeof(struct S)+40);
	if (a == NULL)
		return 1;
	struct S* b = (struct S*)realloc(a,sizeof(struct S) + 80);
	if (b != NULL)
	{
		a = b;
		b = NULL;
	}
	free(a);
	a = NULL;
	return 0;
}

示例2:

struct S
{
	int n;
	int *arr;
}a;
int main()
{
	struct S* a = (struct S*)malloc(sizeof(struct S));
	if (a == NULL)
		return 1;
	a->arr = (int*)malloc(40);
	int* b = (int*)realloc(a->arr, 80);
	if (b == NULL)
		return 1;
	free(a->arr);
	free(a);
	a = NULL;
	return 0;
}

示例1的方法比示例2的好,1只需要malloc和free一次,而2中需要两次,malloc次数越多,内存碎片就可能越多,性能也会越低。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值