C语言_动态内存管理

本文详细介绍了C语言中的动态内存管理,包括malloc、calloc、realloc和free函数的使用,以及动态内存错误如NULL解引用、越界访问、非动态内存释放、释放内存一部分和多次释放的解释。此外,文章还探讨了动态内存管理在实际应用中的问题和柔性数组的概念及其特点。
摘要由CSDN通过智能技术生成

目录

1. 为什么存在动态内存管理

2. 动态内存函数介绍

2.1 开辟内存块函数_malloc

2.2 动态内存释放和回收函数_free

2.3 开辟空间初始化元素为0的函数_calloc

2.4 调整动态内存开辟大小的函数_realloc

3. 常见的动态内存错误

3.1 对NULL进行解引用操作

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

3.3 对非动态开辟的内存使用free释放和回收

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

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

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

4. 动态内存管理的应用(经典笔试题)

4.1 题目1

4.2 题目2

4.3 题目3

4.4 题目4

5. C/C++程序的内存开辟:

6. 柔性数组

6.1 什么是柔性数组

6.2 包含柔性数组的结构体大小计算

6.3 柔性数组的功能


1. 为什么存在动态内存管理

截止到目前,我们知道的内存使用方式有:

1. 创建一个变量 

int a=0;//局部变量     存放在 栈区

int g_a=0;//全局变量  存放在 静态区;局部变量和全局变量的区别在于:存放在内存的位置不同;

2. 创建一个数组

int arr[10]={0};

如果是局部的数组,放在栈区;如果是全局的数组,放在静态区

我们知道存放内存的区域有:栈区、静态区、堆区;其中,栈区存放局部变量、函数的形参;静态区存放全局变量和静态变量;本节我们将学习存放在堆区的---动态内存分配

struct S
{
	char name[20];
	int age;
};

int main()
{
	struct S arr[50];
	return 0;
}

struct S arr[50]; 的意思是创建一个结构体变量arr;arr是一个数组,数组含有50个数据,每一个数据都是struct S类型的结构体;

但在实际应用中,假设班级的成员有30个,那么数组arr[50]就会造成浪费空间;假设班级成员有60个,那么数组存放不下班级成员;当然理想的,我们希望班级有多少人,我们数组就设置多少个;但实际中我们知道数组必须设置数字去定义,也可以说我们开辟的空间是固定的;数组在申明的时候,必须指定数组长度,而数组所需要的内存是在编译的时候进行分配的;(通过结构体的大小计算和联合体的大小计算我们知道数组所需要的内存是在编译的时候进行分配的);

因为我们需要的内存在程序运行的时候才知道,所以为了达到内存配置的最佳合理性,也可以说是我们想要多大空间就开辟多大空间,换言之,也可以说,我们想要一个空间就增加一个空间,最大程度上减少空间的浪费;所以我们在此引入动态内存分配;

2. 动态内存函数介绍

2.1 开辟内存块函数_malloc

麦芽唠嗑(哈哈):开辟内存块函数;

函数定义:void *malloc(size_t size );参数size 的意思是告诉我们需要多少个字节;单位:字节;函数返回类型为void* ,也可以说我向系统申请size个字节,指针传参,将首元素的地址传给你;

引用头文件:#include <stdlib.h>  或者 #include <malloc.h>;

#include <errno.h>
#include <stdlib.h>

int main()
{
	int *p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p+i));
		}
	}
	return 0;
}

引用函数头文件;使用动态内存开辟函数malloc开辟10个整型字节大小的空间;所以函数参数为10* sizeof(int);在此如果我们知道要开辟int型,对应为4个字节,参数直接写40也是可以的;因为函数malloc定义void *malloc(size_t size )返回类型为void * ,所以需要用指针来接收,定义 int *来接收,又因为返回类型为void,与int 不兼容,所以强制类型转换为int* ;

函数malloc的返回值有两种情况:第一,如果开辟内存失败,则返回NULL,我们已经学习了strerror打印错误码函数;如果开辟失败,则通过该函数打印错误码类型;函数参数为errno全局变量;引用头文件#include<errno.h>;第二,如果开辟成功,我们尝试给开辟的10个整型空间存放0-9数字并打印出来;

for循环i<10;i++; 将i=0 1 2 ……9的值赋给 *(p+i);*(p+i)的意思是首元素的地址随着 i 的值递增,解引用逐渐找到数组中每个元素;for循环打印*(p+i);最后输出结果:0 1 2 3 4 5 6 7 8 9 

接下来,我们看一下开辟失败的情况:

