动态内存管理

目录

一、动态内存管理简介

1、为什么要动态内存分配

二、有关动态内存开辟的四个重要函数

2.1、malloc

2.2、free

2.3、calloc

2.4、relloc

​编辑

三、常见的动态内存的错误

3.1、对空指针的解引用错误

3.2、对动态内存空间的越界访问

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

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

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

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

四、柔性数组

4.1、柔性数组的概念及其特点

五、总结C/C++中程序内存区域划分​


一、动态内存管理简介

1、为什么要动态内存分配

我们在创建变量的过程的就是向操作系统申请内存空间的过程,其中最常见的有两种,第一种就是简单的创建一个变量

如:

int a=10;

这里就是向操作系统申请四个字节大小的内存空间,又比如说:

char a= 'w';

这里就是向操作系统申请一个字节大小的内存空间,第二种就是向内存申请一块连续的空间,一次性开辟一片空间,即开辟一个数组,比如:

int arr[10];//四十个字节
char str[20];//二十个字节

无论是第一种还是第二种只要开辟好了,就无法再更改内存空间的大小,这样就导致了在使用内存空间时不够灵活,比如说我开辟了一个数组用来存放学生的成绩,但是再考试前我不知道有多少个学生,如果我只开辟了50个整形的大小,实际上又需要计录55个学生的成绩就不够用,如果实际上需要计入45个人的成绩,那么就会浪费内存空间,那能不能由我们自己来开辟内存空间,使得这块内存空间可变,从而进行灵活的使用呢?答案是肯定的,C语言中引入了动态内存开辟,让程序员自己可以申请和释放空间,这样我们就可以灵活的使用我们的内存空间了。

二、有关动态内存开辟的四个重要函数

2.1、malloc

在c语言中提供了一个动态内存开辟的函数,malloc

void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,(其中size指的是要申请的内存空间的大小),并返回指向这块空间的起始地址。使用时一般需要包含头文件<stdlib.h>

如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个 NULL (空)指针,因此malloc的返回值一定要做检查。​
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(一般会在开辟好后,进行强制类型转化成我们想要的类型)。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。​
 

比如说我们想要开辟一块空间来存放五个整形,五个整形就是20个字节的大小,下面我们来实现一下malloc

#include <stdio.h>
#include <stdlib.h>//malloc所需要的头文件
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)//如果开辟失败,malloc就会返回空指针
	{
		perror("malloc");//则报错
		return 1;//提前返回
	}
	//如果开辟成功,则使用空间
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p+i) = i+1;
	}
	return 0;
}

通过调试我们可以看到,malloc确实能够开辟内存空间用于存储变量,那么malloc在向内存中申请的空间到底在哪呢?我们在学习编程的时候大概关注这么几个区:栈区(用来存放局部变量和形式参数),堆区(内存函数申请和操作的空间),静态区(静态变量和全局变量),而malloc开辟的动态内存空间就存放在堆区上。

2.2、free

我们在申请内存空间,完成要做的事之后,我们还需要把我们申请的内存空间还给操作系统(有借有还,再借不难),那应该怎么还呢?c语言中提供了另一个函数叫做free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

其中ptr指的是要释放的函数的起始地址,因为这个函数就是用来释放内存的,所以就不需要放回什么值,故用void

需要注意的是:

free函数是用来释放动态开辟的内存。​
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的(即只能释放动态内存开辟的空间)。​
如果参数 ptr 是NULL指针,则函数什么事都不做。​
malloc和free都声明在 stdlib.h 头文件中。​

就拿上述例子说:

#include <stdio.h>
#include <stdlib.h>//malloc所需要的头文件
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)//如果开辟失败,就会返回空指针
	{
		perror("malloc");//则报错
		return 1;//提前返回
	}
	//如果开辟成功,则使用空间
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p+i) = i+1;
	}
    free(p);
	return 0;
}

那么是不是只要释放完内存之后就完事了呢?我们需要知道的是,free仅仅只是把原来申请的内存空间还给操作系统,我们就没有了使用的权限,但是我们并没有改变指针p的指向,也就是说,p依旧指向的是那一块空间的首地址,但是由于这块空间已经被释放,指针指向一块被释放的空间,这个指针就变成了野指针,所以为了避免野指针的出现,在释放之后我们可以将其置为空指针(相当于把野狗给拴住了),这样才是稳妥的处理方式。

