动态内存管理那些事:malloc、calloc、realloc、free、柔性数组
c/c++ linux服务器开发学习地址:
在之前我们都是这样开辟空间的:
int i = 20; //在栈空间开辟4个字节
char arr[10] = { 0 }; //在栈空间开辟10个字节的连续空间
特点
1️、开辟的空间大小是固定的
2️、数组在声明的时候,必需包含常量值 (指定数组长度)
小结
以往开辟空间的方式不够灵活,有很大的局限性 (有时候我们需要的空间大小在程序运行的时候才能知道)
所以这篇文章主要了解在内存堆上开辟空间所使用的函数
malloc和free
这两个函数总是成对出现的,一个开辟内存,一个释放内存,这两个函数的单独使用极有可能会导致程序出错。
动态内存开辟的函数malloc
函数原型 void* malloc (size_t size);
函数说明
- 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。 如果开辟成功,则返回一个指向开辟好空间的指针
- size_t size表示开辟几个字节大小的空间
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。
动态内存释放函数free
函数原型 void free (void* ptr);
函数说明
- -ptr 传过来的是开辟空间的起始地址, 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的,如果为空指针什么也不会发生
- 如果参数 ptr 是NULL指针,则函数什么事都不做。 free函数释放内存空间后,并不会将接受开辟空间起始地址的的指针置为空指针
malloc和free都声明在 stdlib.h 头文件中,接下来我举一个开辟内存释放内存的例子:
这里有几个细节的地方学要注意:
malloc开辟空间后,free函数释放P指向的内存空间,但不会把p指针里面地址的内容释放,这可能就会造成,p又通过地址访问之前的内存空间,造成内存非法访问,所以一定要手动的把把P置为NULL
calloc
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。
函数原型如下: void* calloc (size_t num, size_t size); 函数说明:
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
举个例子我们使用calloc开辟10个整形空间的大小
打印calloc开辟空间里面的内容如图:
所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
realloc
realloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理地管理内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下: void* realloc (void* ptr, size_t size);
函数说明:
- 这个函数可以在原有的其它内存函数开辟空间的基础上,继系管理空间的大小,也可以自己重新开辟一块新的内存空间,开辟空间时不初始化里面的内容。
- ptr 是要调整的内存地址(原内存的起始地址), size 为调整之后内存空间的新大小 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间时,如果原内存空间后面有足够的空间则开辟相应的空间,如果原内存空间后面没有足够的空间可以开辟,就在堆区重新找一块空间开辟内存,之后还会将原来内存中的数据移动到新 的空间。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
以下面的的代码为例,下图分析两种情况:
情况一:
情况二:
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。当是情况2 的时候,如果原内存空间后面没有足够的空间可以开辟,就在堆区重新找一块空间开辟内存,之后还会将原来内存中的数据移动到新的空间。
对NULL指针的解引用操作
空指针就是没有任何指向的指针,不能对它进行解引用,加减整数等操作,任何对它的操作都是不和法的,都会造成程序的崩溃。malloc函数在开辟内存失败时,会返回空指针,所以在对malloc函数开辟的空间进行使用之前要判断是否为返回空指针。
对动态开辟空间的越界访问
其实这个很好理解,就像静态的创建10个数组元素,你可不能访问20个元素啊;如下面的代码,malloc动态的开辟了10整形大小的空间,下面使用空间时却访问了40个整形元素,这也是不合法的。
对非动态开辟内存使用free释放
通过什么方式创建,就要通过什么方式释放
使用free释放一块动态开辟内存的一部分free(p),p一定要指向开辟空间的起始位置,这样才能释放开辟的整块动态空间,如果p因为使用原因进行了移动一定要定义另一个指针记录p的开始指向位置,否则进行的内存释放是局部的内存释放。下面代码中通过指针的移动对空间内容进行了赋值,p发生了移动,但没有记录p的起始位置。
对同一块动态内存多次释放
对于开辟的一块动态空间,一次释放就行,但有时由于程序的复杂,在多个函数里面使用这块空间,也就可能会进行多次释放。
动态开辟内存忘记释放(内存泄漏)
如下p是局部变量,出了这个函数就销毁了,下面的main 函数里面就不能使用了,之后就找不到p原来指向的空间的内容了,下面就不能把这块空间释放掉,造成内存泄漏,时间一长会消耗很多内存,如果服务器里面有内存泄漏,导致这个服务器崩溃。
题目一:
首先思考这个代码有哪些问题??
这个代码有两个问题:
❌1. 对空指针的使用 ❌ 2. 存在内存泄漏
图示解释
代码修改的两种方法
在函数调用时,传值调用是无法改变实参的大小的,要传地址。
题目二:
❌静态开辟的空间是在栈上,栈上开辟的空间出了作用域就销毁了,动态开辟的空间是在堆上开辟的,要么自己手动释放空间,要么程序结束自动释放空间。所以下面的代码,随机打印内存中的一些值,没有达到预想到的效果。
题目三
❌返回&x是无效的,局部变量返回地址是无效的
题目四
❌开辟空间没有free释放掉
题目六
❌动态空间开辟好后,free释放开辟的空间,那么维护开辟空间起始位置的指针也应该置为空指针,这是紧紧相连的步骤,不可缺少。虽然free(str)释放了开辟好的空间,但str里面任然存储着开辟空间的起始地址,free不会释放strl里面的内容的,导致非法访问,导致程序错误。
C/C++程序内存分配的几个区域如下图: 不同的变量,程序在内存中占有不同的区域,理解他们所在的区域,理解他们的作用域与生命周期,可以帮助我们更好地编写程序。
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3、数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。
编译产生的可执行代码(可执行程序)是放在代码段区域的,常量字符串
也是放在代码段里面的。
有了这幅图,我们就可以更好地理解static关键字修饰的局部变量了
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。
柔性数组(flexible array)这个概念我们很少听到,但是它确实是存在的。 C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。我自己对于柔性数组的理解就是,柔性数组是定义在结构体当中的一个成员,它的起始大小为零,在使用过程中,根据情况的需要,通过动态内存开辟函数改变它的大小,达到数组内容改变的效果。
如:
柔性数组的特点:
- 结构体中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应 柔性数组的预期大小。
如下代码进行结构体的大小的计算:
下面为柔性数组的简单使用:
代码一
首先数组元素个数为10个,不够时再动态的开辟为20个
可能有老铁觉得,柔性数组的存在是多余的,没有柔性数组,我也可以可以创建一个结构体的数组成员,实现动态的变化,那我们通过下面的代码简单的来分析一下吧,我们不使用柔性数组,实现上面柔性数组的功能,对比一下
代码二
通过以上代码和图示分析,为了实现柔性数组的功能,我们通过上面的代码会存在如下的问题:
上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:
第一个好处是:方便内存释放包含柔性数组的空间开辟只需要一次malloc,不包含的需要两次malloc,两次free释放内存
第二个好处是:再由空间局部性原理可知,这样有利于访问速度.连续的内存有益于提高访问速度,也有益于减少内存碎片。多次开辟堆上面的空间,导致堆空间残留许多未被利用的内存块(内存碎片),占用内存,影响程序运行。
信号量函数 (semget、semctl、semop)及示例
Linux进程间通信之信号量(semaphore)是变量,是一种特殊的变量。它紧取正值。对信息号量的操作只有2中:
- 等待(wait)
- 发送信号(signal)
信号量比较难理解。下面我们一个个的看一下各个函数。与信号量处理的函数有:semget();semctl();semop();
与共享内存的shmget()函数类似。
使用格式:
功能:创建一个新的信号量或获取一个已经存在的信号量的键值。
返回值:成功返回信号量的标识码ID。失败返回-1;
参数:
_key 为整型值,用户可以自己设定。有两种情况:
1、键值是IPC_PRIVATE,该值通常为0,意思就是创建一个仅能被进程进程给我的信号量。
2、键值不是IPC_PRIVATE,我们可以指定键值,例如1234;也可以一个ftok()函数来取得一个唯一的键值。
_nsems 表示初始化信号量的个数。比如我们要创建一个信号量,则该值为1.,创建2个就是2。
_semflg :信号量的创建方式或权限。有IPC_CREAT,IPC_EXCL。
IPC_CREAT如果信号量不存在,则创建一个信号量,否则获取。
IPC_EXCL只有信号量不存在的时候,新的信号量才建立,否则就产生错误。
下面我们看个小例子:
运行结果为:
我们可以用ipcs –s 来查看是否创建成功。
用ipcrm -s semid号来删除指定的信号量。
需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
=========================================================================
这是控制信号量的函数,在这个函数中我们可以删除信号量或初始化信号量。
格式:
功能:控制信号量的信息。
返回值:成功返回0,失败返回-1;
参数:
_semid 信号量的标志码(ID),也就是semget()函数的返回值;
_semnum, 操作信号在信号集中的编号。从0开始。
_cmd 命令,表示要进行的操作。
下面列出的这些命令来源于百度!
参数cmd中可以使用的命令如下:
IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID将信号量集从内存中删除。
GETALL用于读取信号量集中的所有信号量的值。
GETNCNT返回正在等待资源的进程数目。
GETPID返回最后一个执行semop操作的进程的PID。
GETVAL返回信号量集中的一个单个的信号量的值。
GETZCNT返回这在等待完全空闲的资源的进程数目。
SETALL设置信号量集中的所有的信号量的值。
SETVAL设置信号量集中的一个单独的信号量的值。
Semunion ;第4个参数是可选的;semunion :是union semun的实例。
=========================================================================
格式:
功能:用户改变信号量的值。也就是使用资源还是释放资源使用权。
返回值:成功返回0,失败返回-1;
参数:
_semid : 信号量的标识码。也就是semget()的返回值。
_sops是一个指向结构体数组的指针。
sem_num: 操作信号在信号集中的编号。第一个信号的编号为0;
sem_op : 如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
_semflg IPC_NOWAIT //对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
IPC_UNDO //程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
nsops:操作结构的数量,恒大于或等于1。
下面看个例子:
源文件1:
源程序2:
运行结果:
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。