int main()
{
	int *p = (int*)malloc(INT_MAX);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p+i));
		}
	}
	return 0;
}

这次,我们不再开辟10个整型空间了,而是开辟INT_MAX(最大值);转到函数定义:#define INT_MAX       2147483647    /* maximum (signed) int value */    该数的值为:2147483647;

打印结果为:Not enough space  (没有足够的空间)

2.2 动态内存释放和回收函数_free

通过对malloc函数的学习,我们已经学会了开辟动态内存;但是面临一个新的问题;我们知道内存是一定的,我们开辟内存,总会有内存开辟完的一天;理想的,我们希望开辟完一块内存,供我们使用完成以后能够将该内存还给操作系统,供后续开辟;正如现实中的:好借好还,再借不难;此时,我们引入free函数来将这一理想的想法变为现实;

free:动态内存释放和回收函数;

函数的使用:你想要释放哪部分空间,就把哪部分空间的地址传给我即可;

函数原型:void free(void* ptr); 

头文件:#include <stdlib.h> 或者 #include <malloc.h>;

#include <errno.h>
#include <stdlib.h>

int main()
{
	int *p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p+i));
		}
	}
	free(p);
	return 0;
}

加上free(p)和不加的区别:如果没有free(p),也就是没有主动将开辟的内存还给操作空间;其实按照C语言的逻辑,当程序执行完成以后,也就是return 0以后,开辟的空间也会还给操作系统;原因很简单,如果程序操作完成以后,没有还给操作系统,那么谁还会来写代码,每写一份代码,空间就少一部分,听起来是不是很恐怖;

但是之所以还是要加free(p)去主动释放内存的原因是:如果开辟完这么一块内存以后,我们不只是像上述程序一样,打印出0-9就结束程序,而是在打印完以后还有其他的用处,或者说是程序还要再跑个1-2天(这里是夸张啦),这时候在程序结束之前,这块内存一直会被占用,造成空间的浪费;而无法供其他程序去开辟;所以我们希望在使用完一块内存以后,加上free(p)主动将内存还给操作系统供其他程序去开辟;

	free(p);
	p = NULL;
	return 0;

上述我们讲解了free(p),也就是内存释放和回收函数的实用性;但是依旧面临着一个问题:我们用malloc函数开辟一块内存,用指针p来代表这块内存,精确的说是p代表这块内存的首元素的地址;然后我们通过free函数将p代表的这块内存释放给操作系统;但是p仍然代表这块地址,虽然已经还给了操作系统,但是我们拿到p指针,依旧有能力去调用这块地址;这样会给以后开辟这块空间的程序带来不安全性,所以为了解决这一问题,通常在free函数后,将这块代表地址的指针p赋值给NULL;通过以往的学习,我们知道通常情况下,是不会随便调用空指针NULL的;

小结:

malloc函数:

1. 如果开辟成功,则返回一个指向开辟空间的指针;

2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查--if进行判断

3. 返回值的类型是void * ,所以malloc函数并不知道开辟空间的类型,也就是给定参数时通过sizeof(类型)进行开辟空间的字节传输,具体在使用的时候使用者自己进行决定;开辟什么类型的空间,就通过什么类型的空间去接收;

4. 如果参数size 为0,malloc的行为是标准的未定义的,取决于编译器;

free函数:

1. 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的;也就是说,如果使用free函数,那么这块空间一定是通过动态开辟的空间;

2. 如果参数ptr是NULL指针,那么函数什么类型都不做;

2.3 开辟空间初始化元素为0的函数_calloc

看呀唠嗑(哈哈):calloc,开辟一块空间并且初始化元素为0;

函数原型:void *calloc(size_t num,size_t size);参数为元素的个数、每一个元素的长度。单位是字节;

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

头文件:#include <stdlib.h> 或者 #include <malloc.h>

初始化元素0的意思如下程序所示: 输出结果:0 0 0 0 0 0 0 0 0 0   

#include <errno.h>
#include <stdlib.h>

