动态内存分配

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

我们已经学过的申请内存的方法有两种
1.int a = 0;//直接创建变量进行申请
2.char arr[20] = {0};//用数组申请内存
不过上述的两种方式有两个特点:
空间开辟大小都是固定的
数组在声明的必须指定数组长度,数组的大小一旦确定后,就无法改变
但是有一些程序它需要的空间是在程序运行时才能知道的,所以就加入了动态内存分配让程序员自己申请和释放空间,使得代码更加灵活

二.malloc、calloc、realloc、free函数

上述所有的函数都是存放在<stdlib.h>头文件中的

1.malloc

malloc函数就是用来动态内存分配的函数,其函数原型如下:

void* malloc(size_t num);

malloc只需要你传送一个 你想要开辟的空间的大小 作为参数

1.如果开辟成功,返回该空间的起始地址;如果开辟失败,则返回NULL,所以在使用malloc的时候一定要做检查
2.由于malloc函数的设计者也不知道你要开辟什么类型的空间,所以必须由你自己来设计返回值的类型,例如:

	//         把这一块空间变成你想要的类型
	int* ptr = (int*)malloc(sizeof(int)*10);
	//                      开辟10个int大小的空间(40字节)
	//检查是否开辟失败
	assert(ptr != NULL);
	

3.如果你向malloc传递的参数为0,那这是标准未定义的,由编译器决定

在使用动态内存开辟的空间时,最好检查该空间是否开辟失败
assert函数包含在<assert.h>头文件中,它需要接收一个表达式作为参数

2.calloc

calloc与malloc一样都是用来动态内存分配的函数,其函数原型如下:

void* calloc(size_t num,size_t size);

代码的意思是开辟num个大小为size的空间

只不过calloc与malloc唯一的区别就是calloc会把开辟的空间的每个字节都初始化为0
举个栗子:

int main()
{
	//malloc开辟
	int* ptr_i1 = (int*)malloc(sizeof(int) * 10);
	//检查是否开辟失败
	assert(ptr_i1 != NULL);
	
	for (int k = 0; k < 10; k++)
	{
		printf("%d ", ptr_i1[k]);
	}

	//calloc开辟
	int* ptr_i2 = (int*)calloc(10,sizeof(int));
	//检查是否开辟失败
	assert(ptr_i2 != NULL);
	
	putchar('\n');
	
	for (int k = 0; k < 10; k++)
	{
		printf("%d ", ptr_i2[k]);
	}
	
	free(ptr_i1);
	ptr_i1 = NULL;
	free(ptr_i2);
	ptr_i2 = NULL;
	
	return 0;
}

在这里插入图片描述

3.realloc

realloc是用来调整开辟后的空间的大小的,原型如下:

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

ptr是你要调整的那块空间的地址,size是重新调整后的大小

如果size的大小,小于原来空间的大小,则是减少原来动态开辟的空间;如果大于原来空间的大小,则是扩大原来动态开辟的空间
举个例子:

int main()
{
	//malloc开辟40个字节的空间
	int* ptr_i1 = (int*)malloc(sizeof(int) * 10);
	//检查是否开辟失败
	assert(ptr_i1 != NULL);
	
	//代码...
	

	//检查是否为空
	assert(ptr_i1 != NULL);
	
	//发现40个字节不够用了
	//扩充40个字节,变为80个字节
	int* ptr_i2 = (int*)realloc(ptr_i1, sizeof(int) * 20);

	return 0;
}

而开辟大于原来空间的内存又分为两种情况:
1.如果向后扩充的空间没有被其他数据占用,这也是正常的情况
在这里插入图片描述
2.向后扩充的空间被其他数据所占用,从下图可以看到realloc扩充的空间与内存中其他数据所占用的空间重合了
在这里插入图片描述

4.free

free函数与前面的函数正好相反,它是用来释放动态内存开辟的空间的。
原型如下:

void free(void* ptr);

它只需要一个指针告诉该函数需要释放的空间在哪

1.如果给它传递NULL,那么free函数什么都不做
2.如果给free函数传递的参数不是动态内存开辟的,那么该函数的行为是未定义的
int main()
{
	//开辟
	int* p = (int*)malloc(sizeof(int) * 10);
	//检查是否开辟失败
	assert(p != NULL);
	
	//释放
	free(p);
	//问题来了
	//需要把指针p赋值为空指针码?
	return 0;
}

