首页 > 系统相关 >【C语言】动态内存管理

【C语言】动态内存管理

时间:2024-09-23 10:20:46浏览次数:3  
标签:ps malloc 管理 int free C语言 内存 动态内存 NULL

目录

一、为什么要动态内存分配

二、malloc

三、free

四、calloc

五、realloc

六、常见动态内存分配的错误

(1)解引用NULL 指针

(2)越界访问动态分配内存

(3)用 free 释放非动态分配内存

(4)用 free 释放动态分配内存的一部分

(5)对同一块动态内存的多次释放

(6)动态分配内存忘记释放(内存泄漏)

七、动态内存经典笔试题

(1)题目1

(2)题目2

(3)题目3

(4)题目4:

八、柔性数组

(1)柔性数组的概念

(2)柔性数组的特点

(3)柔性数组的使用

(4)柔性数组的优势

九、总结 C/C++ 中程序内存区域划分


一、为什么要动态内存分配

        以往的申请内存的方法:

// 定义变量
int a = 10; //一次性开辟一块空间 - 4个字节

// 定义数组
int arr[5]; //一块连续的空间 - 20 个字节

        但是这样方法在申请内存的时候,内存的大小就固定了,不能再调整。但在有些时候,比如用 S 类型的结构体存储一个学生的信息:

struct S
{
	char name[20];
	int age;
};

        再定义一个 S 结构体类型的数组,存储一个班级所有学生的信息:

struct S s[30];

        但是一个班级的学生,如果是20个,那么分配30个就会浪费;如果是32个,分配30个又会不够。

        因此,我们就希望根据键盘的输入申请变量内存大小,或者能在后续的代码中调整变量内存的大小。这种在程序运行时,或者后续编程时,才能确定需要的内存空间大小的情况,就需要用到动态内存分配。让程序员自己申请、调整、释放空间,更灵活,但更容易出错(比如忘记释放内存,程序又一直在运行,就容易导致内存不够,其它程序无法正常运行)。

二、malloc

        函数原型:

void* malloc (size_t size);

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

        参数:

  • size 是需要分配的内存块大小(单位是字节)。
  • 如果 size 设置为0,malloc 的行为是未定义的,由编译器决定(避免出错,不要设置为0)。

        返回值:

  • 申请成功,则返回指向开辟好的空间的指针;申请失败,则返回 NULL(一定要检查返回值)
  • 返回值类型为 void*,具体是什么类型自己指定,因此需要强制转换

        示例:

int main()
{
    // 申请 10 个 int 类型变量的大小
	int* p = (int*)malloc(40); // 或者 10 * sizeof(int)
    // 检查返回值
	if (p == NULL)
	{
		perror("malloc");
		return 1;//异常返回
	}
	//使用空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

        注意:并不是想申请多少内存空间都可以,不能超过内存的大小。如下,VS2019,x86(x64 可申请的内存空间比 x86 大)环境,malloc 申请内存空间,就超过了可分配内存的大小,出现错误:

三、free

        就像在图书馆借了书不还,不还太多,那么图书馆就空了。申请了内存空间不释放,内存空间就没有了,无法使用内存就会导致意外情况的发生,即内存泄漏(这是指申请内存的程序一直运行的情况。运行结束,操作系统会自动回收内存)。

        C语言中,使用 free 函数回收内存,函数原型如下:

void free (void* ptr);

        使用:

  • 如果 ptr 指向的内存不是动态分配的,free 的行为则是未定义的。如下,VS 2019 中报错:

  • 如果 ptr 指向的 NULL,free 则什么也不做。
  • 用 free 释放空间后,将指针设置为 NULL 是必要的(否则是野指针,即指向不可用内存的指针)。应像如下写:

        因为 free 释放后, p 实际上还是存储着原来的动态分配的内存空间的地址,厚着脸皮访问也行,但是会出现问题,所以 free 释放后要置 NULL,让指针无法访问被回收的空间:

四、calloc

        函数原型:

void* calloc (size_t num, size_t size);

        作用:为 num 个大小为 size (单位字节)的元素开辟一块空间,并且把空间的每个字节初始化为0效率会变低),而 malloc 不会初始化。如下:

        malloc,没初始化:

        calloc 会初始化为0:

        因此,想效率高,选 malloc;想初始化为0,选calloc

