首页 > 系统相关 >C语言进阶之动态内存管理【概念篇】

C语言进阶之动态内存管理【概念篇】

时间:2024-04-08 18:58:45浏览次数:27  
标签:malloc 进阶 int free C语言 内存 动态内存 空间 开辟

前言:我们知道C语言是一门接触底层的语言,其核心用法之一就是对内存的操作,本篇将就详细介绍C语言中是如何灵活开辟内存空间的以及如何管理使用这些空间等等。

一.为什么要引入动态内存管理 ?

 在C语言中我们目前已经掌握两种开辟内存空间的方式:

1.int data=10;//在栈(stack)空间上开辟4个字节

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

 这样的空间开辟方式有两个特点:

  • 空间开辟的大小是固定的,无法修改。
  • 数组在申请空间的时候,必须指定或间接指定数组的长度,数组空间一旦确定了大小不能调整。

但显然这两种空间开辟方式不能满足我们对空间灵活处理的需求,试想一个案例,例如我们要做一个客户端app,其中需要收集用户信息,但我们事先是无法预料到用户群体的个数的,这样以来我们就不能事先指定一个固定长度的空间,而是需要一种更为灵活的空间开辟方式,以适应现实需求。C语言引入了动态内存开辟的方式,使得程序员自己就可以申请和释放空间,就比较灵活了。

 二.内存的基本划分

在开始讲解动态内存管理之前,我们首先需要搞懂内存是如何划分区域的,只有对内存分区搞懂,我们才能知道数据存储在内存的哪个区域,加深我们的理解。

一个由C/C++编译的程序占用的内存通常分为以下几个部分:栈区,堆区,全局区(静态区),文字常量区,代码区。

 这里我们主要了解栈区,堆区和全局区(静态区):

栈区(stack)
由编译器自动分配释放,主要存储局部变量值,函数参数。

堆区(heap)
允许程序在运行时动态地申请某个大小的内存空间, 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事。

全局区(静态区)
存放全局变量和静态变量。

 具体图示如下:

上图过于复杂的话也可以参考下面这个简要示意图: 

 

这里我们只是粗浅地了解了一下内存中各个区域的作用,实际上远比介绍的复杂的多,为避免冗杂,这里我们还不深入作探讨 ,感兴趣的小伙伴可以自行了解。如图我们不难发现今天我们学习的动态内存管理开辟的空间不同于我们之前的局部变量,函数参数,它是开辟在堆上的。堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于静态区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时可能会由操作系统回收。

三.动态内存管理的具体实现

3.1malloc和free

  1. malloc

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

void* malloc   (size_t size);

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

  • 若开辟成功,则返回一个指向开辟好的空间的指针;
  • 若开辟失败,则返回空指针NULL,由于并不是每次都能成功开辟空间,因此对于malloc函数的返回值一定要作检查;
  • malloc函数类型为void*,所以malloc函数并不知道开辟空间的具体类型,实际在使用的时候需要程序员自己决定;
  • 参数size的单位为字节,表示开辟空间大小,若size为0,malloc的行为是标准是未定义的,取决于编译器,最好不要发生这种情况。

假设要开辟一个20字节的空间,如何实现呢?如下: 

#include<stdio.h>
#include <stdlib.h>
int main()
{
	//开辟空间:20 个字节 - 存放5个整型
	int* ptr = (int*)malloc(20);
	//检查返回值
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;//非正常情况程序结束
	}

	//使用空间
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
//注意不能是*p++=i+1;
//因为这样会改变p指向的位置,则返回值指向的空间就不是开辟的空间地址了
	}

	//释放内存
	free(p);//传递给free函数的是要释放的内存空间的起始地址
	p = NULL;//使用完毕要置空

	return 0;
}

 对于malloc开辟空间时并不能保证每一次都能成功开辟,存在以下三种情况:

对于情况1空间足够则直接正常开辟空间,对于情况2也有自己的解决方式:我愿称之为另辟蹊径,具体实现如下:

情况3则是无法开辟空间,返回一个空指针NULL。 

        2.free

上文提及过,动态申请完的空间使用完一定要释放。所以C语言提供了另外一个函数,专门是用来做动态内存的释放和回收的,函数原型如下:

 void  free   (void* ptr);

  • 若参数ptr指向的空间不是动态开辟的,则free函数的行为是未定义的;
  • 若ptr为空指针NULL,则该函数什么也不做。 