#include <stdio.h>
#include <stdlib.h>//malloc所需要的头文件
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)//如果开辟失败,就会返回空指针
	{
		perror("malloc");//则报错
		return 1;//提前返回
	}
	//如果开辟成功,则使用空间
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p+i) = i+1;
	}
    free(p);//free传的是要释放内存空间的起始地址
    p=NULL;
	return 0;
}

这里再次申明一下,free传的是要释放内存空间的起始地址,所以上述代码中如果我们用free函数时想传的是指针p,而p最开始的时候指向的已经是开辟空间的起始地址,所以我们就不能在更改p的值了,即我们在写循环的时候就不能让p自加,因为单目操作符会改变p的值:

/*
错误写法
int i=-0;
for(i=0;i<5;i++)
{
  *p=i;
   p++;//自加改变了p的值
}
*/
//正确写法
int i=0;
for(i=0;i<5;i++)
{
   *(p+i)=i;
)

2.3、calloc

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

void* calloc (size_t num, size_t size);

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。比如说​,num传的是5,size传的是4个整形的大小,就是给每个元素申请4个字节的大小空间,总共大小就是20个字节,而且申请完之后每个字节的数据内容都是0,所以这个数据取出之后就是0

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0, 而malloc不会,malloc开辟后元素的每个字节初始化的数据是随机值

 calloc能够将开辟后元素的数据初始化为0,而malloc却不可以,但是相比之下,calloc多了一步,速度肯定也就慢一些,两者各有优缺点,选择使用时需要按照实际情况考虑

2.4、realloc

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用系统的内存空间,我们一定会对要使用的内存空间的大小做灵活的调整。而c语言中提供了一个函数,名字叫做realloc,reclloc函数的出现让动态内存管理更加灵活,它的作用就是可以对动态开辟的内存大小做出调整。

 函数原型如下:

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

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

那么,realloc到底是怎样调整空间的呢?其实在开辟空间的时候就必然会有两三种情况:

情况1:原来动态开辟好的空间后面还有空间,此时若我们还想要扩展内存,就直接在原有内存之后直接追加空间即可,并且原来空间的数据不发生变化,然后返回原空间的起始地址就可以了。

情况2:原有空间之后没有足够多的空间时,此时就不能直接追加空间了,那又该怎么扩展呢?

扩展的方法是:在堆区的内存空间中找一块新的内存空间,并且这块新的内存空间要符合大小,它会把原来的数据拷贝到这块空间中,同时它还会释放原来的旧内存空间(所以就不用再释放原来的malloc所开辟的旧空间了),完成后返回的是新内存空间的起始地址。

情况3:调整失败,返回空指针(NULL)。

