【ONE·C || 动态内存管理】

总言

  C语言:动态内存管理介绍。


  
  

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

  1)、内存开辟方式:
  下示为常见变量开辟方式:

int n = 2;//全局变量:在静态区开辟四个字节

int main()
{
	int val = 20;//局部变量:在栈空间上开辟四个字节
	char arr[10] = { 0 };//局部变量:在栈空间上开辟10个字节的连续空间
	return 0;
}

  对于上述变量,可以知道的是: 这些变量开辟出的空间大小是固定的。即使是数组,为了在编译时确定其所需要的内存,也会固定数组长度。
  这样就存在一个问题,有时候我们需要的内存空间大小只有在程序运行的时候才能知道,直接固定内存大小,会遇到匹配不当的现象,比如内存空间过大浪费,或内存空间过小不够。
  因此才有了即将了解的动态内存开辟。
  
  
  2)、内存空间区域分配简单引入:
在这里插入图片描述

  
  
  

2、动态内存函数介绍

在这里插入图片描述

2.1、malloc、free

2.1.1、malloc函数

  1)、malloc函数介绍:

  相关函数:malloc
在这里插入图片描述
  说明:
  1、这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
  2、如果开辟成功,则返回一个指向开辟好空间的指针。如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  3、返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  4、如果参数 size0malloc的行为是标准是未定义的,取决于编译器。
  
  
  2)、使用演示:

#include<stdlib.h>

int main()
{
	int* ptr = (int*)malloc(40);//注意此处40的单位是字节,类型为int*,那么指针每次能访问4字节空间。
	if (ptr == NULL)//因为有申请失败的可能性,因此需要做检查。
	{
		perror("malloc");
		return 1;
	}
	//申请到的动态内存空间的相关使用举例:
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = i;
	}
	return 0;
}

在这里插入图片描述

  
  

2.1.2、free函数

  1)、free函数介绍:

  相关函数链接:free
在这里插入图片描述

  说明:
  1、函数free是用来释放和回收态内存的。
  2、如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  3、如果参数 ptrNULL指针,则函数什么事都不做。

	int* np = NULL;
	free(np);//合法的,free参数可为空

  4、当我们不释放动态申请的内存空间时,如果程序结束,动态申请的内存由操作系统自动回收,如果程序不结束,动态内存是不会自动回收的,就会形成内存泄漏的问题。
  
  
  2)、使用演示:

#include<stdlib.h>

int main()
{
	int* ptr = (int*)malloc(40);//注意此处40的单位是字节,类型为int*,那么指针每次能访问4字节空间。
	int* p = ptr;
	if (ptr == NULL)//因为有申请失败的可能性,因此需要做检查。
	{
		perror("malloc");
		return 1;
	}
	//申请到的动态内存空间的相关使用举例:
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = i;
	}
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

  注意事项:
  1、动态申请空间后,不需要时要记得使用free函数释放;
  2、free(p);实际释放的是p指针指向的那块动态内存空间,即将内存使用权收回,但实际上p仍旧指向该内存空间,ptr虽然释放了对应内存空间,但它还能指向该地址空间,即p成为了野指针,因此需要为其赋值为空。
在这里插入图片描述

  
  
  3)、动态内存申请失败举例:

#include<stdlib.h>

int main()
{
	int* ptr = (int*)malloc(INT_MAX);//申请整型最大值这么多的内存空间
	int* p = ptr;
	if (ptr == NULL)//检查。
	{
		perror("malloc");
		return 1;
	}
	//申请到的动态内存空间的相关使用举例:
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = i;
	}
	//释放空间
	free(p);
	p = NULL;

	return 0;
}

在这里插入图片描述

  
  
  

2.2、calloc、realloc

2.2.1、calloc函数

  1)、calloc函数介绍:

  相关函数链接:calloc
在这里插入图片描述

  说明:
  1、colloc函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  2、与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
  
  
  2)、使用演示:

int main()
{
	//等价空间大小:
	//int* ptr = (int*)malloc(40);
	//int* ptr = (int*)malloc(sizeof(int)*10);
	int* ptr = (int*)calloc(10, sizeof(int));//区别在于初始化为零

	if (ptr == NULL)//检查
	{
		perror("malloc");
		return 1;
	}

	//使用举例:
	int* p = ptr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}

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

	return 0;
}

在这里插入图片描述

  
  
  

2.2.2、realloc函数

  相关函数链接:realloc
在这里插入图片描述

  说明:
  1、有时为了合理设计内存空间,我们会对内存的大小做灵活的调整(申请的空间太小、太大)。那么 realloc 函数就可以做到对动态开辟内存大小的调整。
  2、realloc函数的两个参数,ptr 是要调整的内存地址,size 是调整之后新大小。
  3、返回值为调整之后的内存起始位置。根据不同情况,该函数会在调整原内存空间大小的基础上,将原来内存中的数据移动到新的空间。

  4、关于扩容时新空间选择说明:
  情形一: 当内存空间足够,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
在这里插入图片描述
  
  情形二: 原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
  
在这里插入图片描述

  
  
  2)、使用演示:

int main()
{
	//预备处理:
	int* ptr = (int*)calloc(10, sizeof(int));
	if (ptr == NULL)//检查
	{
		perror("malloc");
		return 1;
	}
	printf("%p: ", ptr);

	//使用举例:
	int* p = ptr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}


	//重新扩容:
	int* tmp = (int*)realloc(ptr, 20 * sizeof(int));
	if (tmp != NULL)//检查:防止出现扩容失败,直接赋值的话会导致原先ptr改变指向引起内存泄漏
	{
		ptr = tmp;
	}
	printf("\n%p: ", tmp);

	//使用举例:
	p = ptr;
	for (int i = 0; i < 20; i++)
	{
		printf("%d ", p[i]);
	}

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

	return 0;
}

在这里插入图片描述

  
  
  
  

3、常见的动态内存错误

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

	int* p = (int*)malloc(1000);
	for (int i = 0; i < 250; i++)
	{
		p[i] = i;//error
		printf("%d ", *(p+i));//error
	}

  存在问题:若动态申请空间失败,返回空指针NULL,*NULL是非法的。
  
  解决方案:对动态申请后的相关返回值进行判空。

	int* p = (int*)malloc(1000);
	if (p == NULL)
	{
		//……
		return 1;
	}
	for (int i = 0; i < 250; i++)
	{
		p[i] = i;
		printf("%d ", *(p+i));
	}

  
  

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

	int* p = (int*)malloc(1000);
	if (p == NULL)
	{
		//……
		return 1;
	}
	for (int i = 0; i <=250; i++)//error
	{
		p[i] = i;
		printf("%d ", p[i]);
	}

  存在问题:i <=250实际越界。

  解决方案:对内存边界主动检查。

	int* p = (int*)malloc(1000);
	if (p == NULL)
	{
		//……
		return 1;
	}
	for (int i = 0; i <250; i++)
	{
		p[i] = i;
		printf("%d ", p[i]);
	}

  
  

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

	int a = 10;
	int* p = &a;
	free(p);
	p = NULL;

在这里插入图片描述

  
  
  
  

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

int main()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	//释放空间
	free(p);//error
	p = NULL;

	return 0;
}

  存在问题:
在这里插入图片描述
  
  

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

int main()
{
	int* p = malloc(100);
	if (p == NULL)
		return 1;
	
	free(p);
	//....
	free(p);//error

	p = NULL;

	return 0;
}

在这里插入图片描述

  
  

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

void test()
{
	int* p = malloc(100);
	//……
}

int main()
{
	test();
	//.....
	while (1)
	{
		;
	}

	return 0;
}

  存在问题:test函数结束,栈销毁,局部变量p被销毁,没有释放动态空间,会导致内存泄露。

  解决方案:动态开辟的空间一定要释放,并且正确释放。

void test()
{
	int* p = malloc(100);
	//……
	free(p);
	p = NULL;
}

int main()
{
	test();
	//.....
	while (1)
	{
		;
	}

	return 0;
}

  
  
  

4、习题演练

4.1、题一

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

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

  问题分析:

void GetMemory(char* p)//问题一
{
	p = (char*)malloc(100);//问题二
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);//问题一
	strcpy(str, "hello world");//问题三
	printf(str);
}

  
  1、GetMemory(str);char* p 尽管str是指针,但指针变量仍旧属于变量的范畴,在函数传参时,对该指针变量而言这种写法属于值传递,除非说传递它的地址,即二级指指针(&str),这样才是址传递。
  2、p = (char*)malloc(100); 因值传递,临时变量p出了函数被销毁,在GetMemory中申请到的动态空间由于没有指向它的指针,最终无法找到,也无法释放,形成内存泄漏。
  3、strcpy(str, "hello world"); 因上述问题导致str指针未做任何改变,仍旧是空指针,对空指针的解引用会导致程序崩溃。
  
  延伸:printf(str);,这种写法正确吗?

int main()
{
	char* p = "Why We Sleep\n";//该指针指向字符首元素地址
	printf("Why We Sleep\n");

	char arr[] = "Why We Sleep\n";//数组名表示数组首元素地址
	printf(arr);
	//printf打印字符串,只需要传递首元素地址
	return 0;
}

在这里插入图片描述

  
  解决方案:

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}

在这里插入图片描述

  
  
  

4.2、题二·返回栈空间地址、返回栈空间变量

  1)、基本例题说明:
  问:请问运行Test 函数会有什么样的结果?

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

  
  问题分析:在GetMemory函数中创建数组并将其地址返回给str指针。由于GetMemory函数结束后,其内部局部变量销毁,对应内存空间又还给系统本身,str得到的是一个没有访问权限的地址,则属于野指针。对其打印会出现未定义行为。
在这里插入图片描述

  
  
  2)、返回栈空间地址讲解:

int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();

	printf("%d\n", *p);
	printf("%d\n", *p);

	return 0;
}

  第一次printf能打印成功有时属于侥幸,对应的空间中数据还没有被清理/删除/覆盖,当打印第二次时就能发现问题,这样因为相同空间被其它函数栈帧使用,故其中数据也不复从前。
  需要注意,即使第一次输出结果正确,但不代表它没问题,本质上这种写法就是非法的。举例子:偷窃,虽然结果是没被抓/发现,但这一行为本身不符立定的道德规范。
