C语言 - 动态内存管理

引言

对于C语言的学习,在计算机中,内存通常分为以下3个区域:栈区、堆区、静态区

char arr[20] = { 0 };

在上述语句中,我们在内存中开辟了20个字节,更准确地说,我们是在内存的栈区中开辟了20个字节,这种开辟方式有如下特点:

  • 当这块空间开辟后,我们无法再修改这块空间的大小,它的大小是固定的
  • 数组在申请这块空间时,数组的长度要确定,如在定义数组时省略长度,会自动按照其初始值的个数来确定数组长度
  • 数组需要的内存在编译时分配

而当我们需要长度可变的空间开辟时,上述方式就无法满足,因此就需要接下来所要介绍的动态内存开辟


动态内存开辟的库函数

malloc函数

函数原型

void* malloc (size_t size);

函数用法

  • size是要开辟的字节数,如果size为0,malloc的行为是标准未定义的,它的行为取决于编译器
  • 当成功开辟时,malloc函数的返回值是已开辟空间的起始地址,这个地址是void*类型的,因此在使用时要根据使用者的需求对其进行强制类型转换
  • 当没有足够的空间使用时,开辟失败,malloc函数的返回值时NULL,因为有可能开辟失败,所以在使用时要对malloc函数的返回值进行检查
#include<stdio.h>
#include<stdlib.h>

int main()
{
	int* p = (int*)malloc(20);

	//若条件判断语句为真,说明malloc开辟失败
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i) = i);
	}
	return 0;
}

实际上上述代码是有问题的,因为当我们开辟动态内存后,会有如下的情况

  • 当程序结束时,动态申请的内存由操作系统自动回收
  • 如果程序没有结束,动态申请的内存不会自动回收,就会造成内存泄漏的问题

试想,当一个程序执行时动态申请的每一块内存都不回收,那么每申请一次,堆区的空间就相当于缺失了对应大小,在这种情况下,若还是继续不停申请,最终可能造成电脑死机的结果

为了避免上述问题,我们就需要使用另一个库函数free

free函数

函数原型

void free (void* ptr);

函数用法

  • 先前通过调用malloc、calloc或realloc分配的内存块被释放,使其可再次用于进一步分配
  • 如果ptr指向的空间不是用上述函数分配的内存块,那么free的行为是标准未定义的
  • 如果ptr是一个NULL指针,那么这个函数什么也不做
  • 由于这个函数没有改变ptr本身的值,因此当动态开辟的空间已经被释放后,ptr仍然指向先前位置(此位置的内存已经被释放掉,还给了操作系统),因此ptr现在就是一个野指针,那么就建议当空间被释放后,就立即将ptr赋NULL
  • 不能使用free释放一块动态开辟内存的一部分,因此如果存放空间的起始位置可能被改变,要提前创建另一个指针变量将起始地址保存
#include<stdio.h>
#include<stdlib.h>

int main()
{
	int* p = (int*)malloc(20);

	//若条件判断语句为真,说明malloc开辟失败
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	free(p);
	p = NULL;

	return 0;
}

如上是malloc开辟一块动态内存最后释放的过程

calloc函数

函数原型

void* calloc (size_t num, size_t size);

函数用法

  • num是要分配的元素数量,size是每个元素的大小
  • 为num个元素的数组分配一块内存,每个元素的长度为size字节,并将所有位初始化为0,有效的结果是分配一个(num*size)字节的内存块
  • 其它特点与malloc相同
#include<stdio.h>
#include<stdlib.h>

int main()
{
	int* p = (int*)calloc(5, sizeof(int));

	//若条件判断语句为真,说明calloc开辟失败
	if (p ==NULL)
	{
		perror("calloc");
		return 1;
	}

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;

	return 0;
}

在引言中我提到过,在栈区、静态区开辟的数组,它的空间大小是无法改变的,而若想开辟一个可改变的空间,就应使用动态内存开辟,而上述函数并没有表现出改变空间大小的特点,而接下来的realloc函数则能实现此目的

realloc函数 

函数原型

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

函数用法

  • ptr是要调整大小的空间的起始地址,size是调整之后的新大小
  • realloc函数的返回值为调整后空间的起始地址,如果返回值为空,那么说明realloc分配内存失败,ptr指向的位置没有被修改
  • realloc函数在调整空间大小的基础上,还会将原空间内存中的数据移动到新空间中
  • 当realloc的参数ptr为NULL,那么realloc的操作就相当于malloc

  realloc函数在调整空间大小时,还分为两种情况

  1. 当原空间之后有足够的空间,就直接在原空间之后追加空间,原空间的数据不发生改变
  2. 当原空间之后没有足够的空间时,就在堆区另找一个大小合适的连续空间来使用,并将原空间的数据依次传递给新空间,这时realloc函数返回的是新空间的起始地址

 

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

