《C语言初阶进阶完整教程》- 已完结 - 动态内存管理

⭐️本篇博客我要给大家分享一下动态内存管理。希望对大家有所帮助。
⭐️ 博主码云gitee链接:码云主页

目录

前言

一、为什么会有动态内存分配?

二、动态内存函数

        1.malloc

        2.free

        3.calloc

        4.realloc

三、常见错误

        1对NULL指针的解引用

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

        3对非动态内存空间进行free

        4使用free释放动态内存空间的一部分

        5对—个空间进行重复释放

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

 四、动态内存相关的笔试题

        1.笔试一

        2.笔试二

        3.笔试三

        4.笔试四

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

六、柔性数组 

        1.什么是柔性数组?

        2.柔性数组的特点

        3.柔性数组的使用

        4.柔性数组的优势

总结


前言


一、为什么会有动态内存分配?

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

空间开辟大小是固定的。
数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有些时候,我们并不能提前知道需要的空间大小,而部分编译器并不支持变长数组。这时候以数组的方式开辟连续空间的方法就有点不适用了。
其次,全局变量/局部变量是存放在栈区里面的。如果存放的变量太多,就会出现栈溢出的错误

《C语言初阶进阶完整教程》- 已完结 - 动态内存管理_第1张图片

二、动态内存函数

        1.malloc

#include //malloc的头文件
void* malloc (size_t size);

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

#include 
#include 
#include 

int main()
{
	//开辟10个整型的空间
	//int arr[10];
	int* p = (int*)malloc(40);
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
        //这个函数在之前介绍过,用于转义错误代码
		return 0;
	}
	//使用
	int  i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}

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

        2.free

free函数用来释放动态开辟的内存,头文件是;
如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。如果参数ptr是NULL指针,则函数什么事都不做。
free函数不能用来释放栈区里面的空间,栈区的空间由编译器进行自动创建和自动释放
被free之后的指针p指向的空间已经不属于我们的应用程序了。最好在free之后立马把指针置为NULL避免访问野指针。

free(p);
p = NULL;

        3.calloc

void* calloc (size_t num, size_t size);

calloc函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。

与malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0

#include 
#include 
#include 
int main()
{
	//开辟10个整型的空间
	//int arr[10];
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	//使用
	int  i = 0;             
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}

	//释放
	free(p);
	p = NULL;

	return 0;
}

        4.realloc

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

ptr是要调整的内存地址size调整之后新大小
返回值为调整之后的内存起始地址
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

如果开辟失败,返回空指针NULL

         扩容注意事项

原动态内存空间之后有足够空间进行扩容2.原动态内存空间之后无足够空间
如果是第一种情况,realloc函数会在这之后增添空间,分配给ptr指针。原来空间的数据不发生变化。
如果是第二种情况,realloc会找一块新的空间,开辟好后返回给ptr指针,并把原空间里的数据移动到新空间的对应位置。情况2的时候,开辟完新空间之后,会把原来的空间给free掉(只有开辟成功才会释放原来的空间)

《C语言初阶进阶完整教程》- 已完结 - 动态内存管理_第2张图片

#include 
#include 
#include 
int main()
{
	//开辟10个整型的空间
	//int arr[10];
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 0;
	}
	//使用
	int  i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//需要增容 - 80
	int*ptr = (int*)realloc(p, 80);
	if (NULL != ptr)
	{
		p = ptr;
		ptr = NULL;
	}
	//继续使用了
	
	//释放
	free(p);
	p = NULL;

	return 0;
}

realloc(p,80)80指的是开辟之后新的空间大小为80,而不是增加80的空间
realloc函数可能开辟失败,这时候如果将开辟失败的返回值NULL赋值给了原有指针p,就很危险。

采用中间指针变量ptr,先判断realloc函数是否开辟成功,若成功,则赋值给p指针。
赋值给p之后ptr指针就没用了,置为空指针。
最后我们不需要对ptr进行free,因为ptr的指向和p是—样的free的时候,ptr所指向的空间也被free掉了
realloc缩小空间的时候,会把原来空间后面的内容都剔除 

所以realloc函数在使用时,我们要注意其返回值不能直接用原指针接收,如果动态内存空间申请失败,那么原来那块空间也就找不到了,所以我们要创建一个新的指针变量来接收并检查指针是否为空,这样就能保证空间申请失败时元原空间不丢失,下面我们来看一个实例:;

int main()
{
	int *ptr = (int*)malloc(10*sizeof(int));//开辟40个字节大小的连续空间
	//检查空间是否申请成功
	if (ptr == NULL)
	{
		perror("malloc fail:");
		exit(-1);
	}

	//扩展40个字节的空间
	//创建一个临时指针变量接收新的空间地址
	int* tmp = (int*)realloc(ptr, 10 * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc fail:");
		exit(-1);
	}

	ptr = tmp; //扩展成功就把新的空间地址给指向旧的空间的指针变量

	//释放和回收空间
	free(ptr);
	ptr = NULL;

	return 0;
}

三、常见错误

        1对NULL指针的解引用

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

指针ptr未检查是否为空,直接使用造成对NULL进行解引用操作程序直接崩溃了。

INT_MAX是int类型的最大值,可以在头文件limits.h里面查询并使用


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

void test()
{
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)//应该是i<10
 {
 *(p+i) = i;//当i是10的时候越界访问
 }
 free(p);
}

当i = 10时,指针越界访问,程序崩溃。

和数组一样,动态内存空间也是不能越界访问的! 

        3对非动态内存空间进行free

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

