C语言——动态内存管理(malloc, calloc, realloc, free, 柔性数组详解)

C语言——动态内存管理

1. 为什么需要动态内存管理

我们以往定义数组,都是这么定义的:

int nums[10] = {0};

以这种方式开辟空间有两个特点:

  1. 空间开辟的大小是固定的
  2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配

因此就导致了这样一个现象:我们无法在后续的过程中修改数组的大小,这是一个十分麻烦的事情

而为了解决这个问题,我们就需要学习动态内存开辟了

2. 动态内存函数的介绍

注:需要头文件<stdlib.h>

需要知道,和静态开辟空间不一样,计算机是在堆上开辟的动态空间

2.1 malloc

void* malloc (size_t size);

这个函数向内存申请一块大小为size字节的连续可用的空间,并返回指向这块空间的指针

  • 如果开辟成功,则返回一个指向这块空间的指针
  • 如果开辟失败,则返回一个空指针(NULL),因此我们一定要对malloc的返回值作有效性的判断
  • 返回值为void *,因此当我们用指针变量接受这个返回值时,我们要将这个返回值强制转换为需要的类型
  • 如果参数size为0,malloc的行为是标准未定义的,

例如:

#include<stdio.h>
#include<stdlib.h>
int main()
{
    //向内存申请40个字节的空间,并将返回的指针强制转换成int*型,并将其赋予指针nums
    int *nums = (int*)malloc(sizeof(int) * 10);	

    //检验返回值的有效性
    if (nums == NULL)
    {
        perror("malloc");
        return 1;
    }

    //循环打印nums指向空间的值
    for (int i = 0; i < 10; i++)
        printf("%d\n", *(nums + i));
    
    free(nums);
    nums = NULL;
    
    return 0;
}

output:

-842150451
-842150451
-842150451
-842150451
-842150451
-842150451
-842150451
-842150451
-842150451
-842150451
  • 这说明,malloc申请到空间后,是不会对该空间初始化的

2.2 free

需要注意:凡是动态申请的内存,除非整个程序结束,申请的内存是不会主动归还给系统的,为了避免内存泄漏,我们应该使用函数free来将申请的内存释放

void free (void* ptr);