int main()
{
	//从堆区申请20个字节的存储空间
	int* p = (int*)malloc(20);

	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		//将前5个元素赋为0,1,2,3,4
		printf("%d ", *(p + i) = i);
	}

	//在原基础上再次向堆区申请20个字节的存储空间
	int* ptr = (int*)realloc(p, 40);

	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	//如果申请成功,就将新空间的起始地址赋给p
	else
	{
		p = ptr;
	}

	//对新申请空间的5个元素赋5,6,7,8,9
	for (i = 5; i < 10; i++)
	{
		printf("%d ", *(p + i) = i);
	}

	free(p);
	p = NULL;

	return 0;
}


柔性数组

在C99中,结构体中最后一个成员变量允许是未知大小的数组,这就被叫作柔性数组

特点

  • 结构体中的柔性数组成员前面必须有至少一个其他成员
  • sizeof计算结构体大小时不包括柔性数组的大小
  • 包含柔性数组成员的结构要用malloc函数进行动态内存分配,分配的内存应该大于sizeof计算的结构大小,根据柔性数组的预期大小来分配
#include<stdio.h>

//struct s1
//{
//	int arr[];	//err
//	int a;
//};

struct s2
{
	int a;
	int arr[];
};


int main()
{
	printf("%zd\n", sizeof(struct s2));	//4

	return 0;
}

柔性数组的优势

1.柔性数组

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

struct s1
{
	int a;
	int arr[];
};

int main()
{
	struct s1* p = (struct s1*)malloc(sizeof(struct s1) + 20);

	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	p->a = 1;

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", p->arr[i] = i);
	}

	//相比之前扩容了20个字节
	struct s1* ptr = (struct s1*)realloc(p, sizeof(struct s1) + 40);

	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		p = ptr;
	}

	for (i = 5; i < 10; i++)
	{
		printf("%d ", p->arr[i] = i);
	}

	free(p);
	p = NULL;

	return 0;
}

2.指针

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

struct s2
{
	int a;
	int* arr;
};

int main()
{
	struct s2* p = (struct s2*)malloc(sizeof(struct s2));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	p->arr = (int*)malloc(20);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	//使用

	//要注意free的顺序,如果下面free(p),那么p->arr就无法找到,会造成内存泄漏
	free(p->arr);
	p->arr = NULL;
	free(p);
	p = NULL;

	return 0;
}

1.由于方式2进行了两次动态内存开辟,因此当要释放空间时,也需要释放两次。而方式1用柔性数组只需要开辟一次空间,在释放空间时只需要将这块空间一次释放即可

因此柔性数组第一个优势是:方便内存释放

2.CPU在处理数据时要先到寄存器中拿,如果在寄存器中拿不到,再到缓存中去拿,如果缓存中也拿不到,就去内存中拿,当访问这个数据时,会将其周围数据加载到寄存器中,在这个数据访问结束需要访问下一个数据时,直接在寄存器中就可以拿到,而从寄存器中读取数据相比于从内存中读取数据的效率要高很多。

再看上面那张图,使用柔性数字开辟的空间在内存中是连续的,而没有使用柔性数组时因为开辟两次,这两次的空间很有可能不是连续的,在访问时就会被使用柔性数组慢

因此柔性数组的第二个优势是:有利于提高访问速度

3.在内存中开辟的空间中间剩余的小的空间被称为内存碎片,这些内存碎片可能不会被很好的利用,而不使用柔性数组的方式开辟两次空间,就有可能造成这样的内存碎片,而柔性数组就不会

因此柔性数组的第三个优势是:有利于减少内存碎片,提高空间利用率

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言-学生信息管理系统是一个基于链表数据结构的学生信息管理系统。链表是一种数据结构,可以存储和管理一系列具有相同类型的数据。在学生信息管理系统中,链表被用来存储和操作学生的基本信息。 该系统主要有以下功能: 1. 添加学生信息:可以添加学生的姓名、学号、性别、年龄等信息,并将该学生的信息节点插入到链表中。 2. 删除学生信息:根据学号或其他关键词查找到对应的学生信息节点,并从链表中删除该节点。 3. 修改学生信息:根据学号或其他关键词查找到对应的学生信息节点,并根据需求修改学生的信息。 4. 查询学生信息:可以根据学号或其他关键词查找到对应的学生信息节点,并显示该学生的详细信息。 5. 遍历学生信息:可以遍历整个链表,显示所有学生的基本信息。 链表的优势在于插入和删除节点的操作比较高效,因为只需要改变节点的指针指向即可,不需要移动其他节点。而数组在插入和删除操作时需要移动其他元素,效率较低。 在实现学生信息管理系统时,可以使用指针来操作链表,通过指针的指向找到链表的某个节点,并进行相应的操作。同时,需要注意对内存的管理,确保动态分配和释放内存的正确性,避免内存泄漏和访问错误。 总之,C语言-学生信息管理系统是一个基于链表数据结构的系统,可以实现学生信息的增删改查等功能。通过灵活运用链表的特点,可以高效地管理学生的基本信息。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值