C语言进阶第九课 --------动态内存管理

2023-10-25 18:44

本文主要是介绍C语言进阶第九课 --------动态内存管理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者前言

🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
​🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂


动态内存

  • **作者前言**
  • 动态内存的意义
  • 动态内存函数的介绍
    • malloc
    • free
    • calloc
    • realloc
  • 常见的动态内存错误
    • 对NULL指针的解引用操作
    • 对动态内存的越界访问
    • 对非动态开辟的内存进行free释放
    • 使用free释放动态开辟的内存的一部分
    • 对一块空间多次释放
    • 动态开辟内存忘记释放(内存泄漏)
  • 经典笔试题
    • C/C++中程序内存区域分布
  • 柔性数组
    • 柔性数组的特点
    • 使用柔性数组的好处
  • 总结

动态内存的意义

在我们空间的开辟有定义变量

#include<stdio.h>
int main()
{int a = 0;char arr[] = "sdsd";printf("%p\n", &a);printf("%p\n", arr);return 0;
}

甚至还有局部变量都会向内存申请空间,但是这些空间不擦长久,或者很难随意操作这些空间进行销毁,或者不能随意扩大空间
但是上述的开辟空间的方式有两个特点:

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

动态内存函数的介绍

malloc

在这里插入图片描述
这个函数的动态内存开辟函数,
size_t是一个无符号整数类型,表示需要分配的内存大小(以字节为单位)。malloc函数会在堆上分配一块指定大小的内存,并返回指向该内存块的指针。如果分配失败,则返回NULL。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * INT_MAX);if (arr == NULL){perror("malloc:");return 1;}return 0;
}

可能有一些人会利用malloc申请0个字节,这种做法本身没有错误,但是这个就是脱裤子放屁多此一举
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
在这里插入图片描述

free

有心细的小可爱就会发现,当我们程序运行结束就会释放出开辟的动态内存(被动),如果我们要自己释放就要用到free函数
在这里插入图片描述
free是C语言中用于释放动态分配内存的函数。当我们使用malloc、calloc等函数在程序运行期间动态申请内存时,需要在使用完毕后使用free函数将其释放回系统,以便其他程序或进程可以使用。

其中,ptr是指向需要释放的内存块的指针。使用free函数可以将之前通过malloc、calloc等函数动态分配的内存块释放回系统,以避免内存泄漏和浪费。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * 10);if (arr == NULL){perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++){arr[i] = i;printf("%d ", arr[i]);}free(arr);arr = NULL;return 0;
}

需要注意的是,在使用free函数释放内存块时需要注意以下几点:

只能释放之前通过malloc、calloc等函数动态分配的内存块,不能释放栈上的局部变量和全局变量等静态分配的内存;
不能重复释放同一块内存,否则会导致程序崩溃;
释放内存后,应该将指针设置为NULL,以避免出现野指针问题。

calloc

在这里插入图片描述
calloc是C语言中用于动态分配内存并进行初始化的函数。与malloc函数相似,calloc也用于在程序运行时动态分配内存,但与malloc不同的是,calloc会将分配的内存块初始化为零,而malloc不会
其中,num表示需要分配的元素个数,size表示每个元素的大小。calloc函数会分配num个大小为size的连续内存块,并将其初始化为零。如果分配成功,则返回指向分配内存块的指针;如果分配失败,则返回NULL。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)calloc(10, sizeof(int));if (arr == NULL){perror("calloc");return 1;}int i = 0;for (i = 0; i < 10; i++){arr[i] = i;}free(arr);arr = NULL;return 0;
}

realloc

realloc函数的出现让动态内存管理更加灵活,前面的malloc和calloc申请的空间无法避免申请空间过大或者过小,realloc函数就可以做到动态开辟内存的大小进行调整

realloc是C语言中用于重新分配内存的函数。在程序运行期间,我们有时需要重新分配内存块的大小,可以使用realloc函数来实现。realloc函数可以将之前分配的内存块的大小调整为新的大小,并返回指向新内存块的指针。
在这里插入图片描述
ptr是指向之前分配的内存块的指针,size是需要重新分配的内存块的大小。
如果重新分配成功,则返回指向新内存块的指针;如果分配失败,则返回NULL。需要注意的是,如果新的大小小于原来的大小,则realloc函数会截断原来的内存块;如果新的大小大于原来的大小,则realloc函数会在原来的内存块后面分配新的内存块。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * 10);int* arr1 = (int*)calloc(20, sizeof(int));if (arr == NULL){perror("malloc");}if (arr1 == NULL){perror("calloc");}//调整空间int *arr3 = (int*)realloc(arr, 2000 * sizeof(int));if (arr3 != NULL){arr = arr3;}free(arr);free(arr1);arr = NULL;arr1 = NULL;arr3 = NULL;return 0;
}