int main()
{
	int *p= (int *)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

所以在具体的使用上:如果需要对开辟的空间初始化为0,就使用calloc函数;如果不需要对开辟的空间初始化为0,而只是开辟一块空间使用,则使用malloc函数;

2.4 调整动态内存开辟大小的函数_realloc

瑞雅唠嗑(哈哈):realloc,调整动态开辟内存的大小;有时候我们会发现开辟的内存空间太小了,而有的时候我们又会发现开辟的内存过大,会造成空间的浪费。为了合理的安排开辟空间的大小,我们引入realloc函数;

函数原型:void *realloc(void* ptr,size_t size);参数 ptr 是要调整的内存地址,size 调整之后新的大小;

返回值:返回值为调整之后新的内存的起始位置;

注意事项:

1. realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到的空间;

2. realloc函数返回值的问题:当我需要的空间不足时,我需要追加空间,realloc函数追加空间时,分两种情况:第一、原有空间的后面有足够大的空间够开辟而满足操作要求的,我直接在原有地址的后方开辟空间,返回值为原有地址的首元素地址;第二、我原有空间的后方没有足够大的空间供开辟使用,这种情况下,操作系统会从新给你开辟一块新的空间,该空间的大小是realloc函数参数size的大小,也就是你需要的目标空间的大小;等同于在原有空间上追加空间,而且原有空间上的数据会转移到新空间相应的地址上,返回值为新空间首元素的地址;

这么说可能有些抽象,我们拿个例子来具体说明:

假设我原有空间上放着1 2 3 4 5,定义指针p指向首元素1的地址,这时候我觉得空间太小,想要通过realloc函数进行开辟空间到10,如果在1 2 3 4 5 后方还有5个整型的空间,那么我直接在1 2 3 4 5 后面开辟空间,返回值为p;

如果1 2 3 4 5后方的空间不足以存放5个整型的数据,操作系统会直接再开辟一个能存放下10个整型的空间,等价于在原有的1 2 3 4 5 后面开辟了5个整型的空间。并且新的空间的前5个元素依旧为1 2 3 4 5 ,返回值为新空间的首元素1所对应的地址;所以返回值的P可能会发生变化,这是正常的情况。

3. 需要用一个新的变量来接收realloc函数的返回值;

4. realloc函数开辟的空间,依旧需要使用free函数释放和回收空间。

5. int *p=realloc(NULL,40);的表达等价于malloc(40);  因为调整动态内存是在NULL空指针的基础之上进行调整的,所以等价于新开辟40个字节长度的内存;

总结就是:1. 如果p指向的空间之后有足够的内存空间可以追加,则直接追加,后返回p;

                  2. 如果p指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域开辟一块满足需求的空间,并且把原来内存中的数据拷贝过来,释放旧的内存地址,最后返回新开辟的内存空间地址;

 realloc函数的使用:

#include <errno.h>
#include <stdlib.h>

int main()
{
	int *p = (int *)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;
		}
	}
	int *p2 = (int*)realloc(p, 40);
	int i = 0;
	for (i = 5; i < 10; i++)
	{
		*(p2 + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2 + i));
	}
	return 0;
}// 0 1 2 3 4 5 6 7 8 9

3. 常见的动态内存错误

3.1 对NULL进行解引用操作

 我们在使用malloc函数进行动态内存开辟时,一定要判断malloc函数是否开辟成功,如果开辟失败,那么返回值就是NULL;则在for循环过程中,*(p+i)会进行NULL的解引用操作,这是错误的;

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

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

我使用malloc函数去开辟5个整型大小的空间,然后判断是否开辟成功,如果开辟失败,我直接返回0;如果开辟成功的话,我定义 i , i<10;注意:i<10 在这里是越界访问,因为开辟空间的大小只有5个整型,而在for循环中,我定义的i<10  ,超过了我开辟的空间大小,所以会产生错误:越界访问;

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

3.3 对非动态开辟的内存使用free释放和回收

free函数的使用,必须建立在内存空间是动态开辟的基础之上;如果我直接定义个int a=0;定义一个整型或者一个数组,这种情况下是不能使用free函数进行内存释放的;a是在栈区上存放的,而动态开辟的内存在是在堆区上存放的;

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

	free(p);
	p = NULL;
	return 0;
}

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

free函数释放动态内存一部分的意思是: 首先我们通过malloc函数开辟10个整型空间的内存,malloc函数开辟空间需要进行判断是否开辟成功,如果未开辟成功,return 0;直接结束程序;如果开辟成功,我们进入for循环,*p++ = i 的意思是通过for循环 i<10 i++  将 i 分别赋值0 1 2 3 4 5 6 7 8 9 ,然后将0 1 2 3 4 5 6 7 8 9 分别传给指针p ,解引用拿到对应指针p所对应的地址,后置++,接收完i 的赋值以后,地址向后偏移一位;