回答:
最好是把p赋值为空指针,因为在free后,指针p指向的空间已经被释放了,所以这块空间已经没有办法使用了,但是p还保存着这块空间的地址,如果你后面对p进行检查并使用,那是不是就非法访问了?!但如果你给p赋值为NULL,那么后面对他进行检查和使用的时候就知道它是空指针了
所以应这样写

int main()
{
	//开辟
	int* p = (int*)malloc(sizeof(int) * 10);
	
	//检查是否开辟失败
		assert(p != NULL);
		
	//释放
	free(p);
	p = NULL;
	return 0;
}

我们知道动态内存管理开辟的空间也是需要进行释放的,一般有两种方法
一是程序结束后由操作系统释放(小程序可以,大程序不行)
二是程序员主动使用free函数进行释放
[链接]动态分配内存,不释放,程序退出后会被系统回收吗

三.常见错误

1.free释放非动态内存开辟的空间

int main()
{
	int a = 0;
	int* p = &a;
	free(p);
	return 0;
}

2.内存泄漏(动态开辟空间未释放)

int main()
{
	int* ptr = (int*)malloc(40);
	assert(ptr != NULL);
	

	//代码...


	return 0;
}

3.对动态内存多次释放

int main()
{
	int* ptr = (int*)malloc(40);
	assert(ptr != NULL);

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

4.对NULL进行解引用操作(对开辟失败的动态内存操作)

int main()
{
	int* ptr = (int*)malloc(40);
	//不进行检查
	
	*ptr = 20;//如果malloc开辟失败就会返回NULL

	return 0;
}

5.对部分动态内存进行释放

int main()
{
	int* ptr = (int*)malloc(40);
	assert(ptr != NULL);
	for (int i = 0; i < 5; i++)
	{
		*ptr = i + 1;
		printf("%d ", *ptr++);
	}
	//此时ptr指向第六块空间,已不再是指向整块起始位置
	free(ptr);
	ptr = NULL;
	return 0;
}

6.对动态内存越界访问

int main()
{
	int* ptr = (int*)malloc(20);
	assert(ptr != NULL);
	for (int i = 0; i < 10; i++)
	{
		ptr[i] = i + 1;
		//i==5时就已经越界
		printf("%d ", ptr[i]);
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

四.柔性数组

在结构体中有这样一个东西,如果该结构体在最后一个成员前有大于等于1个成员(也就是该结构体内至少有两个元素),那么这最后一个成员就可以是一个不指定大小的数组,其名为柔性数组
柔性数组不占用内存空间,而数组名本身不占用空间,它只是一个偏移量,它代表了一个不可修改的地址常量

//柔性数组
struct stu
{
	//必须含至少一个其他成员
	char n;
	//这就是柔性数组成员
	int arr[];
};
int main()
{
	//                                    结构成员的空间        为柔性数组开辟的空间
	struct stu* ptr = (struct stu*)malloc(sizeof(struct stu) + 40);
	assert(ptr != NULL);
	ptr->n = 'A';
	printf("%c\n", ptr->n);
	for (int i = 0; i < 10; i++)
	{
		ptr->arr[i] = i;
		printf("%d ", ptr->arr[i]);
	}
	free(ptr);
	ptr = NULL;

	return ;

还有一种方法可以模拟柔性数组

struct stu
{
	char n;
	//把数组换成指针
	int* p;
};
int main()
{
	
	struct stu* ptr = (struct stu*)malloc(sizeof(struct stu));
	//这里的这个指针其实也可以换成,下面这种,直接给成员开辟空间
	//struct stu s = { 0 };
	//s.ptr = (int*)malloc(40);
	assert(ptr != NULL);
	ptr->n = 'A';
	printf("%c\n", ptr->n);
	ptr->p = (int*)malloc(sizeof(int)*10);

	for (int i = 0; i < 10; i++)
	{
		ptr->p[i] = i;
		printf("%d ", ptr->p[i]);
	}
	//这里顺序一定要对,要先释放成员,再释放指针变量
	free(ptr->p);
	free(ptr);
	ptr->p = NULL;
	ptr = NULL;
	
	return 0;
}

但是使用柔性数组有下面这样几个函数:
1.方便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
2.这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)

五.C/C++内存区域划分

在这里插入图片描述

1.栈区(stack)
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元将自动释放。栈 内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限。栈区主要用来存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2.堆区(heap)
堆区内的空间一般由程序员分配释放,若程序员不释放,程序结束后则可能由操作系统回收。分配方式类似于链表
3.数据段(静态区)
用于存储全局变量和静态数据,程序结束后由操作系统回收
4.代码段
存放函数体(类成员函数和全局函数)的二进制代码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值