realloc函数开辟的空间情况分两种
第一种:
在这里插入图片描述
之前malloc开辟空间的后面刚好满足扩大后的空间大小就会在原来的地址处重新扩大,这种情况返回的新地址就会和旧地址的值是一样的
在这里插入图片描述

第二种:

之前malloc开辟的空间的后面不能满足扩大后的空间大小,就会放弃在原来的空间扩大,反而会重新找一块能满足条件的内存进行开辟,一次性开辟好,会将旧的空间的数据就会拷贝到新的空间,旧的空间就会销毁
在这里插入图片描述

常见的动态内存错误

对NULL指针的解引用操作

这种情况的出现是因为没有对返回值进行判断造成的

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = malloc(sizeof(int) * INT_MAX);int i = 0;for (i = 0; i < INT_MAX; i++){arr[i] = 1;}return 0;
}

如上面代码一样,没有判断是否创建成功,直接解引用操作就会报错
在这里插入图片描述

对动态内存的越界访问

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)calloc(10, sizeof(int));if (arr == NULL){perror("calloc");return 1;}int i = 0;for (i = 0; i <= 10; i++){arr[i] = i;}for (i = 0; i <= 10; i++){printf("%d ", *(arr + i));}free(arr);arr = NULL;return 0;
}

这种情况就和我们创建数组,越界访问是一样的

对非动态开辟的内存进行free释放

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int a = 0;int* p = &a;free(p);p = NULL;return 0;
}

这种情况就是懵了

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

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = calloc(10, sizeof(int));int i = 0; for (i = 0; i < 5; i++){*arr = i;arr++;}free(arr);arr = NULL;return 0;
}

在这里插入图片描述

对一块空间多次释放

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{int* arr = (int*)malloc(sizeof(int) * 10);if (arr == NULL){perror("malloc");return 1;}free(arr);free(arr);return 0;
}

解决这个错误我们可以养成一个习惯,只要释放过的空间,对应的变量全部赋值为NULL,因为free释放NULL不会有啥错误

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

include <stdio.h>
#include <stdlib.h>
#include <limits.h>
void Other()
{int* arr = (int*)malloc(sizeof(int) * 10);
}
int main()
{Other();while (1){;}return 0;
}

这样很容易造成空间不足,我们要记住谁申请,谁释放,如果没有释放交接任务也要告诉别人释放

经典笔试题

第一题

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

上面这些代码主要的错误就是当我们调用Test函数时,也会调用到 GetMemory函数,而传参传递的时变量str的值,而不是地址,最后str的值还是NULL,还造成了内存泄漏对NULL解引用就会报错,如果把 GetMemory函数更改为 GetMemory(char* *p ),然后传递str的地址
更改后:

void GetMemory(char* *p)
{*p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(&str);strcpy(str, "hello world");printf(str);free(str);str = NULL;
}
int main()
{Test();return 0;
}

还要注意的printf函数的使用没有错,是可以这样写的,这个写法只针对字符串
第二题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
char* GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
int main()
{Test();return 0;
}

这里的错误是GetMemory函数在调用结束后,p的空间销毁掉了,返回的值也就成了野指针
第三题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
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释放,内存泄漏了
第四题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
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;
}

这里的错误就是野指针,在我们释放了动态开辟的内存,str还是指向那块地址,这样就会非法访问内存,这里我们要注意释放后一定要赋值NULL

C/C++中程序内存区域分布

在这里插入图片描述
其中代码段是用来存储代码翻译成的二进制指令只读常量(字符串常量),里面的数据不可更改
在这里插入图片描述
这个图就可以很明了的知道动态内存开辟的空间是在堆区,局部变量和形式参数的空间是在栈区m全局变量和静态变量的空间是在数据段(静态区)
还要注意的是函数的空间也是在栈上创建的,函数栈帧的创建和销毁前面我已经写过,可以去看看
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
    束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
    分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
    回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
    配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

柔性数组