free函数用来释放动态开辟的内存

  • 如果参数``ptr指向的空间不是动态开辟的,那free`函数的行为是未定义的
  • 如果参数ptr是NULL指针,则函数什么事都不做
  • 正常释放后,ptr指向不明,称为野指针,因此,应该置为空(NULL)

例如:

#include<stdio.h>
#include<stdlib.h>

int main()
{
    //向内存申请40个字节的空间,并将返回的指针强制转换成int*型,并将其赋予指针nums
    int *nums = (int*)malloc(sizeof(int) * 10);	

    //检验返回值的有效性
    if (nums == NULL)
    {
        perror("malloc");
        return 1;
    }

    free(nums);	//释放ptr所指向的动态内存
    nums = NULL;	//将野指针置空
    
    return 0;
}

2.3calloc

void* calloc (size_t num, size_t size);
  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数malloc的区别只在于calloc会在返回指针之前把申请的空间的每个字节初始化为0

例如:

##include<stdio.h>
#include<stdlib.h>
int main()
{    
	//申请10个大小为4的空间,并让指针nums指向它
    int* nums = (int*)calloc(10, sizeof(int));

    //判断返回值的有效性
    if(nums == NULL)
    {
        perror("calloc");
        return 1;
    }

    //打印nums指向空间的元素
    for (int i = 0; i < 10; i++)
        printf("%d\n", *(nums + i));
    
    
    free(nums);
    nums = NULL;
    
    return 0;
}

output:

0
0
0
0
0
0
0
0
0
0

2.4 realloc

所谓的动态内存管理,“内存管理”我们好像已经会了,那这个“动”又是怎么做到的呢?我们前面所学的malloc, calloc好像并不能让申请的内存动起来呀。

要想实现对内存的增加或减小,就需要我们的函数realloc

void* realloc (void* ptr, size_t size);
  • ptr是要调整的内存地址

    • 如果ptr不为空,那么就会修改ptr所指向空间的大小
    • 如果ptr为空,那么就和malloc的功能相似,会直接返回一个指向大小为size字节空间的指针
  • size为调整之后的大小

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

  • 扩容后的空间不会被初始化

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

  • ealloc在调整内存空间时存在两种情况:

    • 情况一:原有空间之后有足够大的空间,那么就在原有的地方增容,并返回原来的起始地址

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-18eQPqE6-1689339767174)(C:/Users/HUASHUO/AppData/Roaming/Typora/typora-user-images/image-20230714151133794.png)]

    • 情况二:原有空间之后没有足够大的空间,那么就在合适的地方重新开辟一块大小为size的空间,将原来空间的数据拷贝到新空间,再释放掉原来的空间,最后再返回新空间的起始地址

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3AeLIzI-1689339767175)(C:/Users/HUASHUO/AppData/Roaming/Typora/typora-user-images/image-20230714151338596.png)]

  • 如果调整失败,就会返回空指针,为了考虑到这种情况,我们应该避免以下的代码:

//error example

#include<stdio.h>
#include<stdlib.h>

int main()
{
	int* nums = (int*)malloc(10 * sizeof(int));
	if (nums == NULL)
	{
		perror("malloc");
		return 1;
	}

    //如果开辟失败,那么nums就成了空指针,前面nums管理的40个字节的空间就找不到了,这样就造成了内存泄漏
	nums = (int*)realloc(nums, 20 * sizeof(int));
	if (nums == NULL)
	{
		perror("realloc");
		return 1;
	}
	
	free(nums);
	nums = NULL;

	return 0;
}
  • 正确的方式应该是这样的:
//right example

#include<stdio.h>
#include<stdlib.h>

int main()
{
	int* nums = (int*)malloc(10 * sizeof(int));
	if (nums == NULL)
	{
		perror("malloc");
		return 1;
	}

    //先用一个中间变量temp接受
	int* temp = (int*)realloc(nums, 20 * sizeof(int));
	if (temp == NULL)
	{
		perror("realloc");
		return 1;
	}
	
    //当temp有效时,再用nums接受
    nums = temp;
    
	free(nums);
	nums = NULL;

	return 0;
}

最后,再对realloc的具体使用举个例子:

#include<stdio.h>
#include<stdlib.h>

int main()
{
    //先动态开辟40个字节的内存
	int* nums = (int*)malloc(10 * sizeof(int));
	if (nums == NULL)
	{
		perror("malloc");
		return 1;
	}

    //将这块空间初始化为0
	memset(nums, 0, 10 * sizeof(int));

    //将这块空间扩容到60个字节,并先用中间变量temp接受
	int* temp = (int*)realloc(nums, 15 * sizeof(int));
	if (temp == NULL)
	{
		perror("realloc");
		return 1;
	}

    //确定temp有效后再用nums指向temp
	nums = temp;

    //打印扩容后空间的数据
	for (int i = 0; i < 15; i++)
	{
		printf("%d\n", nums[i]);
	}
	
    //释放内存
	free(nums);
	nums = NULL;

	return 0;
}

output:

0
0
0
0
0
0
0
0
0
0
-842150451
-842150451
-842150451
-842150451
-842150451

3. 常见的关于动态内存开辟的错误

3.1 对NULL指针的解引用操作

void test()
{
    int *p = (int *)malloc(sizeof(int));
    *p = 20;	//如果p为空指针,就会有问题,一定先要检查返回指针的有效性
    free(p)
}

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

void test()
{
    int *p = (int *)malloc(10 * sizeof(int));
    if(NULL == p)
    {
        perror("malloc");
        return 1;
	}
    for(int i = 0; i <= 10; i++)
        *(p + i) = i;	//当i是10的时候就会越界访问
    
    free(p);
}

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

void test()
{
    int nums[10] = {0};
    free(nums);
}

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

void test()
{
    int *p = (int *)malloc(10 * sizeof(int));
    if(NULL == p)
    {
        perror("malloc");
        return 1;
	}
    
    free(p);
    free(p);
}

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

void test()
{
    int *p = (int *)malloc(10 * sizeof(int));
    if(NULL == p)
    {
        perror("malloc");
        return 1;
	}
    for(int i = 0; i < 10; i++)
        *(p + i) = i;	
}

4. 柔性数组(flexible array)

C99中,结构体中的最后一个元素允许是位置大小的数组,这就叫做柔性数组

例如:

typedef struct ST
{
	int i;
	int a[0];	//柔性数组成员,也可以写成 a[];
}ST;

4.1 柔性数组的特点

  1. 柔性数组前至少有一个其他成员

  2. sizeof返回的结构体大小不包含结构中柔性数组的内存,例如对于上面的代码:

    printf("%d\n",sizeof(ST));
    

    output:

    4
    
  3. 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小,同样,也可以用realloc进行增容,例如:

    #include<stdio.h>
    #include<stdlib.h>
    
    typedef struct ST
    {
    	int i;
    	int a[];	//柔性数组成员
    }ST;
    
    int main()
    {
    	ST *st1 = (ST*)malloc(sizeof(ST) + sizeof(int) * 10);	//向内存申请大小为结构大小加10个整型的内存空间
    	if (NULL == st1)
    	{
    		perror("malloc");
    		return 1;
    	}
    
    	st1->i = 10;
    
        //给空间赋值
    	for (int i = 0; i < 10; i++)
    		(st1->a)[i] = i + 1;
    	//打印空间元素
    	for (int i = 0; i < 10; i++)
    		printf("%d\n", (st1->a)[i]);
        
        //增容,将内存扩大5个int型
        ST* temp = (ST*)realloc(st1, sizeof(ST) + sizeof(int) * 15);
    	if (NULL == temp)
    	{
    		perror("realloc");
    		return 1;
    	}
        
        st1 = temp;
    
        //打印空间数据
    	for (int i = 0; i < 15; i++)
    		printf("%d\n", (st1->a)[i]);
    
        //释放动态内存
    	free(st1);
    	st1 = NULL;
    
    	return 0;
    }
    

    由上面的分析我们可以看到,我们完全可以在结构体里面创建一个整形指针(其他类型也可以),然后对其进行动态内存开辟就可以完全替代柔性数组的功能,因此柔性数组这一功能并不常用,我们仅作了解即可

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Forward♞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值