此时需要注意,当for循环结束以后,指针p指向的是i=9 多对应的地址,而不再是开辟空间的首元素所对应的地址;因此free释放指针p所对应的空间,已不再是我们最初开辟的空间,而是释放*p++ 以后的空间,所以会出现错误:free释放动态开辟内存的一部分

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

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

 对同一块内存的多次释放这种错误是比较好理解的;当我们的代码比较长,通常正确的编程习惯是开辟一块空间以后,当我们使用完这块空间,我们需要free释放这一块空间,让这块空间供其他操作使用;当代吗比较长时,循环开辟的空间比较多时,我们有时会忘记已经释放了空间,写程序时会出现free(p);------ free(p);会造成同一块空间的多次释放;

为了避免这种错误:

1. 我们开辟完一块空间以后,使用完这块空间,马上就对这块空间进行释放;保持良好的编程习惯;

2. free(p)以后,立刻对p赋值为NULL,则后续的free(p)会建立在p是NULL的基础之上,没有任何意义;

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

	free(p);
	return 0;
}

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

动态开辟内存而没有释放内存就会产生内存泄漏的现象;只是开辟内存,而没有回收内存;

举个简单的例子:

以下程序会源源不断的开辟内存,这时候我们查看我们电脑的运行内存,会发现运行内存会源源不断的进行开辟,而没有释放内存;通常我们的电脑会自带寄存器保护功能, 所以内存开辟达到一个峰值的时候,就会停止开辟内存;

int main()
{
	while (1)
	{
		malloc(1);
	}
}

总结:动态开辟的内存一定要释放,并且要正确的释放;

4. 动态内存管理的应用(经典笔试题)

4.1 题目1

运行Test函数会有什么样的结果?

1. 运行代码程序会出现崩溃的现象

2. 运行代码程序会出现程序泄露的现象

首先,printf(str)等价于printf(“%s\n”,str);打印出来的结果是一样的;其次,程序调用GetMemory函数进行动态空间开辟,但是没有进行free空间释放,所以会出现内存泄露的现象;最后,程序调用Test函数,会将NULL空指针给到str,然后程序调用GetMemory函数进行空间开辟,GetMemory函数传参传的是str,而不是&str,所以传过去的是整个str的内容,也就是NULL,而不是str所对应的地址;在GetMemory函数中,开辟动态内存强制类型转换为char类型,存放在一块新开辟的地址上,p代表这么新开辟的内存,但是当返回到GetMemory函数以后,函数中存放的还是NULL,原本的空间p会丢失,也可以说代表这块空间地址的指针p会丢失(p是GetMemory函数的形参,只在GetMemory函数的内部有效,当返回到Test内以后,p会丢失其地址),所以strcpy字符串拷贝会崩溃,因为str是NULL,想要把hello world拷贝到NULL上,NULL是没有具体意义的;最终该程序会崩溃;

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

想要正确的打印hello world ,需要对上述程序进行修改;有两种修改方式。 

第一、GetMemory函数传参传地址过去,因为NULL存在*str中,所以用二级指针**p接收,将开辟的空间放在*p指针,*p是代表具体地址的,所以str中存放的是新开辟的空间,而不是NULL;

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

第二、 我们首先要了解之所以strcpy字符串拷贝函数会崩溃,是因为NULL没有具体意义,所以只要把GetMemory函数中的地址p找到即可;故定义GetMemory函数返回值为p,把p返回GetMemory函数,返回值为char* ,我们最终是想要把hello world拷贝到str上,所以我们把返回值p赋值给str,即可正确打印出hello world;

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

4.2 题目2

运行Test函数会有什么样的结果?

1. 非法访问地址,程序崩溃;输出结果:随机值;

之所以会出现非法访问地址的错误,是因为局部变量只在定义的函数中生效,离开所定义的函数会变得无效;虽然GetMemory函数有返回值p,但是hello world是我们定义在栈区的;通过过去的学习,我们知道存储的数据会存放在堆区、栈区和静态区,我们返回的p是返回栈区的地址,栈空间上的数据不同于堆区上的数据,栈区的数据在访问完成以后,会自动的还给操作系统,所以当我们调用一次GetMemory函数以后,p地址存放的数据可能会发生更改,所以打印str会出现随机值;这种典型的错误也称作返回栈区地址的问题;所以不要轻易的返回栈区的地址;

这种错误出现的情况是:主函数调用函数接收返回值;这时候一定要擦亮眼睛看清楚,调用的函数(也就是下述程序中的GetMemory函数)中的变量究竟是定义的整型,又或者是数组中的哪种,如果是动态开辟的空间,需要观察是否是第一种错误,局部变量运行后丢失;又或者是定义的数组或整型,返回栈区的地址;这时又需要注意

