Java教程

详解【动态内存管理】

本文主要是介绍详解【动态内存管理】,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

  • 本章重点
  • 为什么存在动态内存分配
  • 动态内存函数的介绍
    • malloc / free
    • calloc
    • realloc
    • 常见的动态内存错误
  • C/C++程序的内存开辟
  • 柔性数组

秃头侠们好呀,今天来聊聊动态内存管理

本章重点

  • 为什么存在动态内存分配
  • 动态内存函数的介绍
    malloc
    calloc
    realloc
    free
  • 常见的动态内存错误
  • 柔性数组

为什么存在动态内存分配

我们以往学过的内存开辟无非是

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

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

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

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

动态内存函数的介绍

malloc / free

malloc

void* malloc (size_t size);

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

  • 如果开辟成功,返回一个指向开辟好空间的指针
  • 如果开辟失败,返回一个NULL,所以malloc的返回值要做检查,看是否开辟成功
  • 返回值是void* 所以malloc开辟的空间不知道类型,由开辟者自己决定
  • 如果size为0,则malloc行为的标准是未定义的,取决于编译器

free

void free (void* ptr);

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

  • free用来释放动态开辟的内存
  • 如果ptr指向的空间不是动态开辟的,则free的行为是未定义的
  • 如果ptr是NULL指针,则啥也不做
  • 如果动态内存不释放会造成内存泄漏(后面具体说)
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	
	//开辟失败
	if (p == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	//开辟成功
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", p[i]);
	}
	printf("\n");
	free(p);
	p = NULL;

	return 0;
}

1、如果你开辟的空间过大有可能会开辟失败,所以必须检查。
2、开辟成功了,因为是连续的空间,所以p相当于一个数组。
3、最后记得释放动态开辟的空间,你拿的就要还回去,防止内存泄漏。

4、最后要把该指针置空NULL,有什么必要?

指针被free后,指针指向的还是原来的区域,但是这片区域已经不归自己使用了,这片区域可以被别人用,被别人覆盖了,所以你已经变成野指针了,如果你不置为空,你去访问这个地方了,就造成非法访问了,会有不安全因素。且可以防止对一个已经释放的指针多次释放,造成程序崩溃,但我们可以对NULL指针多次释放。

举个例子:
比如你有一个女朋友,有一天你和她分手啦,这里相当于free,她已经不属于你了,你们之间已经没有关系了,她现在可以成为别人的女友了,但是你脑子里还记者她,还记着她的联系方式,这里相当于p指针还指向原来的内容,这样对她是不好的,因为你还能根据联系方式去打扰她的生活,置为NULL,就是清除你对她的记忆,喝下忘情水。(当然祝大家都幸福哦)

calloc

calloc

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

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

realloc

realloc

void* realloc (void* ptr, size_t size);
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整
  • ptr是要调整的内存地址
  • size是调整之后新大小
  • 返回值为调整之后的内存起始位置
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
  • realloc在调整内存空间的时候有两种情况

情况1:原有空间之后有足够大的空间

要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化

情况2:原有空间之后没有足够大的空间

原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,前面的数据会拷贝下来,这样函数返回的是一个新的内存地址,之前的realloc会自己free掉

常见的动态内存错误

1、对NULL指针的解引用操作

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

所以使用动态内存分配,需要判断是否开辟成功,如果成功再使用,否则不能使用,返回NULL,但是不能对NULL解引用

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

int*p=(int*)malloc(200);
for(int i=0;i<80;i++)
{
//....
}

总共申请了200÷4=50个元素,而你的for循环的判断条件到80了,所以当大于50的时候,会出现越界访问。

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

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

当然是不可以的显然是不可以的,因为a是在栈上开辟的空间,不是堆上
free只能释放堆上动态开辟的空间!

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

void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//可以吗?
}

当然是不可以的,因为自增后,p指向的位置改变了,而free释放必须是释放全部的动态开辟的空间(起始位置),不能是部分。

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

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

不可以对一个同一块动态内存重复释放,但是可以这样

free(p);
p=NULL;
free(p);
p=NULL;