这个概念是在C99引入的,之前是没有的
形成柔性数组的条件:

  1. 要在结构体中
  2. 是结构体的最后一个成员,并且这个数组是一个未知大小的数组(变长数组)

写法:

struct S{int a;int arr[];//柔性数组}a1;struct S1{int a;int arr[0];//柔性数组}a2;

代码中 的两个结构体属于两种写法,

柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{int a;int arr[0];
};
int main()
{printf("%d", sizeof(struct S1);return 0;
}

在这里插入图片描述
可以看到sizeof返回的大小不包含柔性数组,假设结构体只有一个成员,而这个成员是柔性数组就会发现这个结构体的大小是0


3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{int a;int arr[0];
};
int main()
{struct S1* p = (struct S1*)malloc(sizeof(int) + 20);if (p == NULL){perror("malloc");return;}p->a = 10;int i = 0;for (i = 0; i < 4; i++){p->arr[i] = i;}free(p);p = NULL;return 0;
}

其实我们可以不使用柔性数组,我们也可以模拟出来

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{int a;int arr[0];
};
struct S2
{int a;int* arr;
};
int main()
{/*struct S1* p = (struct S1*)malloc(sizeof(int) + 20);if (p == NULL){perror("malloc");return;}p->a = 10;int i = 0;for (i = 0; i < 4; i++){p->arr[i] = i;}free(p);p = NULL;*/struct S2 a2;a2.a = 10;a2.arr = (int*)malloc(sizeof(int) * 4);if (a2.arr == NULL){perror("malloc");return;}printf("%d", sizeof(struct S2));free(a2.arr);a2.arr = NULL;return 0;
}

使用这种方法虽然可以代替柔性数组,但是使用free的次数会增多,而使用柔性数组只需使用一次free

当我们使用malloc开辟的数次越多,产生的内存碎片就会越多,内存的利用率就会下降

使用柔性数组的好处

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

总结

在这里介绍了动态内存的开辟、常见错误柔性数组,有不懂的小可爱可以私聊

这篇关于C语言进阶第九课 --------动态内存管理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/284492

相关文章

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

SpringBoot集成XXL-JOB实现任务管理全流程

《SpringBoot集成XXL-JOB实现任务管理全流程》XXL-JOB是一款轻量级分布式任务调度平台,功能丰富、界面简洁、易于扩展,本文介绍如何通过SpringBoot项目,使用RestTempl... 目录一、前言二、项目结构简述三、Maven 依赖四、Controller 代码详解五、Service

GO语言短变量声明的实现示例

《GO语言短变量声明的实现示例》在Go语言中,短变量声明是一种简洁的变量声明方式,使用:=运算符,可以自动推断变量类型,下面就来具体介绍一下如何使用,感兴趣的可以了解一下... 目录基本语法功能特点与var的区别适用场景注意事项基本语法variableName := value功能特点1、自动类型推

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

从基础到进阶详解Python条件判断的实用指南

《从基础到进阶详解Python条件判断的实用指南》本文将通过15个实战案例,带你大家掌握条件判断的核心技巧,并从基础语法到高级应用一网打尽,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录​引言:条件判断为何如此重要一、基础语法:三行代码构建决策系统二、多条件分支:elif的魔法三、

Linux系统管理与进程任务管理方式

《Linux系统管理与进程任务管理方式》本文系统讲解Linux管理核心技能,涵盖引导流程、服务控制(Systemd与GRUB2)、进程管理(前台/后台运行、工具使用)、计划任务(at/cron)及常用... 目录引言一、linux系统引导过程与服务控制1.1 系统引导的五个关键阶段1.2 GRUB2的进化优

Python进阶之列表推导式的10个核心技巧

《Python进阶之列表推导式的10个核心技巧》在Python编程中,列表推导式(ListComprehension)是提升代码效率的瑞士军刀,本文将通过真实场景案例,揭示列表推导式的进阶用法,希望对... 目录一、基础语法重构:理解推导式的底层逻辑二、嵌套循环:破解多维数据处理难题三、条件表达式:实现分支

Go语言连接MySQL数据库执行基本的增删改查

《Go语言连接MySQL数据库执行基本的增删改查》在后端开发中,MySQL是最常用的关系型数据库之一,本文主要为大家详细介绍了如何使用Go连接MySQL数据库并执行基本的增删改查吧... 目录Go语言连接mysql数据库准备工作安装 MySQL 驱动代码实现运行结果注意事项Go语言执行基本的增删改查准备工作