1.  如果int a=0;我在程序之前加上static int a=0;static函数修饰会增大函数的生命周期,离开函数,a的值会保留上一次调用函数以后a的值,所以这时候返回p,是可以打印出hello world的;

2. 如果int *ptr=malloc(100);return ptr;开辟的数据是存放在堆区的,如果没有主动free释放空间,程序会默认return 0以后释放空间,所以此时返回ptr主函数是可以接收到动态开辟空间的地址的;也是可以打印hello world的;

再次强调:该程序是说明不要轻易返回栈区的地址;但是加上static以后,数据会存放在静态区;我从来没有说过静态区返回的问题,所以注意区分。

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

修改方法一、

我们需要明白,出现错误的根源在哪里;因为程序中的字符串是字符串常量,如果不主动回收,是可以存在整个程序周期的;我们用char型指针p接收字符串常量,此时字符串常量可以通过p指针来指向;所以返回p可以打印出hello world;p可以存在整个程序周期;

字符串常量:字符串常量是用“双撇号”括起来的多个字符的序列,字符串本质上是多个字符组成的字符数组。在每一个字符串常量的结尾,系统都会自动加一个字符’\0’作为该字符串的“结束标志符”,系统据此判断字符串是否结束。

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

修改方法二、

上面已经说过,不要轻易返回栈区的地址,但是如果通过static扩大变量的生命周期,使其可以存在整个程序周期,就可以使得返回值的生命周期变成整个程序的周期;所以在定义的常量字符串前加上static就可以打印出hello world;

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

4.3 题目3

运行Test函数会有什么样的结果?

1. 忘记释放动态开辟的内存,导致内存泄漏;

输出结果:hello

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

改进:

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

4.4 题目4

运行Test函数会有什么样的结果?

输出结果:world

虽然该程序可以打印出world,但是依旧有很大的问题;首先free函数释放以后,并没有把str主动置为NULL,这时,只要拿到str指针,就可以得到str所对应的地址;且str一定不是NULL,所以下面的if判断毫无意义,直接会进行字符串拷贝,world会直接覆盖hello;

如果在free(str)函数后加上str=NULL,将str赋值为NULL,那么就不会进入if的判断体中,最终的结果是NULL(空);

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

5. C/C++程序的内存开辟:

通过以往的学习,我们已经了解过了栈区、堆区、静态区;

栈区是用来存放局部变量的;堆区用来存放动态开辟的内存;数据段就是静态区,用来存放静态变量和全局变量;代码段是用来存放只读的变量,像图中的字符串常量,是不可以更改的;内核空间用户是不能进行读写的;我们只能在内核空间之外的空间上进行操作;

内核空间简单来说就是我们买电脑或者手机,乃至U盘之类的具有存储数据功能的产品时,打开其存储区,会发现我们买回来的不管是电脑或者手机,先天内存就不是我们买的时候所说的128G、256G等等,而是相比于买的要小10-20G,这些内存是内核空间;

 注意:

1. 对于局部变量、临时变量、函数形参一般是在栈区上创建,它们的生命周期一般是从进入函数到离开函数,一般不会存在整个函数周期;

2. 对于堆区上存放的malloc、realloc、calloc函数,它们的生命周期一般是整个程序,它们的动态内存回收一般是通过主动的free回收或者程序结束,也就是return 0完成以后,操作系统回收;

3. 对于在静态区的全局变量和静态变量,这里包括在代码段的只读程序(常量字符串等),它们的生命周期都是整个程序,也就是return 0 完成以后;

4. static 一般用来修饰局部变量,扩大其生命周期,使得原本只有函数体内部的生命周期变成整个程序的生命周期,也就解释了为什么static修饰的局部变量不会因循环体的改变而改变;

5. 栈区:在执行函数时,函数内的局部变量的存储单元都可以在栈上创建,函数存储结束时,这些存储单元自动释放;栈内存分配的效率高,但是内存容量有限;主要存放局部变量、函数参数、返回数据、返回地址;

6. 堆区:堆区一般通过free释放和回收,也可以说是通过程序员分配释放;否则通过操作系统回收;分配方式类似于链表

7. 静态区:存放全局变量、局部变量;程序结束后由操作系统释放;

8. 代码段:存放函数体(类成员函数和全局变量)的二进制代码;

6. 柔性数组

6.1 什么是柔性数组

结构中的最后一个元素允许是未知大小的数组,这称作柔性数组成员;柔性的意思是这里数组的大小是可以调整的;