6、动态开辟内存忘记释放(内存泄漏)
首先在堆上申请的空间有两种回收方式
1、free
2、程序退出时,申请空间自动回收

如果不对开辟的空间进行释放,则会造成内存泄漏,你的电脑就会越来越卡
所以当使用完动态开辟的内存,一定要记得释放

看几个笔试题

题一:

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

该程序结果是什么?为什么会有这样的结果?

结果是:程序崩溃,什么也不打印
原因
str传给p的时,是值传递,p是str的临时拷贝,当malloc开辟空间的起始位置放到p中时,str并没有改变还是NULL;
当str是NULL,strcpy要把hello world拷贝到str指向的空间时,因为str是NULL,所以不知道拷到哪里,程序崩溃。

那我们应该怎么更改呢?
很简单,我们应该传str地址,这样*p就是str
在这里插入图片描述题二:

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

结果是啥?
在这里插入图片描述为啥是随机值?

这里需要一点函数栈帧的知识
函数在栈上开辟栈帧,函数返回,销毁栈帧,当栈帧销毁后,里面的东西都还给操作系统了,return p,这里的返回值p是通过寄存器(eax)传回来的,把p的地址通过寄存器赋给str,虽然str拿到了p的地址,但是栈帧已经销毁,p地址指向的空间已经不属于p了,这时候p其实已经算是野指针了,如果你再去访问这个空间,就造成了非法访问了。如果空间内容没有被覆盖,还有可能打印出来,如果被别人使用了,就打印随机值了。

题目三:

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

这个代码结果是什么?有什么问题?
在这里插入图片描述看似打印出来了,其实已经出现了问题

char* str = (char*)malloc(100);
strcpy(str, "hello");
这两句没有问题,str指向malloc出来的空间,strcpy把hello拷贝到这片空间
free(str);
if (str != NULL)
{
strcpy(str, “world”);
printf(str);
}
str被free,则malloc出来的空间还给操作系统,不属于自己了,但是str指向的地址没有变,只是变成野指针了,if判断进去,strcpy(str, “world”);这里就出现问题了,因为这片空间已经不属于自己了,你又使用了,所以造成非法访问了,虽然最后打印出来了了,但是早已出错了。
我们应该在free完之后就应该把str置空,养成好习惯,才不容易出错

C/C++程序的内存开辟

在这里插入图片描述
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

现在我们是否就理解了static关键字修饰局部变量的例子了,为啥生命周期会改变

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁 所以生命周期变长

柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
例如:

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

有些编译器会报错,无法编译可以改成下面

struct S
{
int i;
int a[];//柔性数组成员
};

柔性数组的特点:

  • 结构中的柔性数组成员前面必须至少有一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S
{
int i;
int a[];//柔性数组成员
};
printf("%d\n",sizeof(struct S));//结果是4

柔性数组的使用

//代码1
struct S
{
int i;
int a[];
};
int i = 0;
struct S*ps= (struct S*)malloc(sizeof(struct S)+100*sizeof(int));

p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
p=NULL;

这样柔性数组成员a,相当于获得了100个整型元素的连续空间

柔性数组的优势

//代码2
struct S
{
int i;
int *a;
};
struct S*p = (struct S*)malloc(sizeof(struct S));
p->i = 100;
p->a = (int *)malloc(100*sizeof(int));

for(i=0; i<100; i++)
{
p->a[i] = i;
}

free(p->a);
p->a = NULL;
free(p);
p = NULL;

上述 代码1 和 代码2 可以完成同样的功能,但是谁更好呢?代码一更好!
原因:

1、方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。用户free一次可以释放结构体,但是他不知道结构体里成员也是分配出来的,也需要free释放,我们不能指望用户自己发现这个事。如果我们用代码一这种,结构体内存和成员内存一起只分配一次,返回给用户一个指针,那么用户free一次就可以释放所有内存。
2、有利于访问效率
连续的内存有利于提高访问速度,且减少内存碎片(这点效率没有提升很高,都要用偏移量的加法来寻址)


这期就到这里啦,感谢阅读,我们下期再见
如有错 欢迎提出一起交流
关注周周汪

关注三连么么么哒

这篇关于详解【动态内存管理】的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!