这里需要申明一下:只有在情况2下,realloc才会将原来的内存空间释放,但是新的内存空间还需要我们手动释放,而其它两种情况需要我们手动释放旧的(原来的)空间,因为它们不会使得realloc重新找一块内存空间

 弄懂原理之后,我们就来实践一下:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//用malloc开辟空间
	int* p = (int*)malloc(sizeof(int));
	if (p == NULL)
	{
		perror("malloc");//如果开辟失败,就报错
		return 1;//并且提前返回
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	//用realloc调整原来的空间,增加大小,变为40个字节
	int* p= (int*)realloc(p, 40);
	free(p);
	p = NULL;
	return 0;
}

 那么这样写是否有问题呢?因为在实际情况中总有realloc调整内存空间失败的情况(发生情况3),而一但realloc开辟失败,就会返回空指针给p(p就变成了空指针),但是p原来还维护着20个字节,p又变成了空,所以原来的20个字节也找不到了,这就会导致不仅没有调整好空间,还把原来的空间也搞没了。所以我们一般不会直接用原来空间的起始地址来接收realloc调整后返回的地址(即不用p来接收),一般情况下我们会用另一个指针(ptr)接收,然后再判断一下这个指针是否为0,再把它赋给原来空间的起始地址(即把ptr赋给p):

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//用malloc开辟空间
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");//如果开辟失败,就报错
		return 1;//并且提前返回
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i+1;
	}
	//用realloc调整原来的空间,增加大小,变为40个字节
	int* ptr = (int*)realloc(p, 40);
	if (ptr != NULL)//如果调整成功
	{
		p = ptr;
		//使用realloc调整后的空间
		int i = 0;
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i+1;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	else//如果调整失败
	{
		perror("realloc");//报错realloc
		//继续使用原来的空间
		for (i = 0; i < 5; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	//无论是否调整成功,释放的都是原来内存的地址
	free(p);
	p = NULL;
	return 0;
}

我们可以看到ptr和p的地址是一样的,也就是说realloc是在原来的空间上追加的,属于情况1,接下来我们来看情况2:

我们把要增加的空间设为足够大,这样它就会找一块新的空间了:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//用malloc开辟空间
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");//如果开辟失败,就报错
		return 1;//并且提前返回
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i+1;
	}
	//用realloc调整原来的空间,增加大小,变为4000个字节
	int* ptr = (int*)realloc(p, 4000);
	if (ptr != NULL)//如果调整成功
	{
		p = ptr;
		//使用realloc调整后的空间
		int i = 0;
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i+1;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	else//如果调整失败
	{
		perror("realloc");//报错realloc
		//继续使用原来的空间
		for (i = 0; i < 5; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	//无论是否调整成功,释放的都是原来内存的地址
	free(p);
	p = NULL;
	return 0;
}

 

 说明reallo找了一块新的空间,并且把原来空间的数据拷贝过来了,并且结束之后需要手动释放的是新的空间,因为原来的空间已经被realloc给释放了。

其实realloc还可以完成和malloc一样的功能,我们已经知道realloc函数有两个参数,要调整的内存地址,以及调整之后的内存大小,如果我们传地址传入的是空指针,它就无法调整空间,就只能找一块新的空间:

realloc(NULL,20);//==malloc(20);

前面三个开辟动态内存空间函数我们说的都是能够开辟的情况,那到底有没有不能开辟的情形呢?其实是有的,只要我们把要开辟的空间 设为足够大,就可以出现这种情况:

#include <stdio.h>
3include <stdlib.h>
int main()
{
	int*p=(int*)malloc(INT_MAX);
	if (p == NULL)
	{
		perror("malloc()");
	}
	return 0;

}

三、常见的动态内存的错误

3.1、对空指针的解引用错误

我们前面说过如果,动态内存开辟存在失败的情况,就会返回空指针,所以在开辟之后我们一般会对其检查,判断一下其是否为空指针再使用,如果不加以判断检查就是用的话,就有可能会对空指针解引用:

所以我们一般会检查一下,看是否为空指针,再对其进行使用:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(INT_MAX);
	if (p == NULL)//如果开辟失败
	{
		perror("malloc()");//报错
		return 1;//提前返回
	}
//如果开辟成功才使用
	*p = 20;
	return 0;
}

 运行结果:

或者我们用assert断言也可以:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
	int* p = (int*)malloc(INT_MAX);
	//if (p == NULL)
	//{
	//	perror("malloc()");
	//	return 1;//提前返回
	//}
	assert(p);
	*p = 20;
	return 0;
}

运行结果: 

3.2、对动态内存空间的越界访问

其实malloc开辟的空间是连续的,这和数组类似,如果对其访问超出了malloc所开辟的空间,就形成了越界访问:

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(即变成了第11个元素)的时候越界访问​
	}
	free(p);
}
int main()
{
	test();
	return 0;
}

因此我们在使用malloc开辟的空间时,也需要像对数组一样对边界进行把控。

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

 顾名思义,就是对不是动态内存开辟的空间进行了释放,我们来看一段代码:

#include <stdio.h>
int main()
{
	int a = 10;
	int* p = &a;
	//使用空间(省略)....


	//释放空间
	free(p);
	p = NULL;
	return 0;
}

这里a是局部变量,它并不在动态内存的区域中(即不在堆上),而是在栈上(也就是不在任何一个动态内存开辟函数所申请的空间上),所以无法被free释放,咱就是说free时专业要对口。

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

 free是从传的地址开始往后释放的,如果传给free的不是这块空间的起始地址,就会导致这块空间不会完全释放(前面还有空间未被释放),其实在前面我们已经举过一个例子,就是在赋值的时候让p自加 ,改变了p的指向,从而导致使用free时没有完全释放开辟的动态内存空间:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	else
	{
		int i = 0;
		for (i; i < 5; i++)
		{
			*p = i + 1;
			p++;//自加会改变p的值
		}
	}
	free(p);// 这里的p已经不是原来申请的内存空间的起始地址
	p = NULL;
	return 0;
}

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

用释放完一块空间后,由于后续代码较多,忘记了前面的释放,又对其进行释放,也会产生错误

#include <stdio.h>
#include <stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放​
}

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

 为了避免这种重复释放,我们可以写个注释提醒后面不要重复释放,也可以在释放之后将其置为空指针,置为空指针后即使进行反复释放也不会出现错误:

#include <stdio.h>
#include <stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	free(p);
    p=NULL;
	free(p);//重复释放​
}

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

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

动态内存开辟忘记释放就会造成内存泄漏的问题,来看:

#include <stdio.h>
#include <stdlib.h>
void test()
 {
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
 }
 
 int main()
 {
 test();
 while(1);//死循环,意为着程序一直在跑
 }

test函数中,malloc开辟了100个字节的空间,然后使用了一个整形大小的空间,之后没有释放就跳出了函数,回到主函数中已经找不到100个字节的空间了,因为把malloc开辟空间的起始地址放在了一个局部变量中,出函数后,这个局部变量就已经被销毁,既然已经销毁了记录起始地址的变量,那肯定就找不到这块内存空间的地址了,既然找不到,后续主函数中也就无法释放,而且后面是while(1)死循环,代码程序一直不停止,所以test函数中malloc开辟的空间想释放都来不及了,因为已经没有机会释放了,而这块内存空间又不使用又没有释放,这块内存未来就已经找不到了,所以就会损失这一块内存空间,造成内存泄漏的问题,忘记释放内存和来不及释放内存都会造成内存空间的泄露,总的来说,内存是一种资源,不用的时候要将其回收。其实呢,malloc/calloc/realloc申请的内存,如果不想使用时可以用free释放,如果没有用free释放,退出程序时也会由操作系统回收,所以说内存空间物理上是不会丢失的,但是这个程序一直不退出的话,那块内存空间又一直占着,即不用也不还给操作系统,别人也就用不上,所以最好的方式就是谁(可能是函数)申请的资源谁(如果是函数就要函数来释放)释放,也就是说malloc和free要成对出现(不过即使malloc和free成对出现也会有内存空间无法释放的情况,比如说前面提前返回了,就没有执行后续释放的程序),即使申请的空间此时还不能释放(后续还要使用),那也要把这块内存空间的地址告诉后期使用这块空间的人,提醒他记得释放。

四、柔性数组

4.1、柔性数组的概念及其特点

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。​
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。​他相比于其它的普通数组,是在初始化时不规定他的大小(C语言中,普通数组在初始化中一定要给定大小)

注:柔性数组需要满足以下要求:

1、结构体中最后一个成员

2、最后一个成员是数组,并且数组没有指定大小

/*第一种写法
struct s
{
	char c;
	int i;
	int arr[]
};
*/
//第二种写法
struct s
{
	char c;
	int i;
	int arr[0]
};

柔性数组还有以下特点:

1、结构中的柔性数组成员前面必须至少一个其他成员。

2、sizeof 返回的这种结构大小不包括柔性数组的内存(因为在定义的时候就没有给定数组的大小,故没有将其纳入计算范围,这就验证了第一点,就是柔性数组前面必须要有其它成员,如果他前面没有成员,那计算结果就是0,也就没有计算的意义了)。

如:

#include <stdio.h>
struct s
{
	int i;
	int arr[0];
};

int main()
{
	printf("%d", sizeof(struct s));
	return 0;
}

 可以看到计算的结果是只包含了int i。

3、包含柔性数组成员的结构要用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。即给柔性数组成员开辟空间的时候不会以普通结构体内置变量(比如说上述结构体的成员int i)一样直接开辟空间到栈上,而是通过malloc来开辟空间到堆上,而malloc开辟空间时调整的是整块空间的大小(不是单独给结构体成员中柔性数组开辟动态内存,这里需要注意),又可以通过relloc任意的调整大小,那么这块数组空间就会随着整块空间的变化而变化,那么这个数组不就灵活可变,柔性了吗?

 现在我们来简单看一下,用malloc开辟好空间,之后对这块空间使用:

#include <stdio.h>
#include <stdlib.h>
struct s
{
	int i;
	int arr[];
};

