在 C 语言编程的世界里,malloc函数无疑是我们动态分配内存的得力助手。我们经常使用它来申请一块内存空间,让程序在运行时根据实际需求灵活地分配和使用内存。然而,你是否曾好奇过,当我们调用malloc函数时,背后究竟发生了什么?它是如何在看似杂乱无章的内存中找到一块合适的空间分配给我们的?又是怎样管理这些已分配和未分配的内存块的呢?今天,就让我们一起深入剖析malloc函数的实现原理与机制,揭开这一神秘面纱。
内存是计算机中必不可少的资源,因为 CPU 只能直接读取内存中的数据,所以当 CPU 需要读取外部设备(如硬盘)的数据时,必须先把数据加载到内存中。内存分配有三种方式:
从静态存储区分配,生命周期随程序的结束而结束,比如全局变量,静态变量。
从栈空间分配,函数调用结束后自动释放。
从堆空间分配,即动态内存开辟,如malloc、calloc、realloc。
一、Malloc函数概述
1.1基本定义与作用
在 C 语言中,malloc函数可是动态存储管理的标准库函数之一,发挥着极为重要的作用。它主要用于在内存的动态存储区按照指定的字节数来分配一块连续的空间。打个比方,如果我们在程序中需要一块大小不确定的内存区域来存储一些数据,像创建动态大小的数组或者存放一些临时生成的数据结构等情况,就可以借助malloc函数来获取合适的内存空间。
而且,malloc函数的返回值也很关键,它返回的是所分配连续存储域起始地址的指针,这个指针就如同一把钥匙,后续我们可以通过它来访问和操作这块分配好的内存区域。不过要注意哦,分配成功得到的这块内存里,初始值是不确定的,也就是里面的数据是随机的,需要我们根据实际需求去进一步初始化处理呢。当我们不再使用这块内存的时候,为了避免造成内存泄漏等问题,一定要使用free函数将其释放掉,归还给系统,方便后续其他部分的程序继续申请使用内存。
1.2函数原型与头文件
malloc函数的原型是void *malloc(size_t n)。这里面的size_t其实是一种数据类型,它是通过typedef重定义而来的,目的就是让使用者一看就明白这个参数代表的是一个长度,并且通常和整型相关。它的含义就是用于指明要分配的内存空间大小,单位是字节哦。
在使用malloc函数时,可别忘了包含对应的头文件
所以在 C++ 中,如果写成像p = malloc (sizeof(int));这样的代码,程序是无法通过编译的,会报错提示 “不能将void*赋值给int *类型变量”,这时候就需要通过(int *)这样的强制类型转换,把返回的指针转换为实际需要的类型指针。而对于 C 语言来说,虽然没有这个强制要求,但为了让 C 程序更方便地移植到 C++ 环境中,也建议养成进行强制类型转换的好习惯呢。
⑴关于malloc相关的几个函数
关于malloc我们进入Linux man一下就会得到如下结果:
SYNOPSIS
#incLude <stdlib. h>
void *calloc( size_ _t nmemb,
size_t size);
void *malloc(size t size);
void free(void *ptD);
void *realloc(void *ptr, size_ t size);
DESCRIPTION
calloc()
allocates memory for an array of nmemb elements of size bytes
each and ! returns a pointer to the allocated memory; The memory is set
to
zero,
If nmemb or size is 0,then callocO) returns either NULL, or
a unique pointer value that can later be successfully passed to free().
malloc( )
allocates size bytes and returns a pointerto the allocated
memory. The memory is not cleared.
If size
is 0,
then malloc()
returns
either NULL: or a unique pointer value that canlater be suc-
cessfully passed to free().
也可以这样认为(window下)原型:
extern void *malloc(unsigned int num_bytes);
头文件:
#include<malloc.h>或者#include<alloc.h>两者的内容是完全一样的
如果分配成功:则返回指向被分配内存空间的指针,不然返回指针NULL 。同时,当内存不再使用的时候,应使用free()函数将内存块释放掉。
关于:void*,表示未确定类型的指针,c,c++规定void*可以强转为任何其他类型的指针,关于void还有一种说法就是其他任何类型都可以直接赋值给它,无需进行强转,但是反过来不可以 。
malloc分配的内存大小至少为参数所指定的字节数,malloc的返回值是一个指针,指向一段可用内存的起始位置,指向一段可用内存的起始地址,多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)实现malloc时应同时实现内存大小调整和内存释放函数(realloc和free) malloc和free是配对的,如果申请后不释放就是内存泄露,如果无故释放那就是什么也没做,释放只能释放一次,如果一块空间释放两次或者两次以上会出现错误(但是释放空指针例外,释放空指针也等于什么也没做,所以释放多少次都是可以的。)
⑵malloc和new
new返回指定类型的指针,并且可以自动计算所需要的大小。
int *p;
p = new int;//返回类型为int* ,分配的大小是sizeof(int)
p = new int[100];//返回类型是int*类型,分配的大小为sizeof(int)*100
而malloc需要我们自己计算字节数,并且返回的时候要强转成指定类型的指针。
int *p;
p = (int *)malloc(sizeof(int));
malloc的返回是void*,如果我们写成了:p=malloc(sizeof(int));间接的说明了(将void转化给了int*,这不合理)
malloc的实参是sizeof(int),用于指明一个整型数据需要的大小,如果我们写成p=(int*)malloc(1),那么可以看出:只是申请了一个一个字节大小的空间。
malloc只管分配内存,并不能对其进行初始化,所以得到的一片新内存中,其值将是随机的。
一般意义上:我们习惯性的将其初始化为NULL,当然也可以使用memset函数。
简单的说:malloc函数其实就是在内存中找一片指定大小的空间,然后将这个空间的首地址给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上是连续的,而在物理上可以不连续。我们作为程序员,关注的是逻辑上的连续,其他的操作系统会帮着我们处理。
二、Malloc函数的实现原理
2.1空闲链表机制
在malloc函数背后,有着依靠空闲链表来管理内存的一套机制。当我们调用malloc函数时,它会沿着空闲链表去查找满足用户请求大小的内存块。比如说,链表上有多个不同大小的空闲内存块,它就会依次遍历这些块来找到合适的那一个。
找到合适的内存块后,如果这个内存块比用户请求的大小要大,那么就会按需将其分割成两部分,一部分的大小刚好与用户请求的大小相等,这部分就会分配给用户使用,而剩下的那部分则会被放回空闲链表中,等待后续其他的内存分配请求再进行分配。例如,空闲链表中有一个 50 字节的空闲块,而用户请求分配 20 字节的内存,这时malloc就会把这个 50 字节的块分成 20 字节(分配给用户)和 30 字节(放回空闲链表)两块。
而当我们使用free函数释放内存时,相应被释放的内存块又会被重新连接到空闲链上。这样,整个空闲链表就处于一个动态变化的过程,不断地有内存块被分配出去,也不断地有释放的内存块回归链表,以实现内存的循环利用,避免浪费。不过,随着程序不断地分配和释放内存,空闲链有可能会被切成很多的小内存片段,要是后续用户申请一个较大的内存片段时,空闲链上可能暂时没有可以满足要求的片段了,这时malloc函数可能就需要进行一些整理操作,比如对这些小的空闲块尝试合并等,以便能满足较大内存请求的情况。
虚拟内存地址和物理内存地址
为了简单,现代操作系统在处理物理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序层面,当涉及内存地址时,都是使用的虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下每个进程的虚拟地址空间为264Byte。
这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能如此大的空间,实际能用到的空间大小取决于物理内存的大小。由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转化为物理内存地址,才能实现对内存数据的操作。这个转换一般由一个叫MMU的硬件完成。
页与地址构成
在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页为单位。一个内存页是一段固定大小的连续的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096 Byte。所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:
上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4k,所以页内偏移都是用低12位表示,而剩下的高地址表示页号 MMU映射单位并不是字节,而是页,这个映射通过差一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如TLB等机制, 下面给出一个经过简化的内存地址翻译示意图:
内存页与磁盘页
我们知道一般将内存看做磁盘的缓存,有时MMU在工作时,会发现页表表名某个内存页不在物理内存页不在物理内存中,此时会触发一个缺页异常,此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详述。
真实地址翻译流程:
Linux进程级内存管理
内存排布:明白了虚拟内存和物理内存的关系及相关的映射机制,下面看一下具体在一个进程内是如何排布内存的。以Linux 64位系统为例。理论上,64bit内存地址空间为0x0000000000000000-0xFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分。具体分布如图所示:
对用户来说主要关心的是User Space。将User Space放大后,可以看到里面主要分成如下几段:
Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码) Data:这里存放的是初始化过的全局变量
BSS:这里存放的是未初始化的全局变量
Heap:堆,这是我们本文主要关注的地方,堆自底向上由低地址向高地址增长
Mapping Area:这里是与mmap系统调用相关区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存空间,本文不考虑这种情况,这个区域由高地址像低地址增长Stack:栈区域,自高地址像低地址增长 。
Heap内存模型:一般来说,malloc所申请的内存主要从Heap区域分配,来看看Heap的结构是怎样的。
Linux维护一个break指针,这个指针执行堆空间的某个地址,从堆开始到break之间的地址空间为映射好的,可以供进程访问,而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
2.2在操作系统中的实现
以常见的操作系统为例,malloc函数需要通过系统调用来从内核申请内存,像brk(用于堆内存)或者mmap(用于内存映射)就是常用的手段。
对于brk系统调用,它主要的作用是调整堆顶的位置,使得堆内存可以从低地址向高地址增长,以此来扩大进程在运行时的堆大小。一般来说,如果分配的内存小于 128K 时,就常常会使用brk调用来获得虚拟内存。比如在一些小型的数据结构动态分配场景中,brk就能很好地满足需求。当使用brk分配了一段新的虚拟内存区域后,要注意这并不会立即分配物理内存哦,实际的物理内存分配通常是在访问新分配的虚拟内存区域时,如果发生了缺页异常,操作系统才会开始分配并映射相应的物理内存页面。
而mmap系统调用则是在进程的虚拟地址空间中寻找一块空闲的虚拟内存,从而获得一块可以操作的堆内存,当需要分配较大块的内存(通常大于 128K 时),就会更多地借助mmap来完成申请操作。一旦通过mmap建立了内存映射关系,进程就可以通过指针的方式来读写这块内存了,并且系统会自动将脏页(被修改的页)回写到相应的磁盘文件上。
在内存分配程序初始化时,要完成诸如将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向管理的内存的指针等操作,这些都是为了后续能更好地追踪要分配和回收哪些内存。在整个过程中,会不断地去记录内存的分配和回收情况,比如哪些内存块已经分配出去被使用了,哪些又被释放回到了可分配的状态等,通过这些精细的管理,才能让内存资源在程序运行过程中得到合理的调配。
⑴brk与sbrk
由上文知道,要增加一个进程实际上的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:
int brk(void *addr);
void *sbrk(inptr_t increment);
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置为errno为ENOMEM,sbrk成功时返回break移动之前所指向的地址,否则返回(void*)-1;
⑵资源限制和rlimirt
系统为每一个进程所分配的资源不是无限的,包括可映射的空间,因此每个进程有一个rlimit表示当前进程可用的资源上限,这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit 其中rlimt是一个结构体
struct rlimit
{
rlimt_t rlim_cur;
rlim_t rlim_max;
};
每种资源有硬限制和软限制,并且可以通过setrlimit对rlimit进行有条件限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制
2.3ptmalloc 工作原理
在涉及ptmalloc模块的情况下,先来了解一下它的软件架构。ptmalloc中有几个关键概念,比如malloc_state、malloc_chunk等。
malloc_state结构用于统一管理内存分配相关的诸多信息,它里面包含了像fastbinsY(这是用于存储 16 - 160 字节chunk的空闲链表)、top(代表着顶部的内存块,也就是当其他空闲链表中没有匹配的chunk分配给用户程序时,会从这里裁剪出可用的chunk分配给用户)、bins(又可细分为unsortedbins、smallbins、largebins,unsortedbins是chunk缓存区,用于存储从fastbins合并的空闲chunk;smallbins用于存储 32 - 1024 字节的chunk;largebins则用于存储大于 1024 字节大小的空闲chunk)以及binmap(可用bins位图,方便快速查找可用的bin)等重要成员。
而malloc_chunk则是以其为单位来进行内存的申请和释放操作。每个malloc_chunk结构体中有记录前一个chunk大小的mchunk_prev_size成员、表示当前chunk大小的mchunk_size成员,还有像fd(链表后驱指针)、bk(链表前驱指针)等指针成员(当chunk处于空闲状态时,会借助内存区域前 16 个字节作为链表指针,将chunk插入到相应的空闲链表中)。
在内存管理方面,不同大小的内存块有着不同的管理方式。对于小于 160 字节的内存申请,malloc函数会从fastbins空闲链表中查找匹配的chunk进行分配;对于 32 - 1024 字节的内存请求,就会去smallbins中寻找合适的空闲chunk;大于 1024 字节的则在largebins里查找。
当用户申请内存时,malloc会按照对应的大小范围去相应的链表中寻找可用的chunk,找到就分配给用户使用。而当用户释放内存时,释放的chunk会依据其大小等情况,被合理地放回fastbins、bins等相应的链表中,比如从smallbins中释放的chunk可能会先进入unsortedbins缓存,后续再根据具体情况进行合并或者重新分配等操作,以此来维持整个内存分配和回收体系的高效、有序运行。
三、Malloc函数
3.1数据结构
首先我们要确定所采用的数据结构。一个简单可行方案是将堆内存空间以块的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址,可以使用如下结构体定义一个block
typedef struct s_block *t_block;
struck s_block{
size_t size;//数据区大小
t_block next;//指向下个块的指针
int free;//是否是空闲块
int padding;//填充4字节,保证meta块长度为8的倍数
char data[1];//这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta
};
3.2寻找合适的block
现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块 Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块 两种方式各有千秋,best fit有较高的内存使用率(payload较高),而first fit具有较高的运行效率。这里我们采用first fit算法
t_block find_block(t_block *last,size_t size){
t_block b = first_block;
while(b&&b->size>=size)
{
*last = b;
b = b->next;
}
return b;
}
find_block从first_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL,这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block.这是为了如果找不到合适的block而开辟新block使用的。
3.3开辟新的block
如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:
#define BLOCK_SIZE 24
t_block extend_heap{
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE+s)==(void*)-1)
return NULL;
b->size = s;
b->next - NULL;
if(last)
last->next = b;
b->free = 0;
return b;
};
3.4分裂block
First fit有一个比较致命的缺点,就是可能会让更小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block:
void split_block(t_block b,size_t s)
{
t_block new;
new = b->data;
new->size = b->size-s-BLOCK_SIZE;
new->next = b->next;
new ->free = 1;
b->size = s;
b->next = new;
}
3.5malloc的实现
有了上面的代码,我们就可以实现一个简单的malloc.注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE+8才执行分裂操作 由于我们需要malloc分配的数据区是按8字节对齐,所以size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:
size_t align8(size_t s)
{
if(s&0x7 == 0)
return s;
return ((s>>3)+1)<<3;
}
#define BLOCK_SIZE 24
void *first_block=NULL;
void *mallloc(size_t size)
{
t_block b,last;
size_t s;
//对齐地址
s = align8(size);
if(first_block)
//查找适合block
last = first_block;
b = find_block(&last,s);
if(b)
{
//如果可以则分裂
if((b->size-s)>=(BLOCK_SIZE + 8))
split_block(b,s);
b->free = 0;
}
else
{
//没有合适的block,开辟一个新的
b=extend_heap(last,s);
if(!b)
{
return NULL;
}
else
{
b=extend_heap(NULL,s);
if(!b)
{
return NULL;
}
first_block = b;
}
}
return b->data;
}
3.6malloc函数的使用注意事项
⑴内存分配成功判断
在使用 malloc 函数时,一定要进行内存分配成功与否的判断哦。因为 malloc 函数在执行后,有可能会由于系统内存不足等原因,导致无法按照要求分配出相应的内存空间。此时,它会返回一个 NULL 指针来表示分配失败。
例如,我们写这样一段代码:
int *p = (int *)malloc(1000000000 * sizeof(int)); // 尝试分配非常大的内存空间,可能超出系统可分配范围
if (p == NULL) {
printf("内存分配失败,无法继续执行后续操作!\n");
// 在这里可以添加一些应对分配失败的处理逻辑,比如返回错误码、进行相应提示等
return -1;
}
// 如果分配成功,就可以继续使用这块内存,例如进行赋值等操作
*p = 10;
像这样通过检查返回的指针是否为 NULL,就能知道内存分配是不是成功啦。要是忽略了这个判断,后续还继续去使用这个可能为 NULL 的指针,就很容易引发程序崩溃,比如出现段错误等情况呢,所以这一步的判断千万不能省略哦。
⑵内存释放操作
使用完 malloc 函数分配的内存后,及时用 free 函数进行释放是非常重要的操作呀。因为如果一直不释放这些申请来的内存,它们就会始终被占用,久而久之,就容易造成内存泄漏问题。内存泄漏积累起来,会不断消耗系统的可用内存资源,可能使得程序运行越来越卡顿,严重的话甚至会导致整个系统瘫痪呢。
free 函数的使用方式很简单,它的原型是 void free(void *FirstByte),参数就是之前 malloc 分配内存时返回的那个指针。举个简单的例子来说明一下:
char *str = (char *)malloc(50 * sizeof(char)); // 分配可以存放50个字符的内存空间
if (str!= NULL) {
strcpy(str, "Hello World"); // 使用分配的内存空间存放字符串
// 一些其他对这块内存的操作......
free(str); // 使用完后,用free函数释放内存
}
这样就把通过 malloc 申请的内存归还给系统了,让系统可以把这些内存再次分配给其他需要的部分使用哦。
⑶避免野指针问题
当我们使用 free 函数释放了 malloc 分配的内存后,还有一个需要特别注意的点,那就是要避免野指针的出现哦。野指针就是指向了一块已经被释放的内存区域或者是指向不确定内存位置的指针啦。
比如说,有这样一段代码:
int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr!= NULL) {
*ptr = 10;
free(ptr);
// 这里如果没有将ptr置为NULL,ptr就变成了野指针
if (ptr!= NULL) { // 这个判断其实是无效的哦,可能会意外进入
*ptr = 20; // 此时再去对这块已经释放的内存区域进行写操作,就是错误的,可能导致程序出现未定义行为,比如崩溃或者数据错乱等情况
}
}
所以呀,为了避免这种情况发生,在释放内存后,正确的做法是把对应的指针置为 NULL,像这样修改一下上面的代码:
int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr!= NULL) {
*ptr = 10;
free(ptr);
ptr = NULL; // 释放后将指针置为NULL,避免成为野指针
}
这样就能有效地防止意外访问已经释放的内存区域,减少程序出现错误的风险啦。
四、Calloc函数
calloc函数也是与free()函数配套使用的,使用方式与malloc几乎相同,也是在堆区申请动态内存空间。头文件:stdlib.h,返回类型为空指针,size_t num为元素个数,size_t size为每个元素的字节大小。
calloc函数的原型:
void* calloc(size_t num ,size_t size)
4.1calloc函数的使用
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
//calloc与malloc的区别
//1.参数的使用方式不同
//2.calloc会在返回起始地址之前,把在堆区申请的动态内存空间的每个字节都初始化为0
int* p=(int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));//0 0 0 0 0 0 0 0 0 0
}
}
//注意要释放calloc申请的那块空间
//还给操作系统,并把指针置为空
free(p);
p = NULL;
return 0;
}
4.2calloc与malloc的区别
①参数的使用方式不同
malloc(单位:字节):malloc(10 * sizeof(int));或malloc(40)
calloc:calloc(10 , sizeof(int))
②malloc的使用效率较高,因为calloc在返回在堆区申请的那块动态内存的起始地址之前,会将每个字节都初始化为0。
五、Realloc函数
5.1什么是realloc()
realloc()是C库的功能,用于为已分配的内存块增加更多的内存大小。C语言中重新分配的目的是扩展当前的存储块,同时保留原始内容。realloc()函数有助于通过malloc或calloc函数减少先前分配的内存大小。realloc代表内存的重新分配。
在C中realloc的语法:
ptr = realloc (ptr,newsize);
上面的语句在变量newsize中分配具有指定大小的新内存空间。执行完函数后,指针将返回到存储块的第一个字节。新的大小可以大于或小于以前的内存。我们不能确定新分配的块是否将指向与先前存储块相同的位置。
C语言中的realloc函数将在新区域中复制所有先前的数据。它确保数据将保持安全。例如:
#includeint main () {
char *ptr;
ptr = (char *) malloc(10);
strcpy(ptr, "Programming");
printf(" %s, Address = %un", ptr, ptr);
ptr = (char *) realloc(ptr, 20); //ptr is reallocated with new size
strcat(ptr, " In 'C'");
printf(" %s, Address = %un", ptr, ptr);
free(ptr);
return 0;
}
5.2如何使用realloc()
下面的C语言程序演示了如何在C语言中使用realloc来重新分配内存。
#include <stdio.h>
#include <stdlib.h>
int main() {
int i, * ptr, sum = 0;
ptr = malloc(100);
if (ptr == NULL) {
printf("Error! memory not allocated.");
exit(0);
}
ptr = realloc(ptr,500);
if(ptr != NULL)
printf("Memory created successfullyn");
return 0;
}
C示例中的realloc结果:
Memory created successfully
每当重新分配导致操作失败时,它都会返回空指针,并且先前的数据也将被释放。
⑴函数原型:void *realloc(void *ptr,size_f size);ptr是指向需要修改的内存块的指针,size是请求修改的大小
⑵realloc用来修改已分配的内存块的大小
⑶如果调整成功,返回值是调整大小后内存的起始位置;如果失败,则返回NULL,所以要对返回值进行判空。
⑷在扩大内存空间时会出现的两种情况:
①ptr所指向的内存后有足够的内存空间用来扩展
②ptr所指向的内存后没有足够的内存空间用来扩展
否则在堆上重新找一个大小合适的连续空间来使用,这样函数返回的是一个新的内存地址。如果新的内存空间申请成功,则会将ptr所指向的内存中的内容拷贝到新的内存空间中,ptr所指向的内存会被释放,返回新的内存地址;如果不成功,ptr所指向的内存不会被释放,函数返回NULL。
⑸p = realloc(ptr,size)函数返回值不为空时,释放内存不需要写free(ptr),只需要写free(p)
⑹申请的内存空间不会进行初始化
六、Free函数
⑴用来释放动态开辟的内存
⑵函数原型:void free(void *ptr);指针参数是指向malloc等函数动态申请的内存地址,这块内存释放后会返还给堆,虽然指针指向这块区域,但可以重新分配这块数据
⑶一般来说,free释放的是最新开辟的一个内存空间如果程序中malloc了,但是没有free,则会造成内存泄漏(即在程序运行过程中,系统会一直被申请内存,造成可用内存不断减少)
⑷在free之后,需要将ptr再次置空,即ptr = NULL;,如果不置空,后面程序如果通过ptr会再次访问到已经释放/无效的/已经被回收再利用的内存。
⑸free不能同时释放一块内存。
在使用以上函数时,需要加头文件#include
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)malloc(40);
int* ptr = p;
if (ptr == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
//自行添加使用的代码!!
//释放
free(p);
p = NULL;
}
在这个代码中,就是将malloc函数与free函数初步联用!!所以才能更合理的分配内存!!
但是在malloc函数与free函数联用的情况,由于代码的不规范,也会出现或多或少的错误!!
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int test ()
{
int* p= (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
if (1)
{
//某个成立的条件!
return 2;
}
//释放
free(p);
p = NULL;
}//该段代码,存在内存泄露的问题!
int main()
{
test();
return 0;
其实,在该段代码中,可能出现内存泄漏的问题!!
原因在于:在该段代码中:
//使用
if (1)
{
//某个成立的条件!
return 2;
}
如果条件成立,直接返回该值,但并不会继续执行代码,导致,后续的释放(//释放 free(p); p = NULL;)出现问题!
七、Malloc函数在不同场景的特点
7.1时间不确定性
在实际使用 malloc 函数时,我们需要了解它存在时间不确定性这一特点。malloc 函数的实现算法决定了其每次被调用时执行时间可能并不相同。它在运行时,首先会扫描之前由 free() 所释放的空闲内存块列表,尝试从中找到尺寸大于或等于需求的一块空闲内存。要是这块内存的尺寸恰好与要求的一致,那就直接将它返回给调用者;而要是找到的是一块较大的内存,那么 malloc 函数会对其进行分割,在把大小刚好符合要求的那部分内存返回给调用者的同时,把剩下较小的那块空闲内存块保留在空闲列表中,以便后续其他内存分配请求使用。
正是由于这样的实现机制,使得 malloc 函数每次调用的执行时间会受到诸多因素影响,比如当前空闲内存块的分布情况、链表的长度等。所以在那些对实时性要求比较高的场合,就需要充分考虑到这一点啦。比如说在中断处理程序中,就不建议调用 malloc 函数哦。因为中断处理往往要求能快速地执行完毕,尽可能减少对整个系统实时响应的影响,如果在中断里调用 malloc,其不确定的执行时间可能会导致中断处理超时,进而影响到系统的稳定性和实时性呢。
7.2线程安全性
malloc 函数的线程安全情况在不同的编译环境以及系统下是有所区别的。
在常见的 linux 或 windows 系统中,如果使用 -pthread 进行编译,此时 malloc 函数将成为线程安全的。不过呢,在 ANSI C 标准里,malloc 是不可重入的,也就是非线程安全的哦。这对于嵌入式程序员来说就需要格外留意啦,像在 keil、IAR 等编译器中使用 malloc 函数时,默认是非线程安全的情况。在这样的嵌入式编程场景中,如果存在多个线程同时可能调用 malloc 函数的情况,就需要自行采取加锁等措施来保证线程安全啦。
例如在 FreeRTOS 这个嵌入式操作系统的内存管理中,方法 3(heap_3.c)就对 malloc 进行了封装,以此来实现线程安全功能哦。其实现方式大概如下:
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll();
{
pvReturn = malloc( xWantedSize );
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
从代码里可以看到,先是通过 vTaskSuspendAll 暂停所有任务(相当于加锁的操作,避免其他线程干扰),然后调用 malloc 函数进行内存分配,之后再用 ( void ) xTaskResumeAll 恢复所有任务(相当于解锁啦),以此来保证在多线程环境下 malloc 函数调用的线程安全性呢。而对应的内存释放函数 vPortFree 也有类似的处理逻辑,先暂停任务,再调用 free 函数释放内存,最后恢复任务哦。
八、案例分析
下面通过几个简单的代码示例,来帮助大家更直观地理解 malloc 函数的使用过程。
8.1示例一:分配和使用整型数组内存
#include <stdio.h>
#include <stdlib.h>
int main() {
// 定义要分配的数组元素个数
int num_elements = 5;
// 使用malloc函数分配能存放5个整数的内存空间
int *arr = (int *)malloc(num_elements * sizeof(int));
if (arr == NULL) {
printf("内存分配失败!\n");
return -1;
}
// 对分配的内存空间进行赋值操作,模拟使用这块内存
for (int i = 0; i < num_elements; i++) {
arr[i] = i + 1;
}
// 输出数组元素,查看赋值结果
for (int i = 0; i < num_elements; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 使用完内存后,用free函数释放
free(arr);
return 0;
}
在这个示例中,首先我们使用 malloc 函数申请了能存放 5 个整数的内存空间,然后进行了赋值以及输出等操作,最后使用 free 函数将申请的内存归还给系统,避免内存泄漏。
8.2示例二:分配和操作字符串内存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 分配可以存放20个字符的内存空间
char *str = (char *)malloc(20 * sizeof(char));
if (str!= NULL) {
// 先将内存空间初始化为0,避免出现垃圾数据影响后续操作(这一步非必需,但通常是个好习惯)
memset(str, 0, 20 * sizeof(char));
// 将字符串复制到分配的内存空间中
strcpy(str, "Hello");
// 对字符串进行拼接操作,模拟进一步使用这块内存
strcat(str, " World");
printf("字符串内容: %s\n", str);
free(str);
}
return 0;
}
此示例展示了用 malloc 函数为字符串分配内存的情况,先是申请内存,接着初始化、复制字符串以及拼接字符串等操作,使用完后同样及时释放内存。
通过这些示例,大家可以看到 malloc 函数从申请内存、使用内存到释放内存这一完整的操作流程,从而更好地掌握它在实际编程中的应用。
九、全文总结
malloc 函数的实现原理涉及到空闲链表机制、在操作系统中的具体实现(通过如 brk 和 mmap 等系统调用)以及像 ptmalloc 模块(若适用)的相关工作原理等多个方面。空闲链表机制依靠不断查找、分割合适的空闲内存块以及回收释放的内存块来动态管理内存;操作系统层面的实现让我们了解到 malloc 函数如何与系统交互获取内存;而 ptmalloc 模块(在相关环境下)进一步细化了不同大小内存块的管理策略。
在使用 malloc 函数时,有诸多注意事项必须牢记于心。要时刻判断内存分配是否成功,防止因系统内存不足等原因导致的程序崩溃;使用完内存后一定要及时用 free 函数释放,避免内存泄漏问题;释放内存后还需将对应的指针置为 NULL,防止出现野指针进而引发程序的未定义行为。
同时,malloc 函数在不同场景下也有着鲜明的特点。比如存在时间不确定性,这使得在对实时性要求较高的场合(如中断处理程序)要谨慎使用;在线程安全性方面,其在不同编译环境及系统下情况各异,像在常见的 linux 或 windows 系统中使用 -pthread 编译时是线程安全的,但在 ANSI C 标准里却是不可重入(非线程安全)的,嵌入式编程场景中往往需要自行采取加锁等措施来保障线程安全。