【C深度剖析】内存管理相关细节

楔子

C语言的特色之一是:程序员必须亲自处理内存的分配细节。

大多数C语言实现使用栈(Stack)来保存函数返回地址/栈帧基址、完成函数的参数传递和函数局部变量的存储。然而,在部分极特殊的平台上,使用栈并不能获得最大效率。此时的实现由编译器决定。 如果程序需要在运行的过程中动态分配内存,可以利用堆(Heap)来实现。

基本上C程序的元素存储在内存的时候有3种分配策略:

  • 静态分配

如果一个变量声明为全局变量或者是函数的静态变量,这个变量的存储将使用静态分配方式。静态分配的内存一般会被编译器放在数据段或代码段来存储,具体取决于实现。这样做的前提是,在编译时就必须确定变量的大小。 以IA32的x86平台及gcc编译器为例,全局及静态变量放在数据段的低端;全局及静态常量放在代码段的高端。

  • 自动分配

函数的自动局部变量应该随着函数的返回会自动释放(失效),这个要求在一般的体系中都是利用栈(Stack)来满足的。相比于静态分配,这时候,就不必绝对要求这个变量在编译时就必须确定变量的大小,运行时才决定也不迟,但是C89仍然要求在编译时就要确定,而C99放松了这个限制。但无论是C89还是C99,都不允许一个已经分配的自动变量运行时改变大小。
所以说C函数永远不应该返回一个局部变量的地址。
要指出的是,自动分配也属于动态分配,甚至可以用alloca函数来像分配堆(Heap)一样进行分配,而且释放是自动的。

  • 动态分配

变量的大小在运行时有可能改变,或者虽然单个变量大小不变,变量的数目却有很大弹性,不能静态分配或者自动分配,这时候可以使用堆(Heap)来满足要求。ANSI C定义的堆操作函数是malloc、calloc、realloc和free。
使用堆(Heap)内存将带来额外的开销和风险。


一、什么是动态内存

所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

动态内存的好处是归纳为8个字:用时分配,不用释放

二、为什么要有动态内存

1.我们一般申请空间,都是在全局(不建议大量使用)或者栈区申请,而我们的栈空间是有限的,这时候就需要一个大内存空间来存放数据,那么就需要申请堆空间来支持。

例如:

int main()
{
	char a[1024*1024] = 0;
	return 0;
}

程序直接崩溃:一个char是一个字节,而1024字节=1kb,1024kb = 1Mb,所以小小的1Mb就让程序崩溃了

警告 C6262 函数使用了堆栈的“40000000”个字节: 超过了 /analyze:stacksize ‘16384’。 请考虑将某些数据移到堆中。

2.在应用方面,我们无法预估好程序需要花费多大的空间,我们之前定义的数组,因为语法的约束,我们必须明确的指出其空间的大小,但是我们使用动态内存申请(malloc),应为malloc是函数,而函数是可以传参的,也就意味着,我们可以根据具体的应用场景,对需要的内存大小进行动态计算,进而在传参申请时,提高了灵活性。

三、栈,堆和静态区

C程序动态地址空间分布
在这里插入图片描述
接下来我们在Linux环境下验证

int g_val2; 
int g_val1 = 100;

int main()
{
	printf("code addr: %p\n", main); 
	const char* str = "hello world"; 
	printf("readon only: %p\n", str); 
	printf("init g_val: %p\n", &g_val1); 
	printf("uninit g_val: %p\n", &g_val2); 
	char* p = (char*)malloc(sizeof(char) * 10); 
	printf("heap addr: %p\n", p);
	printf("stack addr: %p\n", &str); 
	printf("stack addr: %p\n", &p); 
	free(p);
	return 0;
}

显示结果:
在这里插入图片描述
分析它是符合这样的地址分布

我们在来验证栈区,堆区是相对而生的

int g_val2; 
int g_val1 = 100;

int main()
{
	printf("code addr: %p\n", main); 
	const char* str = "hello world"; 
	printf("readon only: %p\n", str); 
	printf("init g_val: %p\n", &g_val1); 
	printf("uninit g_val: %p\n", &g_val2); 
	char* p = (char*)malloc(sizeof(char) * 10); 
	char* p1 = (char*)malloc(sizeof(char) * 10);
	char* p2= (char*)malloc(sizeof(char) * 10);


	printf("heap addr: %p\n", p);
	printf("heap addr: %p\n", p1);
	printf("heap addr: %p\n", p2);

	printf("stack addr: %p\n", &str); 
	printf("stack addr: %p\n", &p); 
	printf("stack addr: %p\n", &p1);
	printf("stack addr: %p\n", &p2);

	free(p);
	return 0;
}

显示结果:
在这里插入图片描述
堆区的地址是依次增大的,栈区是逐渐增小的

四、常见的内存错误及对策

1)指针没有指向一块合法的内存

对策:合法是指“能够被用户直接使用的”,但是如果指针有具体的指向(包括野指针),它的合法性我们时无法验证的,这样就要求我们 ① 所有的指针,如果没有被直接使用,必须设置为NULL。②在函数内部,要验证指针的合法性,本质是验证指针!=NULL;

2)为指针分配的内存太小

对策:根据实际应用场景多分配空间。

3)内存分配成功,但并未初始化

对策:变量在定义是都应该初始化(这是一个好的编码规范)

4)内存越界

对策:在越界时是不一定会报错,所以需要我们写代码时要细心,方法:链接

5) 内存泄漏

1.在我们本地编译器下,如果我们一直申请空间不释放,会发生什么,如果程序退出,内存泄漏问题还这吗?

int main ()
{
	while(1){
		int *p = malloc(1024):
	}
	return 0;
}

结果是电脑会变地很卡,内存泄漏不存在。操作系统会把泄漏的空间拿回来,

2.free本质

int main()
{
	char* p = (char*)malloc(sizeof(char) * 10);
	free(p);
	return 0;
}

在释放时实际上要比10字节多,所以申请时不止10字节,在free时我们只是传入堆空间的起始地址,并没有传入要释放多少个字节,那么是如何知道呢?其实多出来的部分中记录了这次申请的详细信息,比如申请多大的空间,这样就解决了这个问题。

3.内存释放的本质是什么?

  1. 在free后p会不会被置空?
int main()
{
	char* p = (char*)malloc(sizeof(char) * 10);
	int i = 0;
	for(;i < 10;i++)
	{
		p[i] = 'i';
	}
	printf("before:%p\n", p);
	free(p);
	printf("after:%p\n", p);

	return 0;
}

显示结果:
before:01424A88
after:01424A88

所以不会置空,那么我们还可以通过这个地址来找到其内容吗?答案肯定不可以。

  1. 那么释放释放的是什么呢?

我们打个比方:你的大脑中创建了一个变量叫做女朋友,这个女朋友的内容是如花,有一天如花和你分手了,也就是取消了二者的对应关系,这时如花已经不是你女朋友了,但是你大脑中会记得她曾经是你的女朋友。在这里也是一样的,你创建了一个空间,用p去存放它,由于free取消了这个关系,虽然p还保存这个地址,但它已经不是你的那个她了。
这一层关系是需要数据来维护的,本质就是把这一个关系数据给清除,那我们有没有必要置为NULL;其实是没有必要,因为你已经访问不到这个空间了,
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zzt.opkk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值