int main()
{
	//给柔性数组开辟空间
	struct s*ps = (struct s*)malloc(sizeof(struct s) + 5 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->i = 1616;
	int n = 0;
	for (n = 0; n < 5; n++)
	{
		*(ps->arr + n) = n;
	}
	
	free(ptr);
	ps = NULL;
	return 0;
}

调试一下:(补充一下,从这里也可以看出vs中是以小端字节进行存储的)

当我们想要扩大数组的容量时,把要数组的元素个数增加到10时,我们就可以通过realloc来调整:

#include <stdio.h>
#include <stdlib.h>
struct s
{
	int i;
	int arr[];
};

int main()
{
	//给柔性数组开辟空间
	struct s*ps = (struct s*)malloc(sizeof(struct s) + 5 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->i = 1616;//将结构体中的成员i赋值为1616
	int n = 0;
	for (n = 0; n < 5; n++)//将结构体中的成员数组赋值
	{
		*(ps->arr + n) = n;
	}
	//调整空间,把数组的空间增加为10
	struct s* ptr = (struct s*)realloc(ps, sizeof(struct s) + 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//使用.... 
	//释放
	free(ptr);
	ps = NULL;
	return 0;
}

 其实除了通过柔性数组来实现数组的可大可小,还可以通过其他方式来实现,比方说我们实现一下与上述例子逻辑一样的代码:

 说白了,就是换一种方式实现数组的可长可短,新的方案就是我们可以把原来结构体成员中的柔性数组变为指针,在上述例子中,用malloc开辟空间之后结构体成员都在堆上,为了和上述例子模拟的一模一样,所以新的方案也要把所有的数据都放在堆上。即我们要用malloc开辟一块空间,而这块空间就用来复刻原来的结构体 :

struct S* ps = (struct S*)malloc(sizeof(struct S));

 开辟好后的这块空间(就在堆上)由结构体指针变量ptr指向, 而其中的结构体成员arr是一个指针变量,指针又可以指向一块空间,而这块空间也可以通过malloc开辟,既然也是 malloc开辟的空间,自然也就可长可短了。 

#include <stdio.h>
#include <stdlib.h>
struct S
{
	int i;
	int* arr;
};
int main()
{
	// 把结构体放在动态内存开辟的空间上
	struct S* ptr = (struct S*)malloc(sizeof(struct S));
	if (ptr == NULL)
	{
		return 1;//如果开辟失败,则提前返回
	}
	//使结构体成员arr指向一块大小为5个整形的动态内存空间
	ptr->arr = (int*)malloc(sizeof(int) * 5);
	if (ptr->arr == NULL)
	{
		return 1;//如果开辟失败,则提前返回
	}
	// 使用这块空间
	for (int n = 0; n < 5; n++)
	{
		*(ptr->arr + n) = n;
	}
	for (int n = 0; n < 5; n++)//打印1-5
	{
		printf("%d ", *(ptr->arr + n));
	}
	//重新调整arr指向的这块空间的大小,调整到十个整形的大小
	int* ps = (int*)realloc(ptr->arr, sizeof(int) * 10);
	if (ps == NULL)
	{
		return 1;//如果开辟失败,则提前返回
	}
	//如果开辟成功,则把地址赋给arr
	ptr->arr = ps;
	//使用
	for (int n = 5; n < 10; n++)
	{
		*(ptr->arr + n) = n;
	}
	for (int n = 5; n < 10; n++)//打印5-10
	{
		printf("%d ", *(ptr->arr + n));
	}
	//释放
	//注意,结构体指针ptr和结构体成员arr都是指向的是一块动态内存开辟的空间
	//所以都需要释放,但是要先释放arr,再释放ptr,如果先释放ptr的话,arr指向的那块空间就找不到了
	//即由内向外释放
	free(ptr->arr);
	free(ptr);
	ptr->arr = NULL;
	ptr = NULL;
	return 0;
}

通过这种结构体中包含一个指针方式我们也可以实现数组的柔性变换,即让它可大可小。而柔性数组的方案是结构体中包含一个数组,两种方案相比较,其实柔性数组的方案更加,因为柔性数组只需要用一次malloc,然后用一次free,而指针的方式需要两次,而free的顺序我们需要弄明白,一但弄混顺序就容易出错,所以相比之下,柔性数组的方案不容易出错一些,而且malloc开辟的时候总会留下一下空隙,malloc开辟的次数越多,内存之间的空隙(也叫内存碎片)就越多,内存碎片越多,内存的空间利用率也就越低,相反,我们malloc的次数越少,内存碎片也就越少,空间利用率也就越高。

总结一下,柔性数组的方案总共有两个优点:

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。​
第二个好处是:这样有利于访问速度.​
连续的内存有益于提高访问速度(指针方案中的空间可能不是连续的),也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

五、总结C/C++中程序内存区域划分​

C语言中的内存区域大概可以划分为如图所示的几个区域:

 1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时
这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内
存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。​
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(opera system, 即操作系统)回收 。分配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。​
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
这里有几个需要说明的地方:常量字符串和一些常量只可读不可更改,所以存放在代码段中,还有创建的指针变量虽然是存放在栈上,但是可以指向malloc开辟的空间上,即堆上。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值