注意:malloc和free都声明在头文件stdlib.h中 。

具体使用如上述案例,其中已经使用了free函数。总之就一句话:有借必有还,再借才不难

3.2calloc和realloc

  1. calloc 

C语言还提供了一个函数calloc,calloc函数也课用于动态内存分配,原型如下:

 void* calloc   (size_t num,size_t size);

  • 该函数的功能是 是为num个大小的size的元素开辟一块空间,并且把空间的每个字节都初始化为0;
  • 该函数与malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0.

举个例子:

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

int main()
{
	//申请一个存储5个整型的20个字节的空间
	int* p = (int*)calloc(5, sizeof(int));
	if (p != NULL)
	{
		int i = 0;
		for (i = 0;i < 5;i++)
		{
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

输出结果:

0 0 0 0 0 

由上可知,如果我们对申请的内存空间的内容要求初始化,那么使用calloc可以很方便地完成任务。

        2.realloc

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

函数原型如下:

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

  • ptr 是要调整的内存地址;
  • size是调整之后内存空间的新大小;
  • 返回值为调整之后的内存的起始位置;
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间;
  • realloc函数在调整空间存在两种情况:

情况一:原有空间后有足够大的空间

情况二:原有空间后没有足够大的空间

对于malloc并非每次开辟空间都是一帆风顺的,realloc亦是如此,但是realloc对这种情况有着自己的处理方式:

情况1:当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间原来空间的数据不发生变化。

情况2:当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。

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

int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr != NULL)
	{
		//业务处理
	}
	else
	{
		perror(malloc);
		return 1;
	}

	//扩充容量
	int* p = NULL;//确保容量成功扩充
	p = realloc(ptr, 1000);
	if (p != NULL)
	{
		ptr = p;
	}
	//业务处理

	free(ptr);
	return 0;
}

 简单总结一下:其实malloc,calloc,realloc申请的空间在不想使用的时候可以用free进行释放,如果没有用free释放,在程序运行结束的时候也会由OS(操作系统)回收的。

但是还是要尽量做到:1.谁(函数)申请的空间谁释放;2.如果不能及时释放,要告诉后来使用的人要记得释放。

 四.柔性数组

很少有人会介绍柔性数组(flexible array)这个概念,但它确实是存在的,C99中,结构体中最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。具体结构如下:

struct type

{

        int i;

        int a[0];//柔性数组成员

};

若是部分编译器报错可以改成: 

struct type

{

        int i;

        int a[];//柔性数组成员

};

 4.1柔性数组的特点

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

eg:

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

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

int main()
{
	printf("%zd\n", sizeof(type_a));//4
	return 0;
}

4.2柔性数组的使用 

 示例:

#include<stdio.h>
#include<stdlib.h>
//代码1
typedef struct st_type
{
	int n;
	int a[0];//柔性数组成员
}type_a;

int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
	if (p == NULL)//检验一下
	{
		perror(malloc);
		return 1;
	}
	p->n = 100;
	for (i = 0;i < 100;i++)
	{
		p->a[i] = i;
	}
//扩容
	type_a* ptr = (type_a*)realloc(p, sizeof(type_a) + 5 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	//业务处理...
	free(p);
    p=NULL;
	return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

4.3柔性数组的优势

上述代码还有如下第二种实现方式:

#include<stdio.h>
#include<stdlib.h>
//代码2
struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		return 1;
	}
	ps->arr = (int*)malloc(5 * sizeof(int));
	if (ps->arr == NULL)
	{
		return 1;
	}
	//使用
	ps->n = 100;
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		ps->arr[i] = i;
	}
	//调整数组大小
	int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	//使用
	//...

	//释放
	free(ps->arr);
    ps->arr=NULL;
	free(ps);
    ps=NULL;
	return 0;
}

 上述两种代码都可以完成同样的功能,但是方式1的实现有2个好处:

第一个好处:方便内存释放

如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给用户。用户调⽤free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。一句话来说,malloc和free越少越好。

第二个好处:有利于访问速度

连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。