五、realloc

        函数原型:

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

        作用:如果想要调整过去申请过的内存空间的大小,就使用 relloc。

        参数:

  • ptr:要调整的内存地址。
  • size:调整之后新的内存空间大小(单位为字节)。
  • 如果 ptr 为NULL,则与 malloc 是一样的效果。如下:
//realloc函数也可以实现malloc的功能
int main()
{
	realloc(NULL, 20);//等价于 malloc(20)

	return 0;
}

        返回值:

  • 为调整后内存的起始位置。
  • 三种情况:

        情况1: ptr 指向的原有空间之后,有足够的空间。(返回 ptr)

        情况2: ptr 指向的原有空间之后,没有足够的空间,但能找到新的 size 大小的连续空间。(返回新的地址)

        情况3: ptr 指向的原有空间之后,没有足够的空间,并且不能找到新的 size 大小的连续空间。(返回 NULL)

        示例:

// 申请 20 Byte 空间
int * ptr = (int*)malloc(20);

//20 Byte 空间不够,调整为 40 Byte
int* p = (int*)realloc(ptr, 40);

        三种情况图解:

        不能把返回值直接赋值给 ptr,而是赋值给一个新的指针变量 p 。因为如果 realloc 失败,会返回 NULL,这时直接把返回值赋值给 ptr,就哪内存空间也用不了了。下面是正确的示例:

        注意:旧的空间操作系统会自动回收,所以不需要 free。

        malloc、free、calloc、realloc,都包含在头文件  <stdlib.h> 中。

六、常见动态内存分配的错误

(1)解引用NULL 指针

int main()
{
	int *p = (int*)malloc(40);
	*p = 20;//如果malloc失败,那么p的值是NULL,相当于没有空间,还存值

	return 0;
}

        解决办法:在 malloc、calloc 申请空间后,一定要检查返回值

	if (ptr == NULL)
	{
		perror("malloc"); // 或 calloc
		return 1;
	}

(2)越界访问动态分配内存

int main()
{
	//误认为申请了40个空间,就是40个int的大小
	int *p = (int*)malloc(40);//10*sizeof(int)
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//NULL

	int i = 0;
	for (i = 0; i < 40; i++) //越界访问
	{
		p[i] = i;
	}

	return 0;
}

        解决办法:malloc 时,最好写成如下形式:

// 申请10个int类型的内存空间大小
int *p = (int*)malloc(10*sizeof(int));

(3)用 free 释放非动态分配内存

int main()
{
	int* p = malloc(40);
	if (p == NULL)
	{
		return 1;
	}

	int arr[5] = {0};//栈区的空间
	p = arr;

	free(p);//释放栈区的空间,出错
	p = NULL;
	return 0;
}

(4)用 free 释放动态分配内存的一部分

        不能从中间的位置开始释放,只能从头开始释放一整块动态分配的内存。

(5)对同一块动态内存的多次释放

        多次释放直接报错:

       解决办法,free 后指针置 NULL,free(NULL) 意味着不执行任何操作:

(6)动态分配内存忘记释放(内存泄漏)

        错误示范1:

        错误示范2:

七、动态内存经典笔试题

(1)题目1

        以下代码存在什么问题?

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

        图解:

        问题1:在空地址存储数据,程序崩溃,没有输出。

        问题2:返回主函数后,系统收回 p 形参的空间,但 malloc 的动态内存还存在,没有释放,内存泄漏

        解决方法1(传指针的地址):

        解决方法2(返回动态内存首地址):

(2)题目2

        以下代码存在什么问题?

char* GetMemory(void)
{
    char p[] = "hello world";
    return p;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}

