C语言:动态内存管理(动态内存+柔性数组讲解)

目录

1. 为什么存在动态内存分配

2. 动态内存函数的介绍

2.1 malloc和free

malloc

 free

2.2 calloc

2.3 realloc

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

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

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

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

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

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

4. C/C++程序的内存开辟

5. 柔性数组        

 6.1 柔性数组的特点:

用sizeof计算带柔性数组结构体的大小 ​

6.2 柔性数组的使用

 给柔性数组扩容

扩容方法:

 扩容例子:

6.3 柔性数组的优势

柔性数组的好处


1. 为什么存在动态内存分配

       以下是已经熟知的内存开辟的方法有:

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

 但是上述的开辟空间的方式有两个特点:

        1. 空间开辟大小是固定的。

        2. 数组在申明的时候,必须指定数组的长度,它需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。

 

2. 动态内存函数的介绍

2.1 malloc和free

malloc

C语言提供了一个动态内存的开辟的函数:malloc

 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

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

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

3.返回值的类型是 void* (返回值为指针),所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(强制类型转换)。

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

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请空间
	int* ptr = (int*)malloc(40);//记录起始位置以便free释放空间
	return 0;
}

 

 

 上图只是做示范,在使用过程中直接用唯一开辟地址进行判断空指针是错误行为,可能会导致free函数释放空间时从遍历后的位数向后释放(释放只能释放整个申请空间即需要起始地址)

正确写法:另申请一个空间存储申请空间首元素地址

 free

free用于手动释放申请空间所占的内存

当我们不手动释放动态申请空间的时候:

1.如果程序结束,动态申请的空间由操作系统自动回收

2.若申请空间存在无限循环中(程序不结束)若不手动释放内存,将导致内存占用一直增加,导致内存泄漏 。

 1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

 2.如果参数 ptr 是NULL指针,则函数什么事都不做。

 

 当释放空间后若不赋值NULL,访问ptr时访问的是已经被释放空间的地址,造成该指针为野指针

2.2 calloc

 calloc 函数也用来动态内存分配

 

 1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0(自动初始化)

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

所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。 

2.3 realloc

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整

memblock 是要调整的内存地址

size 调整之后新大小

返回值为调整之后的内存起始位置。

这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

realloc(NULL,20)==malloc(20);
当传入空指针时,相当于malloc

 

 开辟空间的规则:在扩容时,将原空间中元素拷贝到新扩容后的空间并将原空间free(释放),在p中存放新地址

原空间后面可能存放着其他开辟的空间,如果直接在原空间后面扩容,会导致扩容失败

 正确写法:

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

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

 

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

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

 局部变量不需要手动释放,程序生命周期结束自动释放

若手动释放则系统报错

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

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

系统报错

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

4. C/C++程序的内存开辟

 C/C++程序内存分配的几个区域:

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。

2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。

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

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。

5. 柔性数组        

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

上图写法有些编译器会报错无法编译

可以改成:

 6.1 柔性数组的特点:

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

用sizeof计算带柔性数组结构体的大小 

 由sizeof 返回的结构大小不包括柔性数组的内存 ,该结构体大小为int num即4字节

6.2 柔性数组的使用

 给柔性数组扩容

扩容方法:

 扩容例子:
#include <stdio.h>
#include <stdlib.h>

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

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

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

int main()
{
	struct S3* ps = (struct S3*)malloc(sizeof(struct S3) + 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]);
	}
	//扩容
	struct S3* ptr=(struct S3*)realloc(ps, sizeof(struct S3) + 80);
	//判断是否扩容成功
	if (ptr == NULL)
	{
		perror("reallic\n");
		return 1;
	}
	else
	{
		ps = ptr;
	}
	//初始化扩容后数组	
	for (i = 10;i < 20;i++)
	{
		ps->arr[i] = i;
	}
	for (i = 10;i < 20;i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//释放
	free(ps);
	ps = NULL;	

	return 0;	
}

 当然不用柔性数组也可以实现扩容:
 

#include <stdio.h>
#include <stdlib.h>
struct S4
{
	int num;
	int* arr;
};

int main()
{
	struct S4* ps = (struct S4*)malloc(sizeof(struct S4));
	if (ps == NULL)
	{
		return 1;
	}
	ps->arr = (int*)malloc(40);
	if (ps->arr == NULL)
	{
		free(ps);
		ps = NULL;
		return 1;
	}

	return 0;
}

既然不用柔性数组也可以扩容,那柔性数组具有什么优势呢

6.3 柔性数组的优势

由6.2扩容例子:(名为代码段1,代码段2)

 两种方法进行比较

1.从代码两上看,明显使用柔性数组效率更高

2.内存释放柔性数组也只需要一次

3.第二种方法(不使用柔性数组)存在安全隐患


柔性数组的好处


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

第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

代码段1的开辟内存方式:

 代码段2:

 减少内存碎片:

 未被使用的空间不能在被很好的利用即为内存碎片,内存使利用率下降。

直接用malloc创建空间可能造成内存碎片增多。

如果用柔性数组一次malloc一次就可以开辟好空间,提高内存利用率。

就到这啦!感谢阅读,创造不易,希望能给上免费的赞和收藏

将持续带来优质文章哦!

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值