程序直接崩溃


        4使用free释放动态内存空间的一部分

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);//p不再指向动态内存的起始位置
}

指针p的内容发生了改变,只释放了一部分的内存空间 。程序用运行起来也是直接崩溃。 

《C语言初阶进阶完整教程》- 已完结 - 动态内存管理_第3张图片

        5对—个空间进行重复释放

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

在free之后立马把p置为空指针,就能避免这个问题 


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

void test()
{
 	int *p = (int*)malloc(100);
 	if(NULL != p)
 	{
 		*p = 20;
 	}
    //没有释放p的空间
}
int main()
{
 	test();
	while(1);
}

一般情况下,谁使用就谁释放。函数里使用就在函数里释放,除非需要传回主函数进行操作。
如果没有传回主函数,也没在函数里进行释放,该指针变量已经被销毁了,无法进行释放操作。
如果主函数里程序没有结束,就造成了内存浪费

 四、动态内存相关的笔试题

        1.笔试一

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

}

我们可以发现在Test函数内部,Getmemory中传了一个一级指针过去,GetMemory这个函数也是用以及指针接收这个参数,相当于值传递,也就是说在GetMemory函数内部,给指针p申请一块空间,ptr是得不到的,ptr的内容不会发生任何改变。所以Test函数内部进行字符串拷贝会由于目标字符串空间不足而导致程序崩溃。而且要记得free释放动态内存申请的空间。

修改后代码: 

void GetMemory(char** p)
{
	*p = (char *)malloc(100);//修改3
}
void Test(void)
{
	char *str = NULL;
	GetMemory(&str);//修改1
	strcpy(str, "hello world");
	printf(str);
	free(str);//修改2
	str = NULL;
}

        2.笔试二

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

我们来一步一步分析一下,首先创建了一个char类型的指针变量,然后用了接收GetMemory函数的返回值。GetMemory函数内部创建了一个字符数组p,返回了首元素地址,与此同时,p指向的空间也被销毁了,Test函数内部的str接收了这个地址,其实str就是一个野指针了,因为它指向的空间已经被销毁了,然后打印的时候进行了访问,这就对野指针进行了访问操作,这是一个很危险的动作,会导致程序乱码。

        3.笔试三

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

这里其实就是发生了内存泄漏。动态内存开辟的空间没有释放。GetMemory函数内部开辟了100个字节大小的空间给了str,str指向的空间在Test函数内部使用后没有及时释放。造成了内存泄漏。 

        4.笔试四

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

str指针申请了一块大小为100个字节的空间,然后进行字符串拷贝,"hello"被拷贝到str指向的空间中,然后str指向的空间被free释放和回收了,由于没有将str置为NULL,str就变成了一个野指针。下面有再一次的对str进行使用,也就造成了野指针的访问,程序同样会崩,这里又再一次告诫我们,被free释放和回收后的指针一定要置空,你面造成不必要的麻烦。

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

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

数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码。

六、柔性数组 

        1.什么是柔性数组?

在C99中,结构体最后一个元素是一个未知大小的数组,这就叫做柔性数组成员

//第一种
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

//第二种
typedef struct st_type
{
	int i;
	int a[];//柔性数组成员
}type_a;

        2.柔性数组的特点

柔性数组成员前面至少包含一个其他成员。
sizeof返回这种结构体大小不包含柔性数组的内存。
包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

        3.柔性数组的使用

typedef struct st
{
	int i;
	int arr[];//柔性数组成员
}st;

int main()
{
	st* p = (st*)malloc(sizeof(st)+sizeof(int)* 10);
	if (p == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//开始使用
	p->i = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}

	//如果不够,继续扩展
	st* tmp = (st*)realloc(p, sizeof(st)+sizeof(int)* 20);
	if (tmp == NULL)
	{
		perror("realloc fail");
		exit(-1);
	}
	else
	{
		p = tmp;
	}
	for (i = 10; i < 20; i++)
	{
		p->arr[i] = i;
	}

	//释放回收空间
	free(p);
	p = NULL;

	return 0;
}

《C语言初阶进阶完整教程》- 已完结 - 动态内存管理_第4张图片        

 指针实现

typedef struct st
{
	int i;
	int* ptr;
}st;

int main()
{
	st* p = (st*)malloc(sizeof(st));//先开辟一个结构体大小的空间
	if (p == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	p->i = 100;
	p->ptr = (int*)malloc(sizeof(int)* p->i);
	if (p->ptr == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int i = 0;
	//使用
	for (i = 0; i < p->i; i++)
	{
		p->ptr[i] = i;
	}

	//扩展空间
	int* tmp = (int*)realloc(p->ptr, sizeof(int)* (p->i+10));
	if (tmp == NULL)
	{
		perror("realloc fail");
		exit(-1);
	}
	else
	{
		p->ptr = tmp;
	}
	for (i = 10; i < 20; i++)
	{
		p->ptr[i] = i;
	}

	//释放空间
	free(p->ptr);
	p->ptr = NULL;
	free(p);
	p = NULL;

	return 0;
}

《C语言初阶进阶完整教程》- 已完结 - 动态内存管理_第5张图片

        4.柔性数组的优势

第一个优势:第一种方法只需要释放一次内存空间,方便内存释放,但第二种方法要进行两次free,不方便释放。
第二个优势:由于空间连续,有利于内存访问,所以访问速度快。连续的内存有益于提高访问速度,也有益于减少内存碎片。内存碎片化会导致右下空间不可用。所以柔性数组还是比较有优势的。


总结

面试常考,要注意使用

你可能感兴趣的:(C语言初阶和进阶,c语言,开发语言,后端,c++)