在这里插入图片描述  
  需要区分:返回栈空间的地址是有问题的,但返回栈空间的变量是可行的

int* test()//error:返回栈空间地址
{
	int a = 10;//创建一个int变量
	return &a;//返回的是该变量的地址:传址返回
}
int test()//right:返回栈空间变量
{
	int a = 10;
	int*p=&a;
	return *p;
}
int main()
{
	int n=test();
	return 0;
}

  
  延伸问题:
  如下:此提错误之处是访问野指针。非返回栈空间地址。

int* f2(void)
{
	int* ptr;//创建一个指针变量
	*ptr = 10;//error
	return ptr;//返回的是该指针变量,传值返回
}

  
  

4.3、题三

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

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

  
  问题分析:
  1、该代码能正常打印值。
  2、但没有释放动态申请的空间。
  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);
	free(str);//要记得释放动态空间
	str = NULL;
}

  
  
  

4.4、题四

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

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

  
  问题分析:
  1、瑕疵:没有进行判空操作,当malloc申请失败时,第一次字符拷贝传参可能存在问题。
  2、问题:free释放动态内存空间,str仍然指向对应地址,只是该地址被系统收回,后续使用时str为野指针,虽能访问该内存空间,但其操作为非法访问。

  正确写法应在释放内存后令str为空指针。此处的一种错误改法是把free(str)放到if循环之后,这样虽然没有野指针的问题,但其逻辑不能自洽(即先用了strcpy拷贝,再用if语句判断:既然已经投入使用又何必多此一举添个判空,既然判空为什么不在刚完成空间申请后立马做判空检查)

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

  
  
  
  

5、柔性数组

5.1、什么是柔性数组

  C99 中,结构体最后一个元素允许是未知大小的数组,这就叫做柔性数组成员
  以下为结构体中,柔性数组的两种写法:

struct S1
{
	int num;
	double d;
	int arr[];//柔性数组成员
};


struct S2
{
	int num;
	double d;
	int arr[0];//柔性数组成员
	//此处的0不是指数组元素,而是特指柔性数组
};

  
  

5.2、柔性数组的特点和使用

5.2.1、柔性数组的特点

struct S1
{
	int num;
	int arr[0];//柔性数组成员
};

  1、结构中的柔性数组成员前面必须至少一个其他成员。
  2、sizeof 返回的这种结构大小不包括柔性数组的内存。
  
  演示如下:
  sizeof计算结构体,不包含柔性数组的大小,正因此,为了保障结构体大小存在,柔性数组不能单独存在结构体中。
在这里插入图片描述

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

	struct S1* ps1 = (struct S1*)malloc(sizeof(struct S1));//error
	struct S1* ps2 = (struct S1*)malloc(sizeof(struct S1) + 40);

  这种写法错误是因为我们只考虑了num的空间,正确写法是要加上柔性数组需要的空间大小。
  
  
  

5.2.2、柔性数组的使用

int main()
{
	struct S1* ps = (struct S1*)malloc(sizeof(struct S1)+40);
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}

	ps->num = 100;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;//柔性数组成员:赋值
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);//柔性数组成员:打印
	}
	printf("\n");

	//扩容
	struct S1* ptr = (struct S1*)realloc(ps, sizeof(struct S1)+80);
	if (ptr == NULL)
	{
		perror("realloc\n");
		return 1;
	}
	else
	{
		ps = ptr;
	}

	for (i = 10; i < 20; i++)//为后面扩容的值初始化
	{
		ps->arr[i] = i;
	}

	for (i = 0; i < 20; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");

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

	return 0;
}

在这里插入图片描述
  
  
  

5.3、柔性数组的优势

5.3.1、一个对比

  以下为两种结构体数组的写法:
  使用柔性数组:

struct S1
{
	int num;
	int arr[0];//柔性数组成员
};

int main()
{
	struct S1* ps = (struct S1*)malloc(sizeof(struct S1)+40);
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}

	//使用
	//……


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

	return 0;
}

  使用指针:

struct S2
{
	int num;
	int* arr;//指针 
};

int main()
{
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->arr = (int*)malloc(sizeof(int) * 10);
	if (ps->arr == NULL)
	{
		perror("malloc");
		free(ps);
		ps = NULL;
		return 1;
	}

	//使用
	//……

	//释放
	free(ps->arr);
	free(ps);

	return 0;
}

  
  可以看到,上述两种写法都能达到相同效果,那么我们为什么不直接使用指针,而要再学习一个柔性数组呢?
  以下为二者的区别:
在这里插入图片描述
  对第一种,数组是结构体中的一员,内存空间相衔接。
  对第二种,可取代柔性数组得到相同结果,但其内存空间是单独开辟的,存在不衔接的情况。

  
  

5.3.2、柔性数组的优势

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

在这里插入图片描述

  
  
  
  
  
  
  
  

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值