以上便是对动态内存管理一些基本概念和简易使用作以介绍,之后将以本篇为基础带大家认识到动态内存开辟中容易犯的一些问题以及经典面试题,望大家多多关注,提出宝贵意见,同时再次希望屏幕前的你能有所收获。

 

  

标签:malloc,进阶,int,free,C语言,内存,动态内存,空间,开辟
From: https://blog.csdn.net/2301_80827065/article/details/137430495

相关文章

  • 【C语言】:枚举和联合体
    这里写自定义目录标题1、枚举1.1枚举类型的声明1.2枚举类型的优点1.3枚举类型的使用2、联合体(共用体)2.1联合体类型的声明2.2联合体的特点2.3联合体大小的计算1、枚举1.1枚举类型的声明枚举顾名思义就是⼀⼀列举,把可能的取值⼀⼀列举。⼀周的星期⼀到星期⽇......
  • 经典C语言题目——打印罗汉塔图形
    打印如下图形:++++1+++22++333+444455555点击查看代码#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<string.h>intmain(){ inti=0,j=0,k=0; for(i=1;i<=5;i++)//外层循环控制行数 { for(j=1;j<=5-i;j++)//内层循环......
  • 动态内存管理
    目录1.为什么要有动态内存分配.2.动态内存分配的优点3.malloc和free3.1malloc3.2free4.calloc和realloc4.1calloc4.2realloc5.常见的动态内存的错误5.1对NULL指针的解引用操作5.2对动态开辟空间的越界访问5.3对非动态开辟内存使用free释放5.4使用free释放一块动态......
  • 1169: 大整数(指针专题)(c语言)
    题目描述输入3个大整数,位数不超过100位,按从小到大的顺序输出这三个整数。要求定义并使用如下函数比较两个大整数的大小。 intcmp(char*a,char*b) { //若大整数a大于b,返回1; //若a小于b,返回-1; //若a与b相等,返回0 }输入输入有3行,每行输入一个大整数,位数不超过1......
  • 1022: 三整数排序(c语言)
    题目描述从键盘输入三个整数x,y和z,按从大到小的顺序输出它们的值。输入输入三个整数x,y和z。输出按从大到小的顺序输出它们的值。样例输入 201618样例输出 201816#include<stdio.h>intmain(){ intx=0,y=0,z=0; scanf("%d%d%d",&x,&y,&z);......
  • c语言分支结构
    正⽂开始C语⾔是结构化的程序设计语⾔,这⾥的结构指的是顺序结构、选择结构、循环结构,C语⾔是能够实现这三种结构的,其实我们如果仔细分析,我们⽇常所⻅的事情都可以拆分为这三种结构或者这三种结构的组合。我们可以使⽤if、switch实现分⽀结构,使⽤for、while、dowh......
  • python进阶之tkinter模块
    tkinter 是Python的标准图形用户界面(GUI)工具包,用于创建桌面应用程序的用户界面。tkinter 提供了一组丰富的组件和工具,使开发者能够轻松地构建具有按钮、标签、文本框、滚动条等元素的交互式应用程序。以下是 tkinter 模块的一些主要作用:创建窗口:tkinter 提供了创建窗口......
  • 字符串进阶-字符串函数
    字符串进阶-字符串函数应用c++提供了大量的字符串函数,供我们在解题时使用。一、常用函数介绍1-长度(有返回值)a.size()或a.length()2-查找(有返回值)a.find("hello")//返回子串hello在a中第一次出现时开头字母h的下标a.find('h')//返回字符h在a中第一次出......
  • 【C语言】扫雷游戏
    目录1 扫雷游戏功能说明1.1扫雷游戏介绍1.2游戏界面2 游戏分析与设计2.1 读入用户指令2.2 地雷数据生成,处理与储存2.3地雷标记及展开2.4用户界面2.5游戏循环主体3代码实现game.hgame.ctest.c1 扫雷游戏功能说明1.1扫雷游戏介绍    使用......
  • C语言程序设计-实验报告4
    实验项目名称:选择结构程序设计实验项目类型:验证性实验日期:2024年4月1日一、实验目的1.在熟练掌握if语句和switch语句的基础上,能灵活使用if语句和switch语句进行选择结构的程序设计2.学习调试程序二、实验硬、软件环境Windows计算机、Devc6.0三、实验内容及步骤实验......