int main()
{
    Test();
    return 0;
}

        图解:

        问题:返回栈空间地址,非法访问内存空间,输出未知的内容。如下图所示:

        扩展1:

        上面的代码没有问题,虽然 z 被销毁了,但是会先把返回值暂存在寄存器,返回主函数后,再从寄存器返回值。 

        扩展2:

        上面这个代码就是错的,返回值 &a 暂存在寄存器,寄存器再把返回值赋值给 p,但是此时局部变量a的内存空间已经被收回,继续通过指针p访问就是非法的,这就是返回栈空间地址的问题。

        扩展2的运行结果:

        扩展2输出未知内容的解释:

(3)题目3

        下面代码有何问题?

void Test(void)
{
    char* str = (char*)malloc(100);
    strcpy(str, "hello");

    free(str);

    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

int main()
{
    Test();
    return 0;
}

        问题:free后,指针 str 未置NULL,成为野指针,造成后续进入 if 语句,非法访问。

        解决:

(4)题目4:

        下面代码有何问题?

void GetMemory(char** p, int num)
{
    *p = (char*)malloc(num);
}

void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}

int main()
{
    Test();

    return 0;
}

        问题:未 free 释放动态内存。

八、柔性数组

(1)柔性数组的概念

        C99中,结构体允许最后一个成员是大小未知的数组,叫柔性数组。形式如下:

struct S
{
	int n;
    //柔性数组
	int arr[];//或 int arr[0]
};

        但有些编译器只支持 arr[] 的写法。

(2)柔性数组的特点

  • sizeof 返回的结构体大小不包括柔性数组的内存。如下:

  • 结构中的柔性数组成员前面必须至少有一个其他成员(否则这个结构体就不知道大小了)。
  • 包含柔性数组成员的结构体进行动态内存分配,并且分配的内存应该大于结构体的大小(多了柔性数组成员的大小)。

(3)柔性数组的使用

// 柔性数组的使用
struct S
{
	int n;
	int arr[];//柔性数组
};

int main()
{
	//struct S s;//不会这样写
	//会这样写
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);
	// 检查
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	// 使用
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	// 扩大动态内存
	struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 40);
	// 检查
	if (tmp == NULL)
	{
		perror("realloc");
		return 1;
	}
	ps = tmp;
	// 使用
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}
	// 输出
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	// 释放
	free(ps);
	ps = NULL;

	return 0;
}

        运行结果:

        动态内存分配图解及验证:

(4)柔性数组的优势

        使用柔性数组,实现了:

  • 结构体的所有内存空间都是在堆上开辟的。
  • 数组的大小是可以调整的。

        也可以用其它的方法(不使用柔性数组),来实现上面的效果。给结构体变量分配动态的内存空间;把柔性数组成员替换成指针,让指针指向一块动态分配内存空间。代码如下:

struct S2
{
	int n;
	int* arr;
};

int main()
{
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2));
	if (ps == NULL)
	{
		perror("malloc-1");
		return 1;
	}
	ps->n = 100;
	ps->arr = (int*)malloc(5 * sizeof(int));
	if (ps->arr == NULL)
	{
		perror("malloc-2");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i + 1;//6 7 8 9 10
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);//1 2 3 4 5 6 7 8 9 10
	}

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

	return 0;
}

        动态内存分配图解及验证:

        对比两种方法,柔性数组的优势:

  • 内存释放方便:柔性数组只需要释放一次内存,而方法2要分别释放两次(结构体的空间 + arr指针指向的空间。两块不连续的空间,起始地址不同,要分别释放)。
  • 内存碎片更少,避免内存空间的浪费:方法2不连续的内存空间更多,内存中不连续的空间太多,会导致剩下的夹缝中的内存很细碎,无法存储其它需要大块连续内存空间的数据,从而造成空间浪费。
  • 连续的内存有利于提高访问速度

        扩展阅读:C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell

九、总结 C/C++ 中程序内存区域划分

        内存映射段目前不需要知道是什么东西;代码段是存放程序编译之后,代码和常量的二进制指令。

        以左边的代码为例子:紫色框出的是全局变量、静态变量,存放在数据段;绿色框出的是局部变量、有关函数调用的信息,存放在栈区;黄色框出来的是动态分配的内存空间,在堆区;红色框出来的是初始化局部变量的常量,存放在代码段。

        总结:

  • 栈区(stack):① 函数的局部变量、形参、返回值、返回到的地址等函数信息,都在栈区创建函数执行结束,存储这些信息的空间也会被自动释放。② 栈区的内存分配在处理器的指令集中运算,效率高,但可分配的空间有限
  • 堆区(heap):① 由程序员释放,程序员不释放,程序结束后会被操作系统回收。② 分配方式类似于链表。
  • 数据段(静态区):存放全局变量、静态变量,程序结束后由系统释放。
  • 代码段:存放函数体的二进制代码。

标签:ps,malloc,管理,int,free,C语言,内存,动态内存,NULL
From: https://blog.csdn.net/2401_86272648/article/details/142423039

相关文章

  • Spring Boot+MinIO实战:掌握分片上传、秒传与断点续传,让文件管理更高效!
    在现代应用中,随着文件大小的不断增大和网络环境的复杂化,传统的文件上传方式已难以满足用户需求。通过将SpringBoot与MinIO集成,可以轻松实现文件的分片上传、秒传和续传功能,为用户提供更流畅的上传体验。分片上传分片上传是将大文件拆分成多个小块分别上传,避免单次上传大文件带......
  • 花园管理系统
    基于springboot+vue实现的花园管理系统 (源码+L文+ppt)4-0744功能结构  为了更好的去理清本系统整体思路,对该系统以结构图的形式表达出来,设计实现该“花开富贵”花园管理系统的功能结构图如下所示:图4-1系统总体结构图4.1数据库设计  4.1.1基于MySQL数据库的存......
  • 免费分享源码 SSM酒店信息管理系统 计算机毕业设计论文41731
    摘 要酒店信息管理系统是一种基于计算机技术的管理工具,旨在提高酒店业务效率和服务质量。该系统通过集成多个功能模块,实现酒店各项业务的自动化管理,包括客房信息管理、预订信息管理、入住信息管理、退房信息管理、续费信息管理等。该系统可以大大提高酒店管理的效率,减少......
  • springboot中小学数字化教学资源管理平台
    基于springboot+vue实现的中小学数字化教学资源管理平台 (源码+L文+ppt)4-078  第4章系统设计   4.1功能模块设计  系统整体模块分为管理员、教师和学生三大用户角色,整体功能设计图如下所示:图4-1系统整体功能图4.2数据库设计  4.2.1E-R模型结构设......
  • 企业文件管理,警惕破窗效应!
    破窗效应环境中的不良现象如果被忽视或者放任不管,就会诱使人们去模仿和仿效,进而导致更多的不良行为。企业文件管理过程中,出现风险行为不及时管理,就会放任不良行为,引发更多安全隐患。一些企业不重视文件管理,员工办公、离职可能会删除、带走重要文件,企业却无法有效监管和追......
  • 基于Python+Vue开发的体育场馆预约管理系统
    项目简介该项目是基于Python+Vue开发的体育场馆预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的体育场馆预约管理系统项目,大学生可以在实践中学习和提......
  • 基于Python+Vue开发的房产销售管理系统
    项目简介该项目是基于Python+Vue开发的房产销售管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的房产销售管理系统项目,大学生可以在实践中学习和提升自己的......
  • 基于Python+Vue开发的酒店客房预订管理系统
    项目简介该项目是基于Python+Vue开发的酒店客房预订管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的酒店客房预订管理系统项目,大学生可以在实践中学习和提......
  • 基于Python+Vue开发的口腔牙科预约管理系统
    项目简介该项目是基于Python+Vue开发的口腔牙科预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的口腔牙科诊所预约管理系统项目,大学生可以在实践中学习......
  • 基于Python+Vue开发的健身房管理系统
    项目简介该项目是基于Python+Vue开发的健身房管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的健身房管理系统项目,大学生可以在实践中学习和提升自己的能力,......