正如下述程序:柔性数组就是结构体中的最后一个成员变量允许是未知大小的数组;通常我们定义数组时,数组的大小通常需要明确规定,但柔性数组的最后一个成员的大小可以是未知的;这里需要注意arr[] 和 arr[0] 的意思是一样的,数组的大小都是未知的;

struct S
{
	int a;
	int arr[];//未知大小
	//int arr[0];//未知大小
};
int main()
{
	struct S s;
	return 0;

}

6.2 包含柔性数组的结构体大小计算

我们已经在以往学习过如何计算结构体的大小,以及遵循的对齐规则; 但是下述程序是包含了柔性数组的结构体,其大小打印的结果是4;

这里我们需要理解,在计算包含柔性数组的结构体时,柔性数组的大小并不会计入到结构体的总大小中;所以最终结构体大小仍然是整型的大小,也就是4个字节;

struct S
{
	int a;
	int arr[];//未知大小
	//int arr[0];//未知大小
};
int main()
{
	struct S s;
	printf("%d\n", sizeof(s));
	return 0;

}// 4

6.3 柔性数组的功能

柔性数组之所以存在是有其特殊的功能的;柔性数组可以自由的控制数组的大小;也可以说柔性数组的大小是程序员人为来控制的,想要多大的空间可以自己进行设置;

struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	if (ps == NULL)
	{
		return 0;
	}
	else
	{
		int i = 0;
		for (i = 0; i < 5; i++)
		{
			ps->arr[i] = i;
		}
	}
	struct S* ptr = realloc(ps, 44);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	int i = 0;
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	free(ps);
	ps = NULL;
	return 0;
}

代码的意思是:struct S创建结构体,内部含有一个柔性数组;然后用malloc函数动态开辟一个5个字节大小+原结构体大小的动态空间。注意需要判断动态空间是否开辟成功,如果成功,通过结构体指针->将第一个到第五个元素赋值为0 1 2 3 4 ,通过realloc函数将动态开辟的空间拓展为10个字节大小+原结构体大小,并且判断动态空间是否开辟成功,若开辟成功,则将指向新拓展开辟空间的指针赋值给指向原动态开辟的空间,并且将第五个到第十个元素赋值为5 6 7 8 9,此时打印0-9元素,因为已将指向新拓展开辟的空间的指针赋值给指向原动态开辟的空间,所以可以直接操作指向原动态开辟的空间的指针进行打印;同时这也是柔性数组可以实现的功能;

以下的代码也可以灵活的控制arr的大小;

struct S
{
	int n;
	int* arr;//注意是int* arr,保证后续可以使用指针是调用数组
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));//malloc函数返回类型void*,所以用指针来接收;
	ps->arr = malloc(5 * sizeof(int));//人为的通过malloc函数给结构体成员变量arr 5个整型空间的大小;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	int *ptr = realloc(ps->arr, 10 * sizeof(int));//人为的将ps指针指向的动态空间大小拓展为10个字节的大小
	if (ptr != NULL)//判断是否空间开辟成功
	{
		ps->arr = ptr;
	}
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	free(ps->arr);//两次都是通过malloc开辟的,所以需要释放两次空间;
	ps->arr = NULL;//将释放的空间赋值为NULL;
    free(ps);
	ps= NULL;
	return 0;
}

两种方法都可以开辟可大可小的空间,区别在于:通过柔性数组开辟的空间是一体的,通过指针ps控制;一体的意思是说我开辟空间时:struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));是将结构体的大小和所要开辟空间的大小sizeof(struct S) + 5 * sizeof(int)放在一起的,所以开辟的空间是一体的;而通过malloc动态开辟的空间是分开创建的,第一次通过指针ps,第二次通过ptr,虽然整个大小是一样的空间,但是开辟的过程是不一样的;

柔性数组的好处:

1. 用柔性数组在控制arr可大可小的过程中,开辟的空间:整个空间是一块的,也可以说结构体的大小和想要开辟空间的大小是在一个malloc中进行的,使用malloc函数的次数较少;所以相比于动态开辟的空间,释放空间的次数要少,相对要犯的错误也会少很多;

2. 柔性数组控制arr可大可小开辟的空间相对整齐,而动态开辟的空间控制arr可大可小时,空间的排布相对凌乱;所以柔性数组的写法可以加快访问速度;

柔性数组的特点:

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

2. sizeof返回的这种结构大小不包括柔性数组的内